12

Ich bin auf der Suche nach Ideen, wie die Zwei-Faktor-Authentifizierung (2FA) mit Feder Sicherheit OAuth2 zu implementieren. Die Anforderung besteht darin, dass der Benutzer nur für bestimmte Anwendungen mit vertraulichen Informationen eine Zwei-Faktor-Authentifizierung benötigt. Diese Webapps haben ihre eigenen Client-IDs.Zwei-Faktor-Authentifizierung mit Feder-Sicherheit oauth2

Eine Idee, die mir in den Sinn kam, wäre, die Scope-Genehmigungsseite zu "missbrauchen", um den Benutzer zu zwingen, den 2FA-Code/PIN (oder was auch immer) einzugeben.

Probe fließt würde wie folgt aussehen:

Zugriff auf Anwendungen ohne und mit 2FA

  • Benutzer angemeldet ist
  • User-out greift App A, die nicht 2FA erfordert
  • Umleiten zu OAuth-App, Benutzer meldet sich mit Benutzername und Kennwort an
  • Weitergeleitet zu App A und Benutzer ist angemeldet
  • Benutzer greift App B, die 2FA nicht auch
  • Umleiten zu OAuth App benötigen, Umleitung zurück zur App B und Benutzer direkt angemeldet
  • Benutzer greift App S, die nicht erfordern 2FA
  • Umleiten zu OAuth App muss Benutzer zusätzlich die 2FA bereitzustellen Token
  • zu App S und Benutzer Weitergeleitet zurück angemeldet ist

Direkter Zugriff auf App mit 2FA

  • Benutzer abgemeldet
  • User-App S greift die nicht 2FA erfordern
  • Umleiten zu OAuth App, Benutzer meldet sich mit Benutzernamen und Passwort, muss Benutzer zusätzlich die 2FA bereitzustellen Token
  • Umgeleitet zurück zur App S und der Benutzer ist eingeloggt

Haben Sie andere Ideen, wie Sie das anstellen?

Antwort

17

Das ist also, wie zwei-Faktor-Authentifizierung schließlich umgesetzt wurde:

Ein Filter ist registriert für die/oauth/autorisieren Weg nach dem Frühling Sicherheitsfilter:

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

Dieser Filter prüft, ob Der Benutzer hat sich noch nicht mit einem zweiten Faktor authentifiziert (indem er überprüft hat, ob die ROLE_TWO_FACTOR_AUTHENTICATED-Berechtigung nicht verfügbar ist) und erstellt ein OAuth AuthorizationRequest, das in die Sitzung eingefügt wird.Der Benutzer wird dann auf die Seite umgeleitet, wo er den 2FA Code eingeben muss:

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

Die TwoFactorAuthenticationController, die den 2FA-Code behandelt Eingabe der Behörde fügt ROLE_TWO_FACTOR_AUTHENTICATED, wenn der Code korrekt war und leitet den Benutzer zurück in das Verzeichnis/oauth/autorisieren Endpunkt.

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

Eine benutzerdefinierte OAuth2RequestFactory ruft die zuvor gespeicherte AuthorizationRequest aus der Sitzung, wenn verfügbar und gibt das, oder erstellt eine neue, wenn keiner in der Sitzung gefunden werden kann.

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

Diese benutzerdefinierten OAuth2RequestFactory wird auf den Autorisierungsserver wie:

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

Wenn Java-config können Sie eine TwoFactorAuthenticationInterceptor anstelle des TwoFactorAuthenticationFilter erstellen und registrieren Sie es mit einem AuthorizationServerConfigurer mit

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

Die TwoFactorAuthenticationInterceptor enthält die gleiche Logik wie die TwoFactorAuthenticationFilter in seiner preHandle Methode.

+0

Vielen Dank und +1 +1 für das Posten eines detaillierten Beispiels. – CodeMed

+0

Welche Pakete sind nicht eindeutig? – James

+0

Ich habe das Impl geändert, um Java 8 Sterams zu verwenden, so dass die Query und Predicate Klassen nicht mehr benötigt werden. Sie finden das AuthenticationUtil [hier] (https://gist.github.com/active/f9b792edcf589ef43b8c644635c4ac86). – James

Verwandte Themen