Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Two factor authentication with spring security oauth2

Tags:

I'm looking for ideas how to implement two factor authentication (2FA) with spring security OAuth2. The requirement is that the user needs two factor authentication only for specific applications with sensitive information. Those webapps have their own client ids.

One idea that popped in my mind would be to "mis-use" the scope approval page to force the user to enter the 2FA code/PIN (or whatever).

Sample flows would look like this:

Accessing apps without and with 2FA

  • User is logged out
  • User accesses app A which does not require 2FA
  • Redirect to OAuth app, user logs in with username and password
  • Redirected back to app A and user is logged in
  • User accesses app B which also does not require 2FA
  • Redirect to OAuth app, redirect back to app B and user is directly logged in
  • User accesses app S which does require 2FA
  • Redirect to OAuth app, user needs to additionally provide the 2FA token
  • Redirected back to app S and user is logged in

Directly accessing app with 2FA

  • User is logged out
  • User accesses app S which does require 2FA
  • Redirect to OAuth app, user logs in with username and password, user needs to additionally provide the 2FA token
  • Redirected back to app S and user is logged in

Do you have other ideas how to apporach this?

like image 322
James Avatar asked May 19 '15 07:05

James


People also ask

Is OAuth2 the same as 2 factor authentication?

OAuth2 is for "Server Site Authorization" of certain parameter(s) access (designated by Server site) given to a requesting entity (or App). Whereas 2FA is about Authenticating an Account Owner entity logging into an Account on the Server Site (with full owner access).

What is two factor authentication spring boot?

JWT and Social Authentication using Spring Boot Two-factor authentication is a way to increase the security of your product by adding an extra layer of security by asking the user to provide a second form of identification alongside username and password.

Is JWT two factor authentication?

The user will first authenticate using a username and password. If the account has 2FA enabled, we'll receive a JWT with a property marked as not authenticated. If we try to access a protected endpoint while not authenticated, we will get an error.


2 Answers

So this is how two factor authentication has been implemented finally:

A filter is registered for the /oauth/authorize path after the spring security filter:

@Order(200) public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {     @Override     protected void afterSpringSecurityFilterChain(ServletContext servletContext) {         FilterRegistration.Dynamic twoFactorAuthenticationFilter = servletContext.addFilter("twoFactorAuthenticationFilter", new DelegatingFilterProxy(AppConfig.TWO_FACTOR_AUTHENTICATION_BEAN));         twoFactorAuthenticationFilter.addMappingForUrlPatterns(null, false, "/oauth/authorize");         super.afterSpringSecurityFilterChain(servletContext);     } } 

This filter checks if the user hasn't already authenticated with a 2nd factor (by checking if the ROLE_TWO_FACTOR_AUTHENTICATED authority isn't available) and creates an OAuth AuthorizationRequest which is put into the session. The user is then redirected to the page where he has to enter the 2FA code:

/**  * 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;      @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;             } else {                 request.getSession().removeAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);             }         }          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;     } } 

The TwoFactorAuthenticationController that handles entering the 2FA-code adds the authority ROLE_TWO_FACTOR_AUTHENTICATED if the code was correct and redirects the user back to the /oauth/authorize endpoint.

@Controller @RequestMapping(TwoFactorAuthenticationController.PATH) public class TwoFactorAuthenticationController {     private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);      public static final String PATH = "/secure/two_factor_authentication";      @RequestMapping(method = RequestMethod.GET)     public String auth(HttpServletRequest request, HttpSession session, ....) {         if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {             LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);             throw ....;         }         else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {             LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);             throw ....;         }          return ....; // Show the form to enter the 2FA secret     }      @RequestMapping(method = RequestMethod.POST)     public String auth(....) {         if (userEnteredCorrect2FASecret()) {             AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);             return "forward:/oauth/authorize"; // Continue with the OAuth flow         }          return ....; // Show the form to enter the 2FA secret again     } } 

A custom OAuth2RequestFactory retrieves the previously saved AuthorizationRequest from the session if available and returns that or creates a new one if none can be found in the session.

/**  * If the session contains an {@link AuthorizationRequest}, this one is used and returned.  * The {@link com.example.TwoFactorAuthenticationFilter} saved the original AuthorizationRequest. This allows  * to redirect the user away from the /oauth/authorize endpoint during oauth authorization  * and show him e.g. a the page where he has to enter a code for two factor authentication.  * Redirecting him back to /oauth/authorize will use the original authorizationRequest from the session  * and continue with the oauth authorization.  */ 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);     } } 

