Spring的OAuth2实施多重身份验证的完整代码已上传到a file sharing site at this link。下面给出了说明,只需几分钟即可在任何计算机上重新创建当前问题。
<小时/> 当前问题:
大多数身份验证算法都能正常运行。程序不会中断,直到下面显示的控制流程结束。具体而言,在下面的 SECOND PASS 结尾处会抛出Invalid CSRF token found for http://localhost:9999/uaa/oauth/token
错误。上面链接中的应用是通过向此authserver
app的Spring Boot OAuth2 GitHub sample添加自定义OAuth2RequestFactory
,TwoFactorAuthenticationFilter
和TwoFactorAuthenticationController
而开发的。 为解决此CSRF令牌错误并启用双因素身份验证,需要对以下代码进行哪些具体更改?
我的研究让我怀疑CustomOAuth2RequestFactory
(API at this link)可能是配置解决方案的地方,因为它定义了管理AuthorizationRequest
和TokenRequest
的方法。
This section of the official OAuth2 spec表示向授权终端发出的请求的state
参数是添加csrf
令牌的位置。
此外,链接中的代码使用the Authorization Code Grant Type described at this link to the official spec,这意味着流中的步骤C不会更新csrf
代码,从而触发步骤D中的错误。(您可以查看整个流程包括the official spec中的步骤C和步骤D.)
<小时/> 围绕当前错误的控制流程:
在下面的流程图中, SECOND PASS 到TwoFactorAuthenticationFilter
期间会出现当前错误。一切都按预期工作,直到控制流进入 SECOND PASS 。
以下流程图说明了可下载应用程序中代码使用的双因素身份验证过程的控制流程。
具体来说,HTTP
和POST
序列的Firefox GET
标题显示,序列中的每个请求都会发送相同的XSRF
Cookie。在XSRF
之后,POST /secure/two_factor_authentication
令牌值不会导致问题,这会触发/oauth/authorize
和/oauth/token
端点上的服务器处理,/oauth/token
会抛出Invalid CSRF token found for http://localhost:9999/uaa/oauth/token
{1}}错误。
要了解上述控制流程图与/oauth/authorize
和/oauth/token
端点之间的关系,您可以在单独的浏览器窗口中并排比较上述流程图with the chart for the single factor flow at the official spec。上面的 SECOND PASS 只是第二次执行单因素官方规范的步骤,但在 SECOND PASS 期间拥有更多权限。
HTTP请求和响应标头指示:
1。)9999/login
向username
和password
提交正确9999/authorize?client_id=acme&redirect_uri=/login&response_type=code&state=sGXQ4v
的结果,重定向到GET 9999/secure/two_factor_authenticated
后跟9999/secure/two_factor_authentication
。这些交换中,一个XSRF令牌保持不变。
2。)使用正确的PIN码发送到XSRF
的帖子会发送相同的POST 9999/oauth/authorize
令牌,并成功重定向到TwoFactorAuthenticationFilter.doFilterInternal()
并进入request 9999/oauth/token
然后转到9999/oauth/token
,但XSRF
拒绝该请求,因为相同的旧XSRF令牌与新的1.)
令牌值不匹配,这显然是在 FIRST PASS期间创建的。
2.)
和request 9999/oauth/authorize
之间的一个明显区别是2.)
中的第二个9999/authorize?client_id=acme&redirect_uri=/login&response_type=code&state=sGXQ4v
不包含第一个请求{{1}中包含的url参数在1.)
中,也在the official spec中定义。但目前尚不清楚这是否会导致问题。
此外,还不清楚如何访问参数以从TwoFactorAuthenticationController.POST
发送完整形成的请求。我为parameters
控制器方法的Map
做了HttpServletRequest
POST 9999/secure/two_factor_authentication
的SYSO,其中包含的pinVal
和_csrf
变量。
您可以在文件共享网站by clicking on this link上阅读所有HTTP标头和Spring Boot日志。
<小时/> 失败的方法:
我尝试了@RobWinch's approach to a similar problem in the Spring Security 3.2 environment,但这种方法似乎并不适用于Spring OAuth2的上下文。具体来说,当在下面显示的XSRF
代码中取消注释以下TwoFactorAuthenticationFilter
更新代码块时,下游请求标头会显示不同的/新XSRF
标记值,但会抛出相同的错误。
if(AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)){
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
response.setHeader("XSRF-TOKEN"/*"X-CSRF-TOKEN"*/, token.getToken());
}
这表示XSRF
配置需要以/oauth/authorize
和/oauth/token
能够相互通信以及与客户端和资源应用程序进行通信的方式进行更新成功管理XSRF
令牌值。也许CustomOAuth2RequestFactory
是需要更改才能完成此任务的。但是怎么样?
<小时/> 相关代码:
CustomOAuth2RequestFactory
的代码是:
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
@Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
return authorizationRequest;
}
}
return super.createAuthorizationRequest(authorizationParameters);
}
}
TwoFactorAuthenticationFilter
的代码是:
//This class is added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
/**
* Stores the oauth authorizationRequest in the session so that it can
* later be picked by the {@link com.example.CustomOAuth2RequestFactory}
* to continue with the authoriztion flow.
*/
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
//These next two are added as a test to avoid the compilation errors that happened when they were not defined.
public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
public static final String ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED = "ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED";
@Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
System.out.println(">>>>>>>>>>> List of authorities includes: ");
for (GrantedAuthority authority : authorities) {
System.out.println("auth: "+authority.getAuthority() );
}
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("------------------ INSIDE TwoFactorAuthenticationFilter.doFilterInternal() ------------------------");
// Check if the user hasn't done the two factor authentication.
if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
System.out.println("++++++++++++++++++++++++ AUTHENTICATED BUT NOT TWO FACTOR +++++++++++++++++++++++++");
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authenticatoin. */
System.out.println("======================== twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) is: " + twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) );
System.out.println("======================== twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities()) is: " + twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities()) );
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
// redirect the the page where the user needs to enter the two factor authentiation code
redirectStrategy.sendRedirect(request, response,
ServletUriComponentsBuilder.fromCurrentContextPath()
.path(TwoFactorAuthenticationController.PATH)
.toUriString());
return;
}
}
//THE NEXT "IF" BLOCK DOES NOT RESOLVE THE ERROR WHEN UNCOMMENTED
//if(AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)){
// CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
// this is the value of the token to be included as either a header or an HTTP parameter
// response.setHeader("XSRF-TOKEN", token.getToken());
//}
filterChain.doFilter(request, response);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
}
重新设置计算机上的问题:
您可以通过以下简单步骤在几分钟内在任何计算机上重新创建问题:
1。)下载zipped version of the app from a file sharing site by clicking on this link。
2。)键入以下内容解压缩应用:tar -zxvf oauth2.tar(2).gz
3。)通过导航到authserver
然后输入oauth2/authserver
来启动mvn spring-boot:run
应用。
4.。)导航到resource
,然后输入oauth2/resource
mvn spring-boot:run
应用
5.。)导航到ui
,然后输入oauth2/ui
mvn spring-boot:run
应用
6。)打开Web浏览器并导航到http : // localhost : 8080
7。)点击Login
,然后输入Frodo
作为用户,输入MyRing
作为密码,然后点击提交。
8。)输入5309
作为Pin Code
,然后点击提交。 这将触发上面显示的错误。
您可以通过以下方式查看完整的源代码:
a。)将maven项目导入IDE,或者
b。)在解压缩的目录中导航并使用文本编辑器打开。
<小时/> 您可以在文件共享站点by clicking on this link读取所有HTTP标头和Spring Boot日志。
答案 0 :(得分:1)
突然出现在我脑海中的一个想法:
如果激活会话固定,则在用户成功验证后创建新会话(请参阅SessionFixationProtectionStrategy)。如果您使用默认的HttpSessionCsrfTokenRepository,这当然也会创建一个新的csrf标记。由于您提到了XSRF-TOKEN标头,我假设您使用了一些JavaScript前端。我可以想象用于登录的原始csrf令牌会被存储并在之后重用 - 这不会起作用,因为此csrf令牌不再有效。
您可以尝试禁用会话固定(<session-management session-fixation-protection="none"/>
或response.writeHead(statusCode[, statusMessage][, headers])
or
window.setTimeout(func, [delay, param1, param2, ...]);
)或在登录后重新获取当前的CSRF令牌。
答案 1 :(得分:-3)
您的CustomOAuth2RequestFactory
将之前的request
放在当前request
的原位。但是,当您进行此切换时,您不会更新旧版XSRF
中的request
令牌。以下是我建议更新的CustomOAuth2Request
:
@Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
//UPDATE THE STATE VARIABLE WITH THE NEW TOKEN. THIS PART IS NEW
CsrfToken csrf = (CsrfToken) attr.getRequest().getAttribute(CsrfToken.class.getName());
String attrToken = csrf.getToken();
authorizationRequest.setState(attrToken);
return authorizationRequest;
}
}
return super.createAuthorizationRequest(authorizationParameters);
}
我正在重新审视这个问题,因为我最初的答案草案已被低估了。这个版本沿着相同的路径走,我认为这是正确的途径。