【发布时间】:2016-08-22 07:34:36
【问题描述】:
Spring OAuth2 实现多因素身份验证的完整代码已上传至a file sharing site that you can download by clicking on this link。下面的说明解释了如何使用该链接在任何计算机上重新创建当前问题。 提供 500 点赏金。
当前错误:
当用户尝试在the Spring Boot OAuth2 app from the link in the preceding paragraph 中使用两因素身份验证进行身份验证时,将触发错误。当应用程序应提供第二个页面要求用户输入 PIN 码以确认用户身份时,会在此过程中引发错误。
鉴于空客户端正在触发此错误,问题似乎是如何在 Spring Boot OAuth2 中将 ClientDetailsService 连接到 Custom OAuth2RequestFactory。
entire debug log can be read at a file sharing site by clicking on this link。日志中的完整堆栈跟踪仅包含对实际在应用程序中的代码的一个引用,该代码行是:
AuthorizationRequest authorizationRequest =
oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
调试日志中抛出的错误是:
org.springframework.security.oauth2.provider.NoSuchClientException:
No client with requested id: null
抛出错误时的控制流程:
我创建了以下流程图来说明@James' suggested implementation 中多因素身份验证请求的预期流程:
在前面的流程图中,当前错误是在 用户名和密码视图 和 GET /secure/two_factor_authenticated 步骤之间的某个时间点引发的。
此 OP 的解决方案仅限于 FIRST PASS,即 1.) 通过/oauth/authorize 端点,然后 2.) 通过TwoFactorAuthenticationController 返回/oauth/authorize 端点。强>
所以我们只想解决NoSuchClientException,同时还证明客户端已成功在POST /secure/two_factor_authenticated 中授予ROLE_TWO_FACTOR_AUTHENTICATED。鉴于后续步骤是样板,只要用户输入 SECOND PASS 进入CustomOAuth2RequestFactory,流程明显中断是可以接受的strong> 以及成功完成 FIRST PASS 的所有工件。只要我们在此处成功解决了 FIRST PASS,SECOND PASS 可以是一个单独的问题。
相关代码摘录:
这是AuthorizationServerConfigurerAdapter 的代码,我尝试在其中建立连接:
@Configuration
@EnableAuthorizationServer
protected static class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired//ADDED AS A TEST TO TRY TO HOOK UP THE CUSTOM REQUEST FACTORY
private ClientDetailsService clientDetailsService;
@Autowired//Added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
private CustomOAuth2RequestFactory customOAuth2RequestFactory;
//THIS NEXT BEAN IS A TEST
@Bean CustomOAuth2RequestFactory customOAuth2RequestFactory(){
return new CustomOAuth2RequestFactory(clientDetailsService);
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource("keystore.jks"), "foobar".toCharArray()
)
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("acme")//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/builders/ClientDetailsServiceBuilder.ClientBuilder.html
.secret("acmesecret")
.authorizedGrantTypes("authorization_code", "refresh_token", "password")
.scopes("openid");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerEndpointsConfigurer.html
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.requestFactory(customOAuth2RequestFactory);//Added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.html
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}
这是TwoFactorAuthenticationFilter 的代码,其中包含触发错误的上述代码:
package demo;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
//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) {
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 {
// Check if the user hasn't done the two factor authentication.
if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authenticatoin. */
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;
}
}
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(1).gz
3.) 导航至oauth2/authserver,然后输入mvn spring-boot:run,启动authserver 应用程序。
4.) 通过导航到 oauth2/resource 然后输入 mvn spring-boot:run 来启动 resource 应用程序
5.) 通过导航到 oauth2/ui 然后输入 mvn spring-boot:run 来启动 ui 应用程序
6.) 打开网络浏览器并导航到http : // localhost : 8080
7.) 点击Login,然后输入Frodo作为用户,MyRing作为密码,然后点击提交。 这将触发上面显示的错误。
您可以通过以下方式查看完整的源代码:
a.) 将 maven 项目导入您的 IDE,或通过
b.) 在解压后的目录中导航并使用文本编辑器打开。
注意:上面文件共享链接中的代码是the Spring Boot OAuth2 GitHub sample at this link和suggestions for 2 Factor Authentication offered by @James at this link的组合。 Spring Boot GitHub 示例的唯一更改是在 authserver 应用程序中,特别是在 authserver/src/main/java 和 authserver/src/main/resources/templates 中。
缩小问题范围:
根据@AbrahamGrief 的建议,我添加了
FilterConfigurationBean,解决了NoSuchClientException。但是 OP 询问如何通过图表中的控制流程完成 FIRST PASS 以获得 500 点赏金。
然后我通过在Users.loadUserByUername() 中设置ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED 来缩小问题范围,如下所示:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password;
List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
if (username.equals("Samwise")) {//ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED will need to come from the resource, NOT the user
auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED");
password = "TheShire";
}
else if (username.equals("Frodo")){//ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED will need to come from the resource, NOT the user
auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED");
password = "MyRing";
}
else{throw new UsernameNotFoundException("Username was not found. ");}
return new org.springframework.security.core.userdetails.User(username, password, auth);
}
这消除了配置客户端和资源的需要,因此当前的问题仍然很窄。然而,下一个障碍是 Spring Security 拒绝用户对/security/two_factor_authentication 的请求。 还需要进行哪些更改才能通过控制流完成 FIRST PASS,以便 POST /secure/two_factor_authentication 可以 SYSO ROLE_TWO_FACTOR_AUTHENTICATED?
【问题讨论】:
标签: spring spring-mvc spring-security spring-boot spring-security-oauth2