This custom OAuth2RequestFactory is set to the authorization server like:

<bean id="customOAuth2RequestFactory" class="com.example.CustomOAuth2RequestFactory">     <constructor-arg index="0" ref="clientDetailsService" /> </bean>  <!-- Configures the authorization-server and provides the /oauth/authorize endpoint --> <oauth:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"     user-approval-handler-ref="approvalStoreUserApprovalHandler" redirect-resolver-ref="redirectResolver"     authorization-request-manager-ref="customOAuth2RequestFactory">     <oauth:authorization-code authorization-code-services-ref="authorizationCodeServices"/>     <oauth:implicit />     <oauth:refresh-token />     <oauth:client-credentials />     <oauth:password /> </oauth:authorization-server> 

When using java config you can create a TwoFactorAuthenticationInterceptor instead of the TwoFactorAuthenticationFilter and register it with an AuthorizationServerConfigurer with

@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig implements AuthorizationServerConfigurer {     ...      @Override     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {         endpoints             .addInterceptor(twoFactorAuthenticationInterceptor())             ...             .requestFactory(customOAuth2RequestFactory());     }      @Bean     public HandlerInterceptor twoFactorAuthenticationInterceptor() {         return new TwoFactorAuthenticationInterceptor();     } } 

The TwoFactorAuthenticationInterceptor contains the same logic as the TwoFactorAuthenticationFilter in its preHandle method.

like image 134
James Avatar answered Oct 06 '22 06:10

James


I couldn't make the accepted solution work. I have been working on this for a while, and finally I wrote my solution by using the ideas explained here and on this thread "null client in OAuth2 Multi-Factor Authentication"

Here is the GitHub location for the working solution for me: https://github.com/turgos/oauth2-2FA

I appreciate if you share your feedback in case you see any issues or better approach.

Below you can find the key configuration files for this solution.

AuthorizationServerConfig

@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {      @Autowired     private AuthenticationManager authenticationManager;      @Autowired     private ClientDetailsService clientDetailsService;      @Override     public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {          security.tokenKeyAccess("permitAll()")                 .checkTokenAccess("isAuthenticated()");     }       @Override     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {         clients                 .inMemory()                 .withClient("ClientId")                 .secret("secret")                 .authorizedGrantTypes("authorization_code")                 .scopes("user_info")                 .authorities(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED)                 .autoApprove(true);     }       @Override     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {          endpoints             .authenticationManager(authenticationManager)             .requestFactory(customOAuth2RequestFactory());     }       @Bean     public DefaultOAuth2RequestFactory customOAuth2RequestFactory(){         return new CustomOAuth2RequestFactory(clientDetailsService);     }      @Bean     public FilterRegistrationBean twoFactorAuthenticationFilterRegistration(){         FilterRegistrationBean registration = new FilterRegistrationBean();         registration.setFilter(twoFactorAuthenticationFilter());         registration.addUrlPatterns("/oauth/authorize");         registration.setName("twoFactorAuthenticationFilter");         return registration;     }      @Bean     public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter(){         return new TwoFactorAuthenticationFilter();     } } 

CustomOAuth2RequestFactory

public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {      private static final Logger LOG = LoggerFactory.getLogger(CustomOAuth2RequestFactory.class);      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);                   LOG.debug("createAuthorizationRequest(): return saved copy.");                  return authorizationRequest;             }         }          LOG.debug("createAuthorizationRequest(): create");         return super.createAuthorizationRequest(authorizationParameters);     }   } 

WebSecurityConfig

