22

春のセキュリティ OAuth2 で 2 要素認証 (2FA) を実装する方法のアイデアを探しています。要件は、ユーザーが機密情報を含む特定のアプリケーションに対してのみ 2 要素認証を必要とすることです。これらの Web アプリケーションには、独自のクライアント ID があります。

私の頭に浮かんだアイデアの 1 つは、スコープ承認ページを「誤用」して、ユーザーに 2FA コード/PIN (または何でも) の入力を強制することです。

サンプル フローは次のようになります。

2FA を使用しない場合と使用する場合のアプリへのアクセス

  • ユーザーがログアウトしました
  • ユーザーが 2FA を必要としないアプリ A にアクセスする
  • OAuth アプリにリダイレクトし、ユーザーはユーザー名とパスワードでログインします
  • アプリ A にリダイレクトされ、ユーザーがログインしている
  • ユーザーは、同じく 2FA を必要としないアプリ B にアクセスします
  • OAuth アプリにリダイレクトし、アプリ B にリダイレクトして、ユーザーが直接ログインする
  • ユーザーが 2FA を必要とするアプリ S にアクセスする
  • OAuth アプリにリダイレクトします。ユーザーはさらに 2FA トークンを提供する必要があります
  • アプリ S にリダイレクトされ、ユーザーがログインしている

2FA でアプリに直接アクセスする

  • ユーザーがログアウトしました
  • ユーザーが 2FA を必要とするアプリ S にアクセスする
  • OAuth アプリにリダイレクトし、ユーザーはユーザー名とパスワードでログインし、ユーザーはさらに 2FA トークンを提供する必要があります
  • アプリ S にリダイレクトされ、ユーザーがログインしている

これにアプローチする他のアイデアはありますか?

4

2 に答える 2

26

これが最終的に 2 要素認証が実装された方法です。

spring セキュリティ フィルターの後に /oauth/authorize パスにフィルターが登録されます。

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

このフィルターは、ユーザーがまだ第 2 要素で認証されていないかどうかを確認し (機関が利用できないかどうかを確認することによって) 、セッションに入れROLE_TWO_FACTOR_AUTHENTICATEDられる OAuth を作成します。AuthorizationRequestユーザーは、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;
    }
}

TwoFactorAuthenticationController2FA コードの入力を処理する は、コードが正しかった場合に権限を追加し、ROLE_TWO_FACTOR_AUTHENTICATEDユーザーを /oauth/authorize エンドポイントにリダイレクトします。

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

カスタムは、セッションからOAuth2RequestFactory以前に保存AuthorizationRequestされたものがあればそれを取得し、それを返すか、セッションに何も見つからない場合は新しいものを作成します。

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

このカスタム OAuth2RequestFactory は、次のように認可サーバーに設定されます。

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

Java config を使用する場合、 のTwoFactorAuthenticationInterceptor代わりに を作成し、 withTwoFactorAuthenticationFilterに登録できます。AuthorizationServerConfigurer

@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は と同じロジックが含まれています。TwoFactorAuthenticationFilterpreHandle

于 2015-07-15T09:04:51.323 に答える