web-dev-qa-db-ja.com

Spring Security OAuth2とSpring Socialを統合する

私は、Dave Syerの例に触発されたと信じているSpring Boot + Spring Security OAuth2アプリケーションを使用しています。アプリケーションはOAuth2承認サーバーとして構成され、単一のパブリッククライアントがリソース所有者のパスワード資格情報フローを使用します。成功したトークンはJWTとして構成されます。

パブリックAngularクライアントは、クライアントIDとシークレットを含む基本認証ヘッダーを使用して/ oauth/tokenにPOSTリクエストを送信します(これは、取得する最も簡単な方法でした。シークレットはプライベートではありませんが、認証するクライアントです。リクエストの本文には、ユーザー名、パスワード、付与タイプの「パスワード」が含まれています。

アプリケーションは認証サーバーであることに加えて、ユーザー、チーム、および組織のためのRESTfulリソースサーバーです。

Spring Socialを使用して、追加のSSO認証フローを追加しようとしています。 Spring Socialは、/ auth/[provider]を介して外部プロバイダーを介して認証するように構成されています。ただし、次のリクエストではSecurityContextが正しく設定されなくなりました。おそらく、Spring Security OAuthサーバーまたはクライアントがSecurityContextをオーバーライドしていますか?

Spring Socialフローの後でSecurityContextを正しく設定できる場合、事前に認証されたユーザーのSecurityContextHolderをチェックする「ソーシャル」の新しい付与タイプを許可する新しいTokenGranterを入手しました。

SecurityContextの特定の問題の解決策(Spring OAuth + Social Integration)の問題だと思います)、または外部プロバイダーでの認証と取得のための別のアプローチの両方に興味があります独自の認証サーバーからの有効なJWT。

ありがとう!

32
Andrew Ferk

JHipster -で生成されたWebアプリケーションでも同様の問題がありました。最後に、Spring SocialのSocialAuthenticationFilterオプションを(SpringSocialConfigurerを介して)使用することにしました。ソーシャルログインが成功すると、サーバーは自動的に「独自の」アクセストークンを生成して、クライアントアプリへのリダイレクトを介して返します。

これが私の試みです:

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter implements EnvironmentAware {

    //...

    @Inject
    private AuthorizationServerTokenServices authTokenServices;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        SpringSocialConfigurer socialCfg = new SpringSocialConfigurer();
        socialCfg
            .addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
                @SuppressWarnings("unchecked")
                public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
                    filter.setAuthenticationSuccessHandler(
                            new SocialAuthenticationSuccessHandler(
                                    authTokenServices,
                                    YOUR_APP_CLIENT_ID
                            )
                        );
                    return filter;
                }
            });

        http
            //... lots of other configuration ...
            .apply(socialCfg);
    }        
}

そしてSocialAuthenticationSuccessHandlerクラス:

public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    public static final String REDIRECT_PATH_BASE = "/#/login";
    public static final String FIELD_TOKEN = "access_token";
    public static final String FIELD_EXPIRATION_SECS = "expires_in";

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final AuthorizationServerTokenServices authTokenServices;
    private final String localClientId;

    public SocialAuthenticationSuccessHandler(AuthorizationServerTokenServices authTokenServices, String localClientId){
        this.authTokenServices = authTokenServices;
        this.localClientId = localClientId;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
                    throws IOException, ServletException {
        log.debug("Social user authenticated: " + authentication.getPrincipal() + ", generating and sending local auth");
        OAuth2AccessToken oauth2Token = authTokenServices.createAccessToken(convertAuthentication(authentication)); //Automatically checks validity
        String redirectUrl = new StringBuilder(REDIRECT_PATH_BASE)
            .append("?").append(FIELD_TOKEN).append("=")
            .append(encode(oauth2Token.getValue()))
            .append("&").append(FIELD_EXPIRATION_SECS).append("=")
            .append(oauth2Token.getExpiresIn())
            .toString();
        log.debug("Sending redirection to " + redirectUrl);
        response.sendRedirect(redirectUrl);
    }

    private OAuth2Authentication convertAuthentication(Authentication authentication) {
        OAuth2Request request = new OAuth2Request(null, localClientId, null, true, null,
                null, null, null, null);
        return new OAuth2Authentication(request,
                //Other option: new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), "N/A", authorities)
                new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A")
                );
    }

    private String encode(String in){
        String res = in;
        try {
            res = UriUtils.encode(in, GeneralConstants.ENCODING_UTF8);
        } catch(UnsupportedEncodingException e){
            log.error("ERROR: unsupported encoding: " + GeneralConstants.ENCODING_UTF8, e);
        }
        return res;
    }
}

このように、対応する/#/login?access_token=my_access_token&expires_in=seconds_to_expirationSocialAuthenticationSuccessHandlerに設定している限り、クライアントアプリはREDIRECT_PATH_BASEへのリダイレクトを介してWebアプリのアクセストークンを受け取ります。

お役に立てば幸いです。

12
rbarriuso

まず、このようなユースケースでは、password grantから離れることを強くお勧めします。
公開クライアント(JavaScript、インストールされたアプリケーション)はクライアントの秘密を秘密に保つことができないため、クライアントに秘密を割り当ててはなりません。その過程でユーザーのパスワードを保存します。

暗黙の付与は、あなたがやっていることに対して正確に作成されました。
リダイレクトベースのフローを使用することには、アプリケーションごとにその一部を持たせる代わりに、認証メカニズムを承認サーバーに任せるという利点があります。これは主にシングルサインオン(SSO)

そうは言っても、あなたの質問は私が答えたばかりのこれと密接に関連しています: 独自のSpring OAuth2サーバーとサードパーティのOAuthプロバイダー

答えを要約すると:

最終的には、認証サーバーがAuthorizationEndpoint:/ oauth/authorizeを保護する方法についてです。承認サーバーが機能しているため、formLoginを使用して/ oauth/authorizeのセキュリティを処理するWebSecurityConfigurerAdapterを拡張する構成クラスが既にあります。それはあなたが社会的なものを統合する必要があるところです。

達成しようとしていることに対してパスワード付与を使用することはできません。パブリッククライアントを許可サーバーにリダイレクトする必要があります。承認サーバーは、/oauth/authorizeエンドポイントのセキュリティメカニズムとしてソーシャルログインにリダイレクトします。

5

私は上記の良い答え( https://stackoverflow.com/a/33963286/3351474 )から始めていましたが、私のバージョンのSpring Security(4.2.8.RELEASE)ではこれは失敗します。その理由は、org.springframework.security.access.intercept.AbstractSecurityInterceptor#authenticateIfRequiredでは回答のPreAuthenticatedAuthenticationTokenが認証されないためです。一部のGrantedAuthoritiesは合格する必要があります。さらに、URLパラメータでトークンを共有することは適切ではありません。HTTPsペイロードまたはヘッダーで常に非表示にする必要があります。代わりに、HTMLテンプレートがロードされ、トークン値が${token}プレースホルダーフィールドに挿入されます。

ここで改訂版:

注:ここで使用されているUserDetailsorg.springframework.security.core.userdetails.UserDetailsを実装しています

@Component
public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private OAuth2TokenStore tokenStore;

    @Qualifier("tokenServices")
    @Autowired
    private AuthorizationServerTokenServices authTokenServices;

    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        IClient user = ((SocialUserDetails) authentication.getPrincipal()).getUser();
        // registration is not finished, forward the user, a marker interface 
        // IRegistration is used here, remove this if there no two step approach to 
        // create a user from a social network
        if (user instanceof IRegistration) {
            response.sendRedirect(subscriberRegistrationUrl + "/" + user.getId());
        }
        OAuth2AccessToken token = loginUser(user);
        // load a HTML template from the class path and replace the token placeholder within, the HTML should contain a redirect to the actual page, but must store the token in a safe place, e.g. for preventing CSRF in the `sessionStorage` JavaScript storage.
        String html = IOUtils.toString(getClass().getResourceAsStream("/html/socialLoginRedirect.html"));
        html = html.replace("${token}", token.getValue());
        response.getOutputStream().write(html.getBytes(StandardCharsets.UTF_8));
    }

    private OAuth2Authentication convertAuthentication(Authentication authentication) {
        OAuth2Request request = new OAuth2Request(null, authentication.getName(),
                authentication.getAuthorities(), true, null,
                null, null, null, null);
        // note here the passing of the authentication.getAuthorities()
        return new OAuth2Authentication(request,
                new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A",  authentication.getAuthorities())
        );
    }

    /**
     * Logs in a user.
     */
    public OAuth2AccessToken loginUser(IClient user) {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        UserDetails userDetails = new UserDetails(user);
        Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A", userDetails.getAuthorities());
        securityContext.setAuthentication(authentication);
        OAuth2Authentication oAuth2Authentication = convertAuthentication(authentication);
        // delete the token because the client id in the DB is calculated as hash of the username and client id (here also also identical to username), this would be identical to the
        // to an existing user. This existing one can come from a user registration or a previous user with the same name.
        // If a new entity with a different ID is used the stored token hash would differ and the the wrong token would be retrieved 
        tokenStore.deleteTokensForUserId(user.getUsername());
        OAuth2AccessToken oAuth2AccessToken = authTokenServices.createAccessToken(oAuth2Authentication);
        // the DB id of the created user is returned as additional data, can be 
        // removed if not needed
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(new HashMap<>());
        oAuth2AccessToken.getAdditionalInformation().put("userId", user.getId());
        return oAuth2AccessToken;
    }

}

socialLoginRedirect.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Example App</title>
    <meta http-equiv="Refresh" content="0; url=/index.html#/home"/>
</head>
<script>
     window.sessionStorage.setItem('access_token', '${token}');
</script>
<body>
<p>Please follow <a href="/index.html#/home">this link</a>.</p>
</body>
</html>

WebSecurityConfigurerAdapterの構成配線:

@Configuration
@EnableWebSecurity
@EnableWebMvc
@Import(WebServiceConfig.class)
public class AuthenticationConfig extends WebSecurityConfigurerAdapter {

    @Value("${registrationUrl}")
    private String registrationUrl;

    @Autowired
    private SocialAuthenticationSuccessHandler socialAuthenticationSuccessHandler;

    @Value("${loginUrl}")
    private String loginUrl;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<String> permitAllUrls = new ArrayList<>();
        // permit social log in
        permitAllUrls.add("/auth/**");
        http.authorizeRequests().antMatchers(permitAllUrls.toArray(new String[0])).permitAll();

        SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
        springSocialConfigurer.signupUrl(registrationUrl);
        springSocialConfigurer.postFailureUrl(loginUrl);
        springSocialConfigurer
                .addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
                    @SuppressWarnings("unchecked")
                    public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
                        filter.setAuthenticationSuccessHandler(socialAuthenticationSuccessHandler);
                        return filter;
                    }
                });
        http.apply(springSocialConfigurer);

        http.logout().disable().csrf().disable();

        http.requiresChannel().anyRequest().requiresSecure();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
1
k_o_

残りのサービスを保護するためにSpring OAuth2を実装し、さらに初回ログイン時にソーシャルログインと暗黙のサインアップを追加しました。ユーザーuserの場合、ユーザー名とパスワードのみを使用してトークンを生成できます。ソーシャルユーザーのトークンを生成すると問題が発生します。そのためには、処理する前に/ oauth/tokenリクエストをインターセプトするフィルターを実装する必要があります。ここで、ソーシャルユーザーのトークンを生成する場合は、ユーザー名とFacebookトークンを渡します。ここでは、Facebookトークンをパスワードとして使用し、Facebookユーザーのトークンも生成できます。 facebookトークンが更新された場合、ユーザーテーブルのトークンを更新するためにdbトリガーも作成する必要があります....

0
Amit Patel