@EnableResourceServer @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends WebSecurityConfigurerAdapter {      @Autowired     CustomDetailsService customDetailsService;       @Bean     public PasswordEncoder encoder() {         return new BCryptPasswordEncoder();     }       @Bean(name = "authenticationManager")     @Override     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     }      @Override       public void configure(WebSecurity web) throws Exception {         web.ignoring().antMatchers("/webjars/**");         web.ignoring().antMatchers("/css/**","/fonts/**","/libs/**");       }        @Override       protected void configure(HttpSecurity http) throws Exception { // @formatter:off           http.requestMatchers()               .antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication","/exit", "/resources/**")               .and()               .authorizeRequests()               .anyRequest()               .authenticated()               .and()               .formLogin().loginPage("/login")               .permitAll();       } // @formatter:on        @Override     @Autowired // <-- This is crucial otherwise Spring Boot creates its own     protected void configure(AuthenticationManagerBuilder auth) throws Exception {  //        auth//.parentAuthenticationManager(authenticationManager) //                .inMemoryAuthentication() //                .withUser("demo") //                .password("demo") //                .roles("USER");          auth.userDetailsService(customDetailsService).passwordEncoder(encoder());     } } 

TwoFactorAuthenticationFilter

public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {      private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationFilter.class);      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())         );     }        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;     }       @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 (isAuthenticated() && !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 authentication. */             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);                  LOG.debug("doFilterInternal(): redirecting to {}", TwoFactorAuthenticationController.PATH);                  // redirect the the page where the user needs to enter the two factor authentication code                 redirectStrategy.sendRedirect(request, response,                         TwoFactorAuthenticationController.PATH                            );                 return;             }          }          LOG.debug("doFilterInternal(): without redirect.");          filterChain.doFilter(request, response);     }      public boolean isAuthenticated(){         return SecurityContextHolder.getContext().getAuthentication().isAuthenticated();     }      private boolean hasAuthority(String checkedAuthority){           return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(                 authority -> checkedAuthority.equals(authority.getAuthority())                 );     }  } 

TwoFactorAuthenticationController

@Controller @RequestMapping(TwoFactorAuthenticationController.PATH)  public class TwoFactorAuthenticationController {     private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);      public static final String PATH = "/secure/two_factor_authentication";      @RequestMapping(method = RequestMethod.GET)     public String auth(HttpServletRequest request, HttpSession session) {         if (isAuthenticatedWithAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED)) {             LOG.debug("User {} already has {} authority - no need to enter code again", TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);              //throw ....;         }         else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {             LOG.debug("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);             //throw ....;         }          LOG.debug("auth() HTML.Get");           return "loginSecret"; // Show the form to enter the 2FA secret     }      @RequestMapping(method = RequestMethod.POST)     public String auth(@ModelAttribute(value="secret") String secret, BindingResult result, Model model) {         LOG.debug("auth() HTML.Post");          if (userEnteredCorrect2FASecret(secret)) {             addAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);             return "forward:/oauth/authorize"; // Continue with the OAuth flow         }          model.addAttribute("isIncorrectSecret", true);         return "loginSecret"; // Show the form to enter the 2FA secret again     }      private boolean isAuthenticatedWithAuthority(String checkedAuthority){          return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(                 authority -> checkedAuthority.equals(authority.getAuthority())                 );     }      private boolean addAuthority(String authority){          Collection<SimpleGrantedAuthority> oldAuthorities = (Collection<SimpleGrantedAuthority>)SecurityContextHolder.getContext().getAuthentication().getAuthorities();         SimpleGrantedAuthority newAuthority = new SimpleGrantedAuthority(authority);         List<SimpleGrantedAuthority> updatedAuthorities = new ArrayList<SimpleGrantedAuthority>();         updatedAuthorities.add(newAuthority);         updatedAuthorities.addAll(oldAuthorities);          SecurityContextHolder.getContext().setAuthentication(                 new UsernamePasswordAuthenticationToken(                         SecurityContextHolder.getContext().getAuthentication().getPrincipal(),                         SecurityContextHolder.getContext().getAuthentication().getCredentials(),                         updatedAuthorities)         );          return true;     }      private boolean userEnteredCorrect2FASecret(String secret){         /* later on, we need to pass a temporary secret for each user and control it here */         /* this is just a temporary way to check things are working */          if(secret.equals("123"))             return true;         else;             return false;     } } 
like image 45
turgos Avatar answered Oct 06 '22 06:10

turgos