mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-06 02:48:28 +00:00
feat(auth): support oidc login
This commit is contained in:
parent
c5c76d3bb0
commit
bb9f8915e2
7 changed files with 312 additions and 11 deletions
|
|
@ -63,6 +63,9 @@ 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.
|
||||
|
||||
# SMTP configuration for password reset verification emails.
|
||||
SPRING_MAIL_HOST=
|
||||
SPRING_MAIL_PORT=587
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Layer 1: OAuth2 Login │ Spring Security OAuth2 Client
|
||||
│ (一期 GitHub,可扩展) │ 授权码模式 (Authorization Code)
|
||||
│ Layer 1: OAuth2/OIDC Login │ Spring Security OAuth2 Client
|
||||
│ (GitHub/GitLab/OIDC 可扩展) │ 授权码模式 (Authorization Code)
|
||||
│ Layer 1b: Session Bootstrap│ 显式被动会话引导(默认关闭)
|
||||
└─────────────┬───────────────┘
|
||||
│ OAuth2User
|
||||
|
|
@ -100,7 +100,7 @@ astron:
|
|||
|
||||
后续新增 OAuth Provider(Google、GitLab、微信)时,准入策略与 Provider 无关,统一在 AccessPolicy 层判定,不需要重做入驻逻辑。
|
||||
|
||||
## 3. Web 认证流程(OAuth2 Authorization Code)
|
||||
## 3. Web 认证流程(OAuth2 / OIDC Authorization Code)
|
||||
|
||||
```
|
||||
浏览器点击"登录"
|
||||
|
|
@ -124,7 +124,7 @@ Spring Security 自动完成:
|
|||
③ 触发自定义 OAuth2UserService
|
||||
│
|
||||
▼
|
||||
CustomOAuth2UserService:
|
||||
CustomOAuth2UserService / CustomOidcUserService:
|
||||
① 从 OAuth2User 提取 provider + externalId → 构建 OAuthClaims
|
||||
② AccessPolicy.evaluate(claims) → 准入判定
|
||||
│
|
||||
|
|
@ -142,6 +142,21 @@ AuthenticationSuccessHandler:
|
|||
② 重定向到前端页面 (可配置的 redirect_uri)
|
||||
```
|
||||
|
||||
OIDC 登录沿用同一条业务链路,但由 Spring Security 的 `oidcUserService`
|
||||
分支处理。`CustomOidcUserService` 会把标准 OIDC claims 映射为
|
||||
`OAuthClaims`:
|
||||
|
||||
- `provider`:Spring OAuth2 client registration id,例如 `okta`、`keycloak`
|
||||
或 `oidc`
|
||||
- `subject`:OIDC `sub`
|
||||
- `email` / `emailVerified`:`email` 与 `email_verified`
|
||||
- `providerLogin`:优先 `preferred_username`,其次 `name`、`email`、`sub`
|
||||
- `picture` 会同步为 `avatar_url`,供现有头像同步逻辑复用
|
||||
|
||||
因此 OIDC 不需要新增数据库表;现有 `identity_binding(provider_code,
|
||||
subject)` 可以保存任意 OIDC issuer 下的稳定用户标识。不同 IdP 应使用不同
|
||||
registration id,避免多个 issuer 的 `sub` 值空间混用。
|
||||
|
||||
### 3.1 统一 Session 建立约束
|
||||
|
||||
所有 Web 登录入口都必须通过统一的 `PlatformSessionService` 建立登录态,包括:
|
||||
|
|
|
|||
|
|
@ -197,7 +197,36 @@ docker compose --env-file .env.release -f compose.release.yml up -d
|
|||
- 如果要开放真实登录,再补充 `OAUTH2_GITHUB_CLIENT_ID` / `OAUTH2_GITHUB_CLIENT_SECRET`
|
||||
- 如果要启用密码重置验证码邮件,参见:`docs/19-smtp-password-reset-email-setup.md`
|
||||
|
||||
## 8 裸金属上线清单
|
||||
## 8 OIDC 登录配置
|
||||
|
||||
SkillHub 复用 Spring Security OAuth2 Client 的 OIDC 支持。前端不需要单独
|
||||
配置回调页;登录页会从 `/api/v1/auth/methods` 读取后端暴露的
|
||||
`OAUTH_REDIRECT` 方法并跳转到 `/oauth2/authorization/{registrationId}`。
|
||||
|
||||
生产环境接入 OIDC 时,为后端增加一组 OAuth2 client registration 配置即可。
|
||||
下面以 `oidc` 作为 registration id:
|
||||
|
||||
```bash
|
||||
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=replace-me
|
||||
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=replace-me
|
||||
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=https://idp.example.com/realms/skillhub
|
||||
```
|
||||
|
||||
要接入多个 OIDC IdP,使用不同 registration id,例如 `okta`、`keycloak`,
|
||||
并把上面的环境变量中的 `OIDC` 替换为对应大写 id。registration id 会作为
|
||||
`identity_binding.provider_code`,请保持稳定。
|
||||
|
||||
Docker Compose 发布模板默认只透传常用变量。若使用 OIDC,请通过 compose
|
||||
override 或部署平台环境变量把上述 `SPRING_SECURITY_*` 变量注入 `server`
|
||||
容器。Kubernetes 部署同理,将这些变量放入 `backend-deployment.yaml` 的
|
||||
`server` 容器环境变量或统一的配置管理系统中。
|
||||
|
||||
## 9 裸金属上线清单
|
||||
|
||||
推荐顺序:
|
||||
|
||||
|
|
@ -223,7 +252,7 @@ docker compose --env-file .env.release -f compose.release.yml up -d
|
|||
- 立即修改管理员密码
|
||||
- 如果后续完全走 OAuth,可将 `BOOTSTRAP_ADMIN_ENABLED=false`
|
||||
|
||||
## 9 可观测性
|
||||
## 10 可观测性
|
||||
|
||||
| 维度 | 方案 |
|
||||
|------|------|
|
||||
|
|
@ -231,7 +260,7 @@ docker compose --env-file .env.release -f compose.release.yml up -d
|
|||
| 日志 | 容器 stdout / stderr |
|
||||
| 指标 | Spring Boot Actuator,后续可接 Prometheus |
|
||||
|
||||
## 10 安全扫描服务
|
||||
## 11 安全扫描服务
|
||||
|
||||
如果要启用 `skill-scanner` 后端链路,当前仓库建议按下面的方式部署:
|
||||
|
||||
|
|
@ -252,7 +281,7 @@ docker compose --env-file .env.release -f compose.release.yml up -d
|
|||
- `scripts/verify-scanner.sh`
|
||||
- `docs/security-scanning.md`
|
||||
|
||||
## 11 数据迁移
|
||||
## 12 数据迁移
|
||||
|
||||
Flyway 仍是唯一 schema 变更入口:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.iflytek.skillhub.auth.config;
|
||||
|
||||
import com.iflytek.skillhub.auth.oauth.CustomOAuth2UserService;
|
||||
import com.iflytek.skillhub.auth.oauth.CustomOidcUserService;
|
||||
import com.iflytek.skillhub.auth.oauth.OAuth2LoginFailureHandler;
|
||||
import com.iflytek.skillhub.auth.oauth.OAuth2LoginSuccessHandler;
|
||||
import com.iflytek.skillhub.auth.oauth.SkillHubOAuth2AuthorizationRequestResolver;
|
||||
|
|
@ -51,6 +52,7 @@ public class SecurityConfig {
|
|||
"form-action 'self'");
|
||||
|
||||
private final CustomOAuth2UserService customOAuth2UserService;
|
||||
private final CustomOidcUserService customOidcUserService;
|
||||
private final SkillHubOAuth2AuthorizationRequestResolver authorizationRequestResolver;
|
||||
private final OAuth2LoginSuccessHandler successHandler;
|
||||
private final OAuth2LoginFailureHandler failureHandler;
|
||||
|
|
@ -62,6 +64,7 @@ public class SecurityConfig {
|
|||
private final RouteSecurityPolicyRegistry routeSecurityPolicyRegistry;
|
||||
|
||||
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService,
|
||||
CustomOidcUserService customOidcUserService,
|
||||
SkillHubOAuth2AuthorizationRequestResolver authorizationRequestResolver,
|
||||
OAuth2LoginSuccessHandler successHandler,
|
||||
OAuth2LoginFailureHandler failureHandler,
|
||||
|
|
@ -72,6 +75,7 @@ public class SecurityConfig {
|
|||
ObjectProvider<MockAuthFilter> mockAuthFilterProvider,
|
||||
RouteSecurityPolicyRegistry routeSecurityPolicyRegistry) {
|
||||
this.customOAuth2UserService = customOAuth2UserService;
|
||||
this.customOidcUserService = customOidcUserService;
|
||||
this.authorizationRequestResolver = authorizationRequestResolver;
|
||||
this.successHandler = successHandler;
|
||||
this.failureHandler = failureHandler;
|
||||
|
|
@ -112,7 +116,9 @@ public class SecurityConfig {
|
|||
})
|
||||
.oauth2Login(oauth2 -> oauth2
|
||||
.authorizationEndpoint(endpoint -> endpoint.authorizationRequestResolver(authorizationRequestResolver))
|
||||
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
|
||||
.userInfoEndpoint(userInfo -> userInfo
|
||||
.userService(customOAuth2UserService)
|
||||
.oidcUserService(customOidcUserService))
|
||||
.successHandler(successHandler)
|
||||
.failureHandler(failureHandler)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
package com.iflytek.skillhub.auth.oauth;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
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.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Spring Security OIDC user-service bridge that normalizes standard OIDC
|
||||
* claims and reuses the existing OAuth login policy and identity binding flow.
|
||||
*/
|
||||
@Service
|
||||
public class CustomOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||
|
||||
private final OAuthLoginFlowService oauthLoginFlowService;
|
||||
private final OAuth2UserService<OidcUserRequest, OidcUser> delegate;
|
||||
|
||||
@Autowired
|
||||
public CustomOidcUserService(OAuthLoginFlowService oauthLoginFlowService) {
|
||||
this(oauthLoginFlowService, new OidcUserService());
|
||||
}
|
||||
|
||||
CustomOidcUserService(OAuthLoginFlowService oauthLoginFlowService,
|
||||
OAuth2UserService<OidcUserRequest, OidcUser> delegate) {
|
||||
this.oauthLoginFlowService = oauthLoginFlowService;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OidcUser loadUser(OidcUserRequest request) throws OAuth2AuthenticationException {
|
||||
OidcUser upstreamUser = delegate.loadUser(request);
|
||||
OAuthClaims claims = toOAuthClaims(request, upstreamUser);
|
||||
PlatformPrincipal principal = oauthLoginFlowService.authenticate(claims);
|
||||
|
||||
Map<String, Object> userInfoClaims = new HashMap<>(upstreamUser.getClaims());
|
||||
if (upstreamUser.getUserInfo() != null) {
|
||||
userInfoClaims.putAll(upstreamUser.getUserInfo().getClaims());
|
||||
}
|
||||
userInfoClaims.put("platformPrincipal", principal);
|
||||
userInfoClaims.put("providerLogin", principal.userId());
|
||||
|
||||
var authorities = new LinkedHashSet<GrantedAuthority>(upstreamUser.getAuthorities());
|
||||
principal.platformRoles().stream()
|
||||
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
||||
.forEach(authorities::add);
|
||||
|
||||
return new DefaultOidcUser(
|
||||
authorities,
|
||||
upstreamUser.getIdToken(),
|
||||
new OidcUserInfo(userInfoClaims),
|
||||
"providerLogin"
|
||||
);
|
||||
}
|
||||
|
||||
static OAuthClaims toOAuthClaims(OidcUserRequest request, OidcUser oidcUser) {
|
||||
Map<String, Object> claims = new HashMap<>(oidcUser.getClaims());
|
||||
String subject = asString(claims.get("sub"));
|
||||
String email = asString(claims.get("email"));
|
||||
boolean emailVerified = Boolean.TRUE.equals(claims.get("email_verified"));
|
||||
String providerLogin = firstPresent(
|
||||
asString(claims.get("preferred_username")),
|
||||
asString(claims.get("name")),
|
||||
email,
|
||||
subject
|
||||
);
|
||||
if (claims.get("picture") != null && claims.get("avatar_url") == null) {
|
||||
claims.put("avatar_url", claims.get("picture"));
|
||||
}
|
||||
|
||||
return new OAuthClaims(
|
||||
request.getClientRegistration().getRegistrationId(),
|
||||
subject,
|
||||
email,
|
||||
emailVerified,
|
||||
providerLogin,
|
||||
claims
|
||||
);
|
||||
}
|
||||
|
||||
private static String firstPresent(String... values) {
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String asString(Object value) {
|
||||
return value instanceof String str ? str : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,11 @@ public class OAuthLoginFlowService {
|
|||
}
|
||||
|
||||
OAuthClaims claims = extractor.extract(request, upstreamUser);
|
||||
PlatformPrincipal principal = authenticate(claims);
|
||||
return new AuthenticatedLoginContext(upstreamUser, principal);
|
||||
}
|
||||
|
||||
public PlatformPrincipal authenticate(OAuthClaims claims) {
|
||||
AccessDecision decision = accessPolicy.evaluate(claims);
|
||||
|
||||
if (decision == AccessDecision.PENDING_APPROVAL) {
|
||||
|
|
@ -67,8 +72,7 @@ public class OAuthLoginFlowService {
|
|||
);
|
||||
}
|
||||
|
||||
PlatformPrincipal principal = identityBindingService.bindOrCreate(claims, UserStatus.ACTIVE);
|
||||
return new AuthenticatedLoginContext(upstreamUser, principal);
|
||||
return identityBindingService.bindOrCreate(claims, UserStatus.ACTIVE);
|
||||
}
|
||||
|
||||
public void rememberReturnTo(HttpServletRequest request) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
package com.iflytek.skillhub.auth.oauth;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
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.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
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;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class CustomOidcUserServiceTest {
|
||||
|
||||
@Test
|
||||
void loadUser_mapsOidcClaimsThroughExistingLoginFlow() {
|
||||
OAuthLoginFlowService loginFlowService = mock(OAuthLoginFlowService.class);
|
||||
OAuth2UserService<OidcUserRequest, OidcUser> delegate = mock();
|
||||
CustomOidcUserService service = new CustomOidcUserService(loginFlowService, delegate);
|
||||
OidcUserRequest request = oidcRequest();
|
||||
OidcUser upstreamUser = oidcUser(Map.of(
|
||||
IdTokenClaimNames.SUB, "oidc-sub-1",
|
||||
"email", "user@example.com",
|
||||
"email_verified", true,
|
||||
"preferred_username", "preferred-user",
|
||||
"name", "Display User",
|
||||
"picture", "https://idp.example/avatar.png"
|
||||
));
|
||||
PlatformPrincipal platformPrincipal = new PlatformPrincipal(
|
||||
"usr_1",
|
||||
"Preferred User",
|
||||
"user@example.com",
|
||||
"https://idp.example/avatar.png",
|
||||
"okta",
|
||||
Set.of("USER", "SUPER_ADMIN")
|
||||
);
|
||||
when(delegate.loadUser(request)).thenReturn(upstreamUser);
|
||||
when(loginFlowService.authenticate(any())).thenReturn(platformPrincipal);
|
||||
|
||||
OidcUser loadedUser = service.loadUser(request);
|
||||
|
||||
ArgumentCaptor<OAuthClaims> claimsCaptor = ArgumentCaptor.forClass(OAuthClaims.class);
|
||||
verify(loginFlowService).authenticate(claimsCaptor.capture());
|
||||
OAuthClaims claims = claimsCaptor.getValue();
|
||||
assertThat(claims.provider()).isEqualTo("okta");
|
||||
assertThat(claims.subject()).isEqualTo("oidc-sub-1");
|
||||
assertThat(claims.email()).isEqualTo("user@example.com");
|
||||
assertThat(claims.emailVerified()).isTrue();
|
||||
assertThat(claims.providerLogin()).isEqualTo("preferred-user");
|
||||
assertThat(claims.extra()).containsEntry("avatar_url", "https://idp.example/avatar.png");
|
||||
|
||||
assertThat((Object) loadedUser.getAttribute("platformPrincipal")).isEqualTo(platformPrincipal);
|
||||
assertThat((Object) loadedUser.getAttribute("providerLogin")).isEqualTo("usr_1");
|
||||
assertThat(loadedUser.getName()).isEqualTo("usr_1");
|
||||
assertThat(loadedUser.getAuthorities())
|
||||
.extracting(GrantedAuthority::getAuthority)
|
||||
.contains("ROLE_USER", "ROLE_SUPER_ADMIN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toOAuthClaims_fallsBackToNameWhenPreferredUsernameIsMissing() {
|
||||
OAuthClaims claims = CustomOidcUserService.toOAuthClaims(
|
||||
oidcRequest(),
|
||||
oidcUser(Map.of(
|
||||
IdTokenClaimNames.SUB, "subject-2",
|
||||
"email", "fallback@example.com",
|
||||
"email_verified", false,
|
||||
"name", "Fallback Name"
|
||||
))
|
||||
);
|
||||
|
||||
assertThat(claims.provider()).isEqualTo("okta");
|
||||
assertThat(claims.subject()).isEqualTo("subject-2");
|
||||
assertThat(claims.email()).isEqualTo("fallback@example.com");
|
||||
assertThat(claims.emailVerified()).isFalse();
|
||||
assertThat(claims.providerLogin()).isEqualTo("Fallback Name");
|
||||
}
|
||||
|
||||
private static OidcUserRequest oidcRequest() {
|
||||
Instant issuedAt = Instant.parse("2026-04-24T00:00:00Z");
|
||||
OidcIdToken idToken = new OidcIdToken(
|
||||
"id-token",
|
||||
issuedAt,
|
||||
issuedAt.plusSeconds(300),
|
||||
Map.of(IdTokenClaimNames.SUB, "request-sub")
|
||||
);
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(
|
||||
OAuth2AccessToken.TokenType.BEARER,
|
||||
"access-token",
|
||||
issuedAt,
|
||||
issuedAt.plusSeconds(300)
|
||||
);
|
||||
return new OidcUserRequest(clientRegistration(), accessToken, idToken);
|
||||
}
|
||||
|
||||
private static OidcUser oidcUser(Map<String, Object> claims) {
|
||||
Instant issuedAt = Instant.parse("2026-04-24T00:00:00Z");
|
||||
OidcIdToken idToken = new OidcIdToken(
|
||||
"id-token",
|
||||
issuedAt,
|
||||
issuedAt.plusSeconds(300),
|
||||
claims
|
||||
);
|
||||
return new DefaultOidcUser(
|
||||
List.of(new SimpleGrantedAuthority("OIDC_USER")),
|
||||
idToken,
|
||||
new OidcUserInfo(claims)
|
||||
);
|
||||
}
|
||||
|
||||
private static ClientRegistration clientRegistration() {
|
||||
return ClientRegistration.withRegistrationId("okta")
|
||||
.clientId("client")
|
||||
.clientSecret("secret")
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUri("https://skillhub.example/login/oauth2/code/okta")
|
||||
.authorizationUri("https://idp.example/oauth2/v1/authorize")
|
||||
.tokenUri("https://idp.example/oauth2/v1/token")
|
||||
.jwkSetUri("https://idp.example/oauth2/v1/keys")
|
||||
.userInfoUri("https://idp.example/oauth2/v1/userinfo")
|
||||
.userNameAttributeName(IdTokenClaimNames.SUB)
|
||||
.scope("openid", "profile", "email")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue