LoginSignup
11
13

More than 5 years have passed since last update.

OAuth2 Autoconfig(Spring Security OAuth2)をプロキシ環境で使うのが意外と面倒だった件

Last updated at Posted at 2018-04-15

前回、Spring Boot 2とOAuth2 Autoconfigで超シンプルにOAuth2クライアントを実現する方法を解説しましたが、作成したクライアントアプリをプロキシ環境で利用する場合にはOAuth2RestTemplateがプロキシを経由するようセットアップする必要があります。

しかし、プロキシを適用する方法が意外と面倒で、まとまっているサイトもなかったので、参考程度にまとめておきます。

今回使用するライブラリ

  • Spring Boot 2 (Spring & Spring Security 5)
    • spring-boot-starter-web 2.0.1.RELEASE
    • spring-boot-starter-security 2.0.1.RELEASE
  • OAuth2 Autoconfig (Spring Security OAuth2)
    • spring-security-oauth2-autoconfigure 2.0.0.RELEASE

※前回記事と同じです。

OAuth2RestTemplateにプロキシを適用する

OAuth2RestTemplateRestTemplateを拡張しており、プロキシを適用する方法も同様です。

  • 認証なしプロキシを利用する場合
    @Bean
    public ClientHttpRequestFactory requestFactory(
            @Value("${proxy.host}") String host,
            @Value("${proxy.port}") int port) {

        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setProxy(new Proxy(Type.HTTP, new InetSocketAddress(host, port)));
        return factory;
    }

    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
            OAuth2ProtectedResourceDetails details, ClientHttpRequestFactory requestFactory) {

        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details, oauth2ClientContext);
        restTemplate.setRequestFactory(requestFactory);
        return restTemplate;
    }
  • 認証プロキシを利用する場合
    @Bean
    public ClientHttpRequestFactory requestFactory(
            @Value("${proxy.host}") String host,
            @Value("${proxy.port}") int port,
            @Value("${proxy.user}") String user,
            @Value("${proxy.password}") String password) {

        HttpClientBuilder builder = HttpClientBuilder.create();
        builder.setProxy(new HttpHost(host, port));

        if (StringUtils.hasText(user) && StringUtils.hasText(password)) {
            BasicCredentialsProvider provider = new BasicCredentialsProvider();
            provider.setCredentials(new AuthScope(host, port), new UsernamePasswordCredentials(user, password));
            builder.setDefaultCredentialsProvider(provider);
        }

        return new HttpComponentsClientHttpRequestFactory(builder.build());
    }

    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
            OAuth2ProtectedResourceDetails details, ClientHttpRequestFactory requestFactory) {

        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details, oauth2ClientContext);
        restTemplate.setRequestFactory(requestFactory);
        return restTemplate;
    }

今回は、認証プロキシを利用するパターンで検証してみました。

プロキシが適切に適用されない

作成したクライアントアプリに上記の方法で認証プロキシを適用し、SSOログインしてみると、、、

  • コンテキストルート(/)にアクセス
  • ログインURL(/login)にリダイレクト
  • Githubの認証URLにリダイレクト
  • 認可コードを付与してログインURL(/login)にリダイレクト
  • Githubの認証URLにリダイレクト
  • 認可コードを付与してログインURL(/login)にリダイレクト
  • ...

と、延々認証をループしていまいました。
Spring Security OAuth2のロガーをINFOレベルにしてログを確認すると、407 Proxy Authentication Requiredと出力されており、認証プロキシが適切に適用されていないことが分かります。

これは、@EnableOAuth2SsoでセットアップされるOAuth2ClientAuthenticationProcessingFilterでは、Bean定義したOAuth2RestTemplateとは別のOAuth2RestTemplateが使用されるためです。

@EnableOAuth2SsoでセットアップされるOAuth2RestTemplateにプロキシを適用する(解析編)

ここで、@EnableOAuth2Ssoによりセットアップされるコンポーネントを図示します。

alt

これにより、@EnableOAuth2Ssoにより複数のOAuth2RestTemplateが生成されますが、そのすべてがUserInfoRestTemplateFactoryによって生成(または生成されたOAuth2RestTemplateを通じて生成)されていることが分かります。

次に、UserInfoRestTemplateFactoryインターフェイスのデフォルト実装クラス(DefaultUserInfoRestTemplateFactory)を確認します。

public class DefaultUserInfoRestTemplateFactory implements UserInfoRestTemplateFactory {

    // omitted.

    public DefaultUserInfoRestTemplateFactory(
            ObjectProvider<List<UserInfoRestTemplateCustomizer>> customizers,
            ObjectProvider<OAuth2ProtectedResourceDetails> details,
            ObjectProvider<OAuth2ClientContext> oauth2ClientContext) {
        this.customizers = customizers.getIfAvailable();
        this.details = details.getIfAvailable();
        this.oauth2ClientContext = oauth2ClientContext.getIfAvailable();
    }

    // omitted.

    @Override
    public OAuth2RestTemplate getUserInfoRestTemplate() {
        if (this.oauth2RestTemplate == null) {
            this.oauth2RestTemplate = createOAuth2RestTemplate(
                    this.details == null ? DEFAULT_RESOURCE_DETAILS : this.details);
            this.oauth2RestTemplate.getInterceptors()
                    .add(new AcceptJsonRequestInterceptor());
            AuthorizationCodeAccessTokenProvider accessTokenProvider = new AuthorizationCodeAccessTokenProvider();
            accessTokenProvider.setTokenRequestEnhancer(new AcceptJsonRequestEnhancer());
            this.oauth2RestTemplate.setAccessTokenProvider(accessTokenProvider);
            if (!CollectionUtils.isEmpty(this.customizers)) {
                AnnotationAwareOrderComparator.sort(this.customizers);
                for (UserInfoRestTemplateCustomizer customizer : this.customizers) {
                    customizer.customize(this.oauth2RestTemplate); // ★★★
                }
            }
        }
        return this.oauth2RestTemplate;
    }

    // omitted.
}

注目すべきは、UserInfoRestTemplateCustomizer#customizeメソッドにより、OAuth2RestTemplateに任意の設定を追加できる点です。

次に、独自にOAuth2RestTemplateを生成しているAccessTokenProviderの抽象クラス(OAuth2AccessTokenSupport)を確認します。

public abstract class OAuth2AccessTokenSupport {

    // omitted.

    protected RestOperations getRestTemplate() {
        if (restTemplate == null) {
            synchronized (this) {
                if (restTemplate == null) {
                    RestTemplate restTemplate = new RestTemplate();
                    restTemplate.setErrorHandler(getResponseErrorHandler());
                    restTemplate.setRequestFactory(requestFactory); // ★★★
                    restTemplate.setInterceptors(interceptors);
                    this.restTemplate = restTemplate;
                }
            }
        }
        if (messageConverters == null) {
            setMessageConverters(new RestTemplate().getMessageConverters());
        }
        return restTemplate;
    }

    // omitted.

    public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
        Assert.notNull(requestFactory, "'requestFactory' must not be null");
        this.requestFactory = requestFactory;
    }

    // omitted.
}

注目すべきは、冒頭で説明したClientHttpRequestFactoryにより、内部的に生成するOAuth2RestTemplateにプロキシを適用できる点です。

ちなみに、OAuth2AccessTokenSupportではデフォルトで適用するClientHttpRequestFactoryprepareConnectionメソッドをオーバーライドしており、同様にオーバーライドしたほうが良いかもしれません。(細かく検証していないので、問題になるパターンには遭遇していませんが。)

@EnableOAuth2SsoでセットアップされるOAuth2RestTemplateにプロキシを適用する(実装編)

解析結果から、@EnableOAuth2SsoによりセットアップされるすべてのOAuth2RestTemplateに認証プロキシを適用します。

    // (1) リクエストにプロキシ認証情報を組み込むClientHttpRequestFactoryを生成する
    @Bean
    @ConditionalOnProperty("proxy.host")
    public ClientHttpRequestFactory requestFactory(
            @Value("${proxy.host}") String host,
            @Value("${proxy.port}") int port,
            @Value("${proxy.user}") String user,
            @Value("${proxy.password}") String password) {

        HttpClientBuilder builder = HttpClientBuilder.create();
        builder.setProxy(new HttpHost(host, port));

        if (StringUtils.hasText(user) && StringUtils.hasText(password)) {
            BasicCredentialsProvider provider = new BasicCredentialsProvider();
            provider.setCredentials(new AuthScope(host, port), new UsernamePasswordCredentials(user, password));
            builder.setDefaultCredentialsProvider(provider);
        }

        return new HttpComponentsClientHttpRequestFactory(builder.build());
    }

    // (2) 認証プロキシ用ClientHttpRequestFactoryをセットしたAccessTokenProviderを生成する
    @Bean
    @ConditionalOnBean(ClientHttpRequestFactory.class)
    public AccessTokenProvider accessTokenProvider(ClientHttpRequestFactory requestFactory) {

        AuthorizationCodeAccessTokenProvider authorizationCodeAccessTokenProvider = new AuthorizationCodeAccessTokenProvider();
        authorizationCodeAccessTokenProvider.setRequestFactory(requestFactory);
        ImplicitAccessTokenProvider implicitAccessTokenProvider = new ImplicitAccessTokenProvider();
        implicitAccessTokenProvider.setRequestFactory(requestFactory);
        ResourceOwnerPasswordAccessTokenProvider resourceOwnerPasswordAccessTokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
        resourceOwnerPasswordAccessTokenProvider.setRequestFactory(requestFactory);
        ClientCredentialsAccessTokenProvider clientCredentialsAccessTokenProvider = new ClientCredentialsAccessTokenProvider();
        clientCredentialsAccessTokenProvider.setRequestFactory(requestFactory);
        return new AccessTokenProviderChain(
                Arrays.asList(authorizationCodeAccessTokenProvider, implicitAccessTokenProvider,
                        resourceOwnerPasswordAccessTokenProvider, clientCredentialsAccessTokenProvider));
    }

    // (3) UserInfoRestTemplateCustomizerを利用して、OAuth2RestTemplateに認証プロキシ用ClientHttpRequestFactoryとAccessTokenProviderをセットする
    @Bean
    @ConditionalOnBean({ AccessTokenProvider.class, ClientHttpRequestFactory.class })
    public UserInfoRestTemplateCustomizer userInfoRestTemplateCustomizer(AccessTokenProvider accessTokenProvider,
            ClientHttpRequestFactory requestFactory) {

        return new UserInfoRestTemplateCustomizer() {
            @Override
            public void customize(OAuth2RestTemplate restTemplate) {
                restTemplate.setAccessTokenProvider(accessTokenProvider);
                restTemplate.setRequestFactory(requestFactory);
            }
        };
    }

    // (4) Controller等から直接利用するOAuth2RestTemplateにも、認証プロキシ用ClientHttpRequestFactoryとAccessTokenProviderをセットする
    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
            OAuth2ProtectedResourceDetails details, ObjectProvider<AccessTokenProvider> accessTokenProvider,
            ObjectProvider<ClientHttpRequestFactory> requestFactory) {

        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details, oauth2ClientContext);
        accessTokenProvider.ifAvailable(restTemplate::setAccessTokenProvider);
        requestFactory.ifAvailable(restTemplate::setRequestFactory);
        return restTemplate;
    }

今回の実装では、プロキシ環境とパブリック環境の両方で利用できるよう、@ConditionalOnProperty@ConditionalOnBeanにより生成するBeanを切り替えられるようにしています。
また、ObjectProviderは引数で受け取るBeanが存在しない場合や複数ある場合に対応するためのラッパーで、Java 8のOptionalのような役割を果たします。

修正したクライアントアプリで改めてSSOログインしてみると、、、

  • コンテキストルート(/)にアクセス
  • ログインURL(/login)にリダイレクト
  • Githubの認証URLにリダイレクト
  • 認可コードを付与してログインURL(/login)にリダイレクト
  • コンテキストルートが表示される

と、正常にログインできました。

まとめ

Autoconfigがデフォルトでプロキシを考慮していないことはしょうがないのですが、意外なほどOAuth2RestTemplateを使い回しておらず、設定したはずなのに何故動かないんだろう?と躓いてしまいました。。。
公式リファレンスで「OAuth2クライアントではOAuth2RestTemplateをBean定義して使え」と記載しているのに、Bean定義したものが利用されない場合があるのは直感的ではなく、ちょっと不親切ですね。今後改善されるといいなーと思います。

参考

11
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
13