-
Notifications
You must be signed in to change notification settings - Fork 39
feat: add dedicated Logto OIDC config with custom JWS algorithm support #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||||||||||||||||||||||||
| import org.springframework.http.client.reactive.ReactorClientHttpConnector; | ||||||||||||||||||||||||||||
| import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager; | ||||||||||||||||||||||||||||
| import org.springframework.security.core.AuthenticationException; | ||||||||||||||||||||||||||||
| import org.springframework.security.oauth2.client.registration.ClientRegistration; | ||||||||||||||||||||||||||||
| import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager; | ||||||||||||||||||||||||||||
| import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient; | ||||||||||||||||||||||||||||
| import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager; | ||||||||||||||||||||||||||||
|
|
@@ -42,6 +43,7 @@ | |||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||
| public class HaloOAuth2AuthenticationWebFilter implements AuthenticationSecurityWebFilter { | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| static final String JWS_ALGORITHM_METADATA_KEY = "jwsAlgorithm"; | ||||||||||||||||||||||||||||
| private final WebFilter delegate; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| public HaloOAuth2AuthenticationWebFilter(Oauth2LoginConfiguration configuration, | ||||||||||||||||||||||||||||
|
|
@@ -83,25 +85,9 @@ public HaloOAuth2AuthenticationWebFilter(Oauth2LoginConfiguration configuration, | |||||||||||||||||||||||||||
| new OidcReactiveOAuth2UserService() | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| var oidcIdTokenDecodeFactory = new ReactiveOidcIdTokenDecoderFactory(); | ||||||||||||||||||||||||||||
| oidcIdTokenDecodeFactory.setJwsAlgorithmResolver(clientRegistration -> { | ||||||||||||||||||||||||||||
| var configurationMetadata = clientRegistration.getProviderDetails() | ||||||||||||||||||||||||||||
| .getConfigurationMetadata(); | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| var supportedJwsAlgorithms = JSONObjectUtils.getStringList( | ||||||||||||||||||||||||||||
| new JSONObject(configurationMetadata), | ||||||||||||||||||||||||||||
| "id_token_signing_alg_values_supported" | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| // we choose the first one as JWS algorithm | ||||||||||||||||||||||||||||
| if (!supportedJwsAlgorithms.isEmpty()) { | ||||||||||||||||||||||||||||
| var jwsAlgorithm = supportedJwsAlgorithms.get(0); | ||||||||||||||||||||||||||||
| return SignatureAlgorithm.from(jwsAlgorithm); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } catch (ParseException e) { | ||||||||||||||||||||||||||||
| // ignore the error. | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| // default algorithm | ||||||||||||||||||||||||||||
| return SignatureAlgorithm.RS256; | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| oidcIdTokenDecodeFactory.setJwsAlgorithmResolver( | ||||||||||||||||||||||||||||
| HaloOAuth2AuthenticationWebFilter::resolveJwsAlgorithm | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| oidcAuthManager.setJwtDecoderFactory(oidcIdTokenDecodeFactory); | ||||||||||||||||||||||||||||
| var authManager = | ||||||||||||||||||||||||||||
| new DelegatingReactiveAuthenticationManager(oauth2AuthManager, oidcAuthManager); | ||||||||||||||||||||||||||||
|
|
@@ -136,4 +122,27 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { | |||||||||||||||||||||||||||
| return delegate.filter(exchange, chain); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| static SignatureAlgorithm resolveJwsAlgorithm(ClientRegistration clientRegistration) { | ||||||||||||||||||||||||||||
| var configurationMetadata = clientRegistration.getProviderDetails() | ||||||||||||||||||||||||||||
| .getConfigurationMetadata(); | ||||||||||||||||||||||||||||
| var configuredJwsAlgorithm = configurationMetadata.get(JWS_ALGORITHM_METADATA_KEY); | ||||||||||||||||||||||||||||
| if (configuredJwsAlgorithm instanceof String jwsAlgorithm | ||||||||||||||||||||||||||||
| && StringUtils.hasText(jwsAlgorithm)) { | ||||||||||||||||||||||||||||
| return SignatureAlgorithm.from(jwsAlgorithm); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| return SignatureAlgorithm.from(jwsAlgorithm); | |
| try { | |
| return SignatureAlgorithm.from(jwsAlgorithm); | |
| } catch (IllegalArgumentException ex) { | |
| log.warn( | |
| "Invalid JWS algorithm '{}' configured for client '{}' in metadata key '{}'; " | |
| + "falling back to provider metadata/default.", | |
| jwsAlgorithm, | |
| clientRegistration.getRegistrationId(), | |
| JWS_ALGORITHM_METADATA_KEY, | |
| ex | |
| ); | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,7 +38,9 @@ | |||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||
| public class OauthClientRegistrationRepository implements ReactiveClientRegistrationRepository { | ||||||||||||||||||
| static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"; | ||||||||||||||||||
| static final String SSO_PROVIDER_NAME = "sso"; | ||||||||||||||||||
| static final String ADVANCED_OIDC_SETTING_GROUP = "ssoOauth"; | ||||||||||||||||||
| static final String LOGTO_SETTING_GROUP = "logtoOauth"; | ||||||||||||||||||
| static final String JWS_ALGORITHM_METADATA_KEY = "jwsAlgorithm"; | ||||||||||||||||||
| private final ReactiveExtensionClient client; | ||||||||||||||||||
| private final ExternalUrlSupplier externalUrlSupplier; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -74,7 +76,20 @@ private Mono<ClientRegistration> getClientRegistrationMono(AuthProvider authProv | |||||||||||||||||
| ) | ||||||||||||||||||
| .flatMap(data -> { | ||||||||||||||||||
| String value = data.getOrDefault(group, "{}"); | ||||||||||||||||||
| if (SSO_PROVIDER_NAME.equals(name)) { | ||||||||||||||||||
| if (LOGTO_SETTING_GROUP.equals(group)) { | ||||||||||||||||||
| if (StringUtils.isBlank(value) || "{}".equals(value.trim())) { | ||||||||||||||||||
| String legacyValue = data.getOrDefault(ADVANCED_OIDC_SETTING_GROUP, "{}"); | ||||||||||||||||||
| if (!"{}".equals(legacyValue.trim())) { | ||||||||||||||||||
| SsoClientConf ssoClientConf = | ||||||||||||||||||
| JsonUtils.jsonToObject(legacyValue, SsoClientConf.class); | ||||||||||||||||||
| return SsoClientRegistration(ssoClientConf, authProvider); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+80
to
+87
|
||||||||||||||||||
| if (StringUtils.isBlank(value) || "{}".equals(value.trim())) { | |
| String legacyValue = data.getOrDefault(ADVANCED_OIDC_SETTING_GROUP, "{}"); | |
| if (!"{}".equals(legacyValue.trim())) { | |
| SsoClientConf ssoClientConf = | |
| JsonUtils.jsonToObject(legacyValue, SsoClientConf.class); | |
| return SsoClientRegistration(ssoClientConf, authProvider); | |
| } | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package run.halo.oauth; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.junit.jupiter.api.Assertions.fail; | ||
|
|
||
| import java.lang.reflect.InvocationTargetException; | ||
| import java.lang.reflect.Method; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.security.oauth2.client.registration.ClientRegistration; | ||
| import org.springframework.security.oauth2.core.AuthorizationGrantType; | ||
| import org.springframework.security.oauth2.core.ClientAuthenticationMethod; | ||
| import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; | ||
|
|
||
| class HaloOAuth2AuthenticationWebFilterTest { | ||
|
|
||
| @Test | ||
| void explicitJwsAlgorithmShouldTakePrecedence() { | ||
| var registration = registrationBuilder() | ||
| .providerConfigurationMetadata( | ||
| Map.of( | ||
| "jwsAlgorithm", "ES384", | ||
| "id_token_signing_alg_values_supported", List.of("RS256") | ||
| ) | ||
| ) | ||
| .build(); | ||
|
|
||
| assertThat(resolveJwsAlgorithm(registration)).isEqualTo(SignatureAlgorithm.ES384); | ||
| } | ||
|
|
||
| @Test | ||
| void metadataSupportedAlgorithmsShouldFallbackToFirstValue() { | ||
| var registration = registrationBuilder() | ||
| .providerConfigurationMetadata( | ||
| Map.of("id_token_signing_alg_values_supported", List.of("ES384", "RS256")) | ||
| ) | ||
| .build(); | ||
|
|
||
| assertThat(resolveJwsAlgorithm(registration)).isEqualTo(SignatureAlgorithm.ES384); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldFallbackToDefaultRs256WhenMetadataIsMissing() { | ||
| var registration = registrationBuilder().build(); | ||
|
|
||
| assertThat(resolveJwsAlgorithm(registration)).isEqualTo(SignatureAlgorithm.RS256); | ||
| } | ||
|
|
||
| private static ClientRegistration.Builder registrationBuilder() { | ||
| return ClientRegistration.withRegistrationId("logto") | ||
| .clientId("client-id") | ||
| .clientSecret("client-secret") | ||
| .clientName("Logto") | ||
| .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) | ||
| .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) | ||
| .redirectUri("{baseUrl}/login/oauth2/code/logto") | ||
| .authorizationUri("https://logto.example.com/oidc/auth") | ||
| .tokenUri("https://logto.example.com/oidc/token") | ||
| .userInfoUri("https://logto.example.com/oidc/me") | ||
| .jwkSetUri("https://logto.example.com/oidc/jwks") | ||
| .userNameAttributeName("sub"); | ||
| } | ||
|
|
||
| private static SignatureAlgorithm resolveJwsAlgorithm(ClientRegistration registration) { | ||
| try { | ||
| Method method = HaloOAuth2AuthenticationWebFilter.class.getDeclaredMethod( | ||
| "resolveJwsAlgorithm", ClientRegistration.class | ||
| ); | ||
| method.setAccessible(true); | ||
| return (SignatureAlgorithm) method.invoke(null, registration); | ||
| } catch (NoSuchMethodException e) { | ||
| fail("Expected resolveJwsAlgorithm(ClientRegistration) helper to exist", e); | ||
| } catch (IllegalAccessException | InvocationTargetException e) { | ||
| fail("Failed to invoke resolveJwsAlgorithm(ClientRegistration)", e); | ||
| } | ||
|
Comment on lines
+65
to
+76
|
||
| throw new IllegalStateException("Unreachable"); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The metadata key
"jwsAlgorithm"is defined separately here and inOauthClientRegistrationRepository. Duplicating the literal/constant in multiple places risks future drift (one side changes and the other doesn’t), which would silently disable the override; consider centralizing the key in a shared constant (or referencing one class’s constant) to keep writer/reader aligned.