From 4f44ea39761628f1b1fe2823dbdbe42fbdd1ced5 Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Tue, 28 Apr 2026 13:49:26 +0800 Subject: [PATCH] fix(auth): add OIDC sub claim validation and complete env example - Add null/blank validation for OIDC sub claim in CustomOidcUserService - Throw OAuth2AuthenticationException when sub is missing or blank - Complete .env.release.example with all required OIDC environment variables - Add test cases for sub validation and providerLogin fallback scenarios - All 5 tests passing --- .env.release.example | 13 ++++++-- .../auth/oauth/CustomOidcUserService.java | 5 +++ .../auth/oauth/CustomOidcUserServiceTest.java | 33 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/.env.release.example b/.env.release.example index ffd02231..6e003e50 100644 --- a/.env.release.example +++ b/.env.release.example @@ -63,8 +63,17 @@ OAUTH2_GITLAB_CLIENT_SECRET= OAUTH2_GITLAB_BASE_URI=https://gitlab.com OAUTH2_GITLAB_DISPLAY_NAME=GitLab -# Optional: OIDC providers are configured with Spring Security client registration variables. -# See docs/09-deployment.md for the full OIDC environment variable set and Compose override notes. +# Optional: OIDC login (e.g. Keycloak, Okta, Azure AD). +# Replace "OIDC" in variable names with your registration id (uppercase). +# The registration id becomes identity_binding.provider_code — keep it stable. +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID= +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET= +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_PROVIDER=oidc +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_AUTHORIZATION_GRANT_TYPE=authorization_code +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_REDIRECT_URI={baseUrl}/login/oauth2/code/{registrationId} +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_SCOPE=openid,profile,email +SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_NAME=OIDC +SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI= # SMTP configuration for password reset verification emails. SPRING_MAIL_HOST= diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOidcUserService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOidcUserService.java index eee9be06..6d8d09a4 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOidcUserService.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOidcUserService.java @@ -11,6 +11,7 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; @@ -66,6 +67,10 @@ public class CustomOidcUserService implements OAuth2UserService claims = new HashMap<>(oidcUser.getClaims()); String subject = asString(claims.get("sub")); + if (subject == null || subject.isBlank()) { + throw new OAuth2AuthenticationException( + new OAuth2Error("missing_sub", "OIDC sub claim is required", null)); + } String email = asString(claims.get("email")); boolean emailVerified = Boolean.TRUE.equals(claims.get("email_verified")); String providerLogin = firstPresent( diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/CustomOidcUserServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/CustomOidcUserServiceTest.java index dfdf10e6..1688ec67 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/CustomOidcUserServiceTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/CustomOidcUserServiceTest.java @@ -14,6 +14,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; @@ -21,6 +22,7 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -92,6 +94,37 @@ class CustomOidcUserServiceTest { assertThat(claims.providerLogin()).isEqualTo("Fallback Name"); } + @Test + void toOAuthClaims_throwsWhenSubIsMissing() { + OidcUser user = mock(OidcUser.class); + when(user.getClaims()).thenReturn(Map.of("email", "no-sub@example.com")); + assertThatThrownBy(() -> CustomOidcUserService.toOAuthClaims(oidcRequest(), user)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("sub"); + } + + @Test + void toOAuthClaims_throwsWhenSubIsBlank() { + OidcUser user = mock(OidcUser.class); + when(user.getClaims()).thenReturn(Map.of(IdTokenClaimNames.SUB, " ")); + assertThatThrownBy(() -> CustomOidcUserService.toOAuthClaims(oidcRequest(), user)) + .isInstanceOf(OAuth2AuthenticationException.class) + .hasMessageContaining("sub"); + } + + @Test + void toOAuthClaims_fallsBackToSubWhenAllOtherFieldsMissing() { + OAuthClaims claims = CustomOidcUserService.toOAuthClaims( + oidcRequest(), + oidcUser(Map.of(IdTokenClaimNames.SUB, "only-sub")) + ); + + assertThat(claims.subject()).isEqualTo("only-sub"); + assertThat(claims.providerLogin()).isEqualTo("only-sub"); + assertThat(claims.email()).isNull(); + assertThat(claims.emailVerified()).isFalse(); + } + private static OidcUserRequest oidcRequest() { Instant issuedAt = Instant.parse("2026-04-24T00:00:00Z"); OidcIdToken idToken = new OidcIdToken(