12

Sto cercando idee su come implementare l'autenticazione a due fattori (2FA) con la sicurezza Spring OAuth2. Il requisito è che l'utente abbia bisogno dell'autenticazione a due fattori solo per applicazioni specifiche con informazioni sensibili. Quelle webapps hanno i propri ID client.Autenticazione a due fattori con sicurezza a molla oauth2

Un'idea che mi è venuta in mente sarebbe quella di "usare male" la pagina di approvazione dell'oscilloscopio per costringere l'utente a inserire il codice/PIN 2FA (o qualsiasi altra cosa).

flussi campione sarebbe simile a questa:

Accesso applicazioni senza e con 2FA

  • utente è disconnesso
  • utente accede App A che non richiede 2FA
  • reindirizzamento a App OAuth, l'utente effettua l'accesso con nome utente e password
  • Reindirizzato all'app A e l'utente è connesso
  • utente accede a un'applicazione B che inoltre non richiede 2FA
  • reindirizzamento a OAuth app, reindirizzare torna a app B e l'utente è direttamente connesso a
  • utente accede app S che fa richiede 2FA
  • reindirizzamento a OAuth applicazione, l'utente deve fornire inoltre il 2FA gettone
  • reindirizzato app S e l'utente viene registrato in

accesso diretto app con 2FA

  • utente è disconnesso
  • utente accede a un'applicazione S che fa richiede 2FA
  • reindirizzamento a OAuth app, utente accede con nome utente e password, l'utente deve fornire inoltre il 2FA Token
  • Reindirizzato all'app S e l'utente è connesso

Avete altre idee su come creare un app?

risposta

17

Quindi questo è il modo autenticazione a due fattori è stato implementato finalmente:

Un filtro è registrato per l'/ OAuth/autorizzazione percorso dopo il filtro di sicurezza a molla:

@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); 
    } 
} 

Questo filtro verifica se l'utente non ha ancora autenticato con un secondo fattore (controllando se l'autorizzazione ROLE_TWO_FACTOR_AUTHENTICATED non è disponibile) e crea un OAuth AuthorizationRequest che viene inserito nella sessione.L'utente viene reindirizzato alla pagina in cui si deve inserire il codice 2FA:

/** 
* 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; 
    } 
} 

Il TwoFactorAuthenticationController che gestisce entrare nel 2FA-codice aggiunge l'autorità ROLE_TWO_FACTOR_AUTHENTICATED se il codice è stato corretto e reindirizza l'utente alla/oauth/autorizza 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 
    } 
} 

Una consuetudine OAuth2RequestFactory recupera il precedentemente salvato AuthorizationRequest dalla sessione se disponibile, e restituisce quello o ne crea una nuova, se nessuno può essere trovata nella sessione.

/** 
* 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); 
    } 
} 

Questa OAuth2RequestFactory personalizzato è impostato al server di autorizzazione come:

<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> 

Quando si utilizza Java config è possibile creare un TwoFactorAuthenticationInterceptor al posto del TwoFactorAuthenticationFilter e registrarlo con una AuthorizationServerConfigurer con

@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(); 
    } 
} 

TwoFactorAuthenticationInterceptor contiene la stessa logica del TwoFactorAuthenticationFilter nel suo metodo preHandle.

+0

Grazie e +1 +1 per pubblicare un esempio dettagliato. – CodeMed

+0

Quali pacchetti sono ambigui? – James

+0

Ho cambiato l'impl per utilizzare gli steram java 8, quindi le classi Query e Predicate non sono più necessarie. Puoi trovare AuthenticationUtil [qui] (https://gist.github.com/ractive/f9b792edcf589ef43b8c644635c4ac86). – James