Spring Authorization ServerでOpaqueトークンを利用する

2023/02/12に公開

Spring Authorization ServerではアクセストークンはデフォルトでJWTを返す設定となっています。
これをOpaqueトークンに変更する方法です。

設定

まず設定ファイル全体はこのような形となっています。
docs.spring.ioのGetting Startedの設定を元にOpaqueトークンように一部を書き換えています。

AuthorizationServerConfig.java
@Configuration
public class AuthorizationServerConfig {

  @Bean
  @Order(1)
  public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
      throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class);
    http
        // Redirect to the login page when not authenticated from the
        // authorization endpoint
        .exceptionHandling((exceptions) -> exceptions
            .authenticationEntryPoint(
                new LoginUrlAuthenticationEntryPoint("/login"))
        )
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);

    return http.build();
  }

  @Bean
  @Order(2)
  public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
      throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        // Form login handles the redirect to the login page from the
        // authorization server filter chain
        .formLogin(Customizer.withDefaults())
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);

    return http.build();
  }

  @Bean
  public UserDetailsService userDetailsService() {
    UserDetails userDetails = User.withDefaultPasswordEncoder()
        .username("user")
        .password("password")
        .roles("USER")
        .build();

    return new InMemoryUserDetailsManager(userDetails);
  }

  @Bean
  public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId("messaging-client")
        .clientSecret("{noop}secret")
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
        .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
        .redirectUri("http://127.0.0.1:8080/authorized")
        .scope(OidcScopes.OPENID)
        .scope(OidcScopes.PROFILE)
        .scope("message.read")
        .scope("message.write")
        .tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE).build())
        .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
        .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
  }

  @Bean
  public AuthorizationServerSettings authorizationServerSettings() {
    return AuthorizationServerSettings.builder().build();
  }

変更したところを記載します。

まずはSecurityFilterChainのBean生成箇所です。
まずOIDCは利用しないため、oidcの有効設定は除外しています。
またリソースサーバの設定をしていますが、Opaqueトークンを利用するため、OAuth2ResourceServerConfigurer::opaqueTokenの設定に変更しています。JWTの場合はOAuth2ResourceServerConfigurer::jwtです。

  @Bean
  @Order(1)
  public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
      throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class);
    http
        // Redirect to the login page when not authenticated from the
        // authorization endpoint
        .exceptionHandling((exceptions) -> exceptions
            .authenticationEntryPoint(
                new LoginUrlAuthenticationEntryPoint("/login"))
        )
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);

    return http.build();
  }

こちらのSecurityFilterChainのBean生成箇所も変更しています。
先程と同様にoauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken)の設定を追加しています。

  @Order(2)
  public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
      throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        // Form login handles the redirect to the login page from the
        // authorization server filter chain
        .formLogin(Customizer.withDefaults())
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);

    return http.build();
  }

続いてはクライアントの設定です。RegisteredClientRepositoryのBean生成箇所です。
tokenSettingsの箇所でaccessTokenFormatにOAuth2TokenFormat.REFERENCEを設定しています。
JWTの場合はOAuth2TokenFormat.SELF_CONTAINEDです。

  @Bean
  public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
        // 省略
        .scope("message.write")
        .tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE).build())
        .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
        .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
  }

その他Getting StartedではJWKSourceJwtDecoderのBean生成が必要となりますが、Opaqueトークンのため不要になります。

リソースサーバ用にapplication.yamlも設定します。

application.yaml
spring:
  security:
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: http://localhost:8080/oauth2/introspect
          client-id: messaging-client
          client-secret: secret

これで以上になります。

動作確認

念の為動作確認したものも記載しておきます。

  1. 認証エンドポイント

http://localhost:8080/oauth2/authorize?response_type=code&scope=message.read&client_id=messaging-client&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Flogin%2Foauth2%2Fcode%2Fmessaging-client-oidc

  1. ログイン・認可などを行うと以下の以下のリダイレクトにより認可コードが発行される

http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=U9cwPldXFXtd4Fg25YrQDDXhzVEQIB3U5rqT9WIZo-VRV42ClZLPixXo5NlwovGvw_XPzpEOK4tpVU6SDSQBYHhra_HRwMZfQaAxxRT3iJZ2zkMkE94k1HkqvZp1aSey

  1. トークンを発行する
$ curl -X POST -v "http://localhost:8080/oauth2/token" -H 'Content-Type: application/x-www-form-urlencoded' -u messaging-client:secret -d "grant_type=authorization_code&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Flogin%2Foauth2%2Fcode%2Fmessaging-client-oidc&code=U9cwPldXFXtd4Fg25YrQDDXhzVEQIB3U5rqT9WIZo-VRV42ClZLPixXo5NlwovGvw_XPzpEOK4tpVU6SDSQBYHhra_HRwMZfQaAxxRT3iJZ2zkMkE94k1HkqvZp1aSey"

レスポンスの内容です。

{
	"access_token": "LyXqHNzeDcBgZ0FHANC08yo06xITTCKNjQtdvBVMVzJ7vCG6lLGB3vp9be0r2FBbywjTI5sR-tZPRq7kT7aBXbYLSS4jdEGiHqaMv40jqOsbCc504ABFpqf3DH-eh8ua",
	"refresh_token": "NTSTC2XNMdLL-fykdRFk8PPh22xKeZQq-1wvcPGNcu0BYxfEXUJJILc4G2-2k1J5xffL9Td0n7P6OIWfV0lB_IX8l081pTOFfCIz-V7cxuoUvzX17aGXvb068TFcOfx5",
	"scope": "message.read",
	"token_type": "Bearer",
	"expires_in": 299
}

access_tokenがJWTではないことが確認できます。

  1. イントロスペクションエンドポイントでトークンの中身を確認します。
$ curl -v -X POST "http://localhost:8080/oauth2/introspect" -u messaging-client:secret -d "token=LyXqHNzeDcBgZ0FHANC08yo06xITTCKNjQtdvBVMVzJ7vCG6lLGB3vp9be0r2FBbywjTI5sR-tZPRq7kT7aBXbYLSS4jdEGiHqaMv40jqOsbCc504ABFpqf3DH-eh8ua"

レスポンスの内容です。

{
	"active": true,
	"sub": "user",
	"aud": [
		"messaging-client"
	],
	"nbf": 1676191251,
	"scope": "message.read",
	"iss": "http://localhost:8080",
	"exp": 1676191551,
	"iat": 1676191251,
	"jti": "4945e50e-1776-4b77-a993-beb72a055e92",
	"client_id": "messaging-client",
	"token_type": "Bearer"
}

無事にトークンの中身を取得できました。

参考情報

Discussion