Spring Authorization ServerでOpaqueトークンを利用する
Spring Authorization ServerではアクセストークンはデフォルトでJWTを返す設定となっています。
これをOpaqueトークンに変更する方法です。
設定
まず設定ファイル全体はこのような形となっています。
docs.spring.ioのGetting Startedの設定を元にOpaqueトークンように一部を書き換えています。
@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ではJWKSource
とJwtDecoder
のBean生成が必要となりますが、Opaqueトークンのため不要になります。
リソースサーバ用にapplication.yamlも設定します。
spring:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: http://localhost:8080/oauth2/introspect
client-id: messaging-client
client-secret: secret
これで以上になります。
動作確認
念の為動作確認したものも記載しておきます。
- 認証エンドポイント
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
- ログイン・認可などを行うと以下の以下のリダイレクトにより認可コードが発行される
http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=U9cwPldXFXtd4Fg25YrQDDXhzVEQIB3U5rqT9WIZo-VRV42ClZLPixXo5NlwovGvw_XPzpEOK4tpVU6SDSQBYHhra_HRwMZfQaAxxRT3iJZ2zkMkE94k1HkqvZp1aSey
- トークンを発行する
$ 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ではないことが確認できます。
- イントロスペクションエンドポイントでトークンの中身を確認します。
$ 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