fix(auth): provision OIDC users on first contact via resource-server flow
JwtAuthenticationFilter.tryOidcToken validated external access tokens against the IdP's JWKS but never upserted the subject into `users`. Any later write that FKs `users(user_id)` (deployments.created_by, alert_rules.created_by, outbound_connections.created_by, ...) blew up with a foreign-key violation — the interactive /auth/oidc/callback path upserts here, the resource-server path silently skipped it. Add OidcAccountSyncService: short-circuits when the user already exists, otherwise honours OidcConfig.autoSignup (defaulting to true when no DB row, since OIDC-via-env-var implies admin opt-in), enforces the max_users license cap, and persists UserInfo with provider, email, and displayName drawn from the JWT claims. The filter falls through to anonymous (Spring → 401) on refusal instead of authenticating an un-persisted principal that would 5xx on the next FK insert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -173,7 +173,8 @@ Env-scoped read-path controllers (`AlertController`, `AlertRuleController`, `Ale
|
|||||||
## security/ — Spring Security
|
## security/ — Spring Security
|
||||||
|
|
||||||
- `SecurityConfig` — WebSecurityFilterChain, JWT filter, CORS, OIDC conditional. `/api/v1/admin/outbound-connections/**` GETs permit OPERATOR in addition to ADMIN (defense-in-depth at controller level); mutations remain ADMIN-only. Alerting matchers: GET `/environments/*/alerts/**` VIEWER+; POST/PUT/DELETE rules and silences OPERATOR+; ack/read/bulk-read VIEWER+; POST `/alerts/notifications/*/retry` OPERATOR+.
|
- `SecurityConfig` — WebSecurityFilterChain, JWT filter, CORS, OIDC conditional. `/api/v1/admin/outbound-connections/**` GETs permit OPERATOR in addition to ADMIN (defense-in-depth at controller level); mutations remain ADMIN-only. Alerting matchers: GET `/environments/*/alerts/**` VIEWER+; POST/PUT/DELETE rules and silences OPERATOR+; ack/read/bulk-read VIEWER+; POST `/alerts/notifications/*/retry` OPERATOR+.
|
||||||
- `JwtAuthenticationFilter` — OncePerRequestFilter, validates Bearer tokens
|
- `JwtAuthenticationFilter` — OncePerRequestFilter, validates Bearer tokens. Tries internal HMAC first; on failure (and when `oidcDecoder != null`) falls back to external-IdP validation. Resource-server path delegates to `OidcAccountSyncService.ensureProvisioned(jwt)` to upsert the user into `users` on first contact — without it, every later FK-to-`users(user_id)` insert (`deployments.created_by`, `alert_rules.created_by`, …) would 500 with a foreign-key violation. Sets `principal.name` to the bare `oidc:<sub>` so the env-scoped strip-`"user:"` convention is a no-op (still produces the correct FK value).
|
||||||
|
- `OidcAccountSyncService` — provisions OIDC users into `users` from the `JwtAuthenticationFilter` resource-server path. `ensureProvisioned(Jwt)` short-circuits when the user exists; otherwise reads `OidcConfigRepository` (defaults `autoSignup=true` when no row — i.e., OIDC configured purely via env var), enforces the `max_users` cap via `LicenseEnforcer`, then upserts `UserInfo(userId="oidc:<sub>", provider="oidc:<issuer-host>", email, displayName, createdAt)`. Returns `Optional.empty()` on refusal so the filter falls through to anonymous (Spring → 401), never throws.
|
||||||
- `JwtServiceImpl` — HMAC-SHA256 JWT (Nimbus JOSE)
|
- `JwtServiceImpl` — HMAC-SHA256 JWT (Nimbus JOSE)
|
||||||
- `UiAuthController` — `/api/v1/auth` (login, refresh, me, logout). Upserts `users.user_id = request.username()` (bare); signs JWTs with `subject = "user:" + userId`. `refresh`/`me`/`logout` strip the `"user:"` prefix from incoming subjects via `stripSubjectPrefix()` before any DB/RBAC lookup. `logout` revokes outstanding tokens by writing `users.token_revoked_before` and audits under `AuditCategory.AUTH / logout`.
|
- `UiAuthController` — `/api/v1/auth` (login, refresh, me, logout). Upserts `users.user_id = request.username()` (bare); signs JWTs with `subject = "user:" + userId`. `refresh`/`me`/`logout` strip the `"user:"` prefix from incoming subjects via `stripSubjectPrefix()` before any DB/RBAC lookup. `logout` revokes outstanding tokens by writing `users.token_revoked_before` and audits under `AuditCategory.AUTH / logout`.
|
||||||
- `OidcAuthController` — `/api/v1/auth/oidc` (login-uri, token-exchange, logout). Upserts `users.user_id = "oidc:" + oidcUser.subject()` (no `user:` prefix); signs JWTs with `subject = "user:oidc:" + oidcUser.subject()`. `applyClaimMappings` + `getSystemRoleNames` calls all use the bare `oidc:<sub>` form.
|
- `OidcAuthController` — `/api/v1/auth/oidc` (login-uri, token-exchange, logout). Upserts `users.user_id = "oidc:" + oidcUser.subject()` (no `user:` prefix); signs JWTs with `subject = "user:oidc:" + oidcUser.subject()`. `applyClaimMappings` + `getSystemRoleNames` calls all use the bare `oidc:<sub>` form.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JWT authentication filter that extracts and validates JWT tokens from
|
* JWT authentication filter that extracts and validates JWT tokens from
|
||||||
@@ -47,17 +48,20 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
private final JwtDecoder oidcDecoder;
|
private final JwtDecoder oidcDecoder;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final ServerMetrics serverMetrics;
|
private final ServerMetrics serverMetrics;
|
||||||
|
private final OidcAccountSyncService oidcAccountSync;
|
||||||
|
|
||||||
public JwtAuthenticationFilter(JwtService jwtService,
|
public JwtAuthenticationFilter(JwtService jwtService,
|
||||||
AgentRegistryService agentRegistryService,
|
AgentRegistryService agentRegistryService,
|
||||||
JwtDecoder oidcDecoder,
|
JwtDecoder oidcDecoder,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
ServerMetrics serverMetrics) {
|
ServerMetrics serverMetrics,
|
||||||
|
OidcAccountSyncService oidcAccountSync) {
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
this.agentRegistryService = agentRegistryService;
|
this.agentRegistryService = agentRegistryService;
|
||||||
this.oidcDecoder = oidcDecoder;
|
this.oidcDecoder = oidcDecoder;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.serverMetrics = serverMetrics;
|
this.serverMetrics = serverMetrics;
|
||||||
|
this.oidcAccountSync = oidcAccountSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -117,11 +121,20 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
private void tryOidcToken(String token, HttpServletRequest request) {
|
private void tryOidcToken(String token, HttpServletRequest request) {
|
||||||
try {
|
try {
|
||||||
Jwt jwt = oidcDecoder.decode(token);
|
Jwt jwt = oidcDecoder.decode(token);
|
||||||
|
// Provision the user into the `users` table on first contact so any later
|
||||||
|
// FK insert (deployments.created_by, alert_rules.created_by, etc.) resolves.
|
||||||
|
// The interactive /auth/oidc/callback path upserts here too; without this the
|
||||||
|
// resource-server flow leaves the principal authenticated but un-persisted.
|
||||||
|
Optional<String> userId = oidcAccountSync.ensureProvisioned(jwt);
|
||||||
|
if (userId.isEmpty()) {
|
||||||
|
serverMetrics.recordAuthFailure("oidc_rejected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<String> roles = extractRolesFromScopes(jwt);
|
List<String> roles = extractRolesFromScopes(jwt);
|
||||||
List<GrantedAuthority> authorities = toAuthorities(roles);
|
List<GrantedAuthority> authorities = toAuthorities(roles);
|
||||||
UsernamePasswordAuthenticationToken auth =
|
UsernamePasswordAuthenticationToken auth =
|
||||||
new UsernamePasswordAuthenticationToken(
|
new UsernamePasswordAuthenticationToken(
|
||||||
"oidc:" + jwt.getSubject(), null, authorities);
|
userId.get(), null, authorities);
|
||||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("OIDC token validation failed: {}", e.getMessage());
|
log.debug("OIDC token validation failed: {}", e.getMessage());
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package io.cameleer.server.app.security;
|
||||||
|
|
||||||
|
import io.cameleer.server.app.license.LicenseEnforcer;
|
||||||
|
import io.cameleer.server.core.security.OidcConfig;
|
||||||
|
import io.cameleer.server.core.security.OidcConfigRepository;
|
||||||
|
import io.cameleer.server.core.security.UserInfo;
|
||||||
|
import io.cameleer.server.core.security.UserRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provisions OIDC users into the {@code users} table on first contact through the
|
||||||
|
* resource-server (Logto/external IdP) flow. Mirrors the upsert path in
|
||||||
|
* {@link OidcAuthController} but runs from {@link JwtAuthenticationFilter} where only
|
||||||
|
* a parsed {@link Jwt} is available -- there is no token-exchange round-trip and no
|
||||||
|
* {@code OidcUserInfo} record.
|
||||||
|
*
|
||||||
|
* <p>Returns {@link Optional#empty()} (rather than throwing) when the token represents
|
||||||
|
* a user who must not be auto-provisioned, so the filter can fall through to anonymous
|
||||||
|
* and Spring returns 401 on protected endpoints rather than a 5xx FK violation later.
|
||||||
|
*
|
||||||
|
* <p>OIDC config row may be absent when OIDC is configured purely via env var
|
||||||
|
* ({@code CAMELEER_SERVER_SECURITY_OIDCISSUERURI}). In that case autoSignup defaults to
|
||||||
|
* true, since accepting external tokens at boot already implies admin opt-in.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class OidcAccountSyncService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(OidcAccountSyncService.class);
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final OidcConfigRepository oidcConfigRepository;
|
||||||
|
private final LicenseEnforcer licenseEnforcer;
|
||||||
|
|
||||||
|
public OidcAccountSyncService(UserRepository userRepository,
|
||||||
|
OidcConfigRepository oidcConfigRepository,
|
||||||
|
LicenseEnforcer licenseEnforcer) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.oidcConfigRepository = oidcConfigRepository;
|
||||||
|
this.licenseEnforcer = licenseEnforcer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a {@code users} row exists for the OIDC subject in {@code jwt}.
|
||||||
|
* Returns the bare {@code users.user_id} (e.g. {@code "oidc:r6uzeamz0wuy"}) on
|
||||||
|
* success, or empty if provisioning was refused.
|
||||||
|
*/
|
||||||
|
public Optional<String> ensureProvisioned(Jwt jwt) {
|
||||||
|
String userId = "oidc:" + jwt.getSubject();
|
||||||
|
if (userRepository.findById(userId).isPresent()) {
|
||||||
|
return Optional.of(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean autoSignup = oidcConfigRepository.find()
|
||||||
|
.map(OidcConfig::autoSignup)
|
||||||
|
.orElse(true);
|
||||||
|
if (!autoSignup) {
|
||||||
|
log.warn("OIDC user {} not provisioned and autoSignup is disabled; rejecting token.", userId);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
licenseEnforcer.assertWithinCap("max_users", userRepository.count(), 1);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.warn("OIDC user {} not provisioned: {}.", userId, e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String issuerHost = jwt.getIssuer() != null ? jwt.getIssuer().getHost() : "unknown";
|
||||||
|
String email = jwt.getClaimAsString("email");
|
||||||
|
String name = firstNonBlank(
|
||||||
|
jwt.getClaimAsString("name"),
|
||||||
|
jwt.getClaimAsString("preferred_username"),
|
||||||
|
email);
|
||||||
|
userRepository.upsert(new UserInfo(
|
||||||
|
userId,
|
||||||
|
"oidc:" + issuerHost,
|
||||||
|
email != null ? email : "",
|
||||||
|
name != null ? name : "",
|
||||||
|
Instant.now()));
|
||||||
|
log.info("Provisioned OIDC user via resource-server flow: userId={}, issuer={}", userId, issuerHost);
|
||||||
|
return Optional.of(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstNonBlank(String... values) {
|
||||||
|
for (String v : values) {
|
||||||
|
if (v != null && !v.isBlank()) return v;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,8 @@ public class SecurityConfig {
|
|||||||
SecurityProperties securityProperties,
|
SecurityProperties securityProperties,
|
||||||
CorsConfigurationSource corsConfigurationSource,
|
CorsConfigurationSource corsConfigurationSource,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
ServerMetrics serverMetrics) throws Exception {
|
ServerMetrics serverMetrics,
|
||||||
|
OidcAccountSyncService oidcAccountSync) throws Exception {
|
||||||
JwtDecoder oidcDecoder = null;
|
JwtDecoder oidcDecoder = null;
|
||||||
String issuer = securityProperties.getOidc().getIssuerUri();
|
String issuer = securityProperties.getOidc().getIssuerUri();
|
||||||
if (issuer != null && !issuer.isBlank()) {
|
if (issuer != null && !issuer.isBlank()) {
|
||||||
@@ -198,7 +199,7 @@ public class SecurityConfig {
|
|||||||
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
||||||
)
|
)
|
||||||
.addFilterBefore(
|
.addFilterBefore(
|
||||||
new JwtAuthenticationFilter(jwtService, registryService, oidcDecoder, userRepository, serverMetrics),
|
new JwtAuthenticationFilter(jwtService, registryService, oidcDecoder, userRepository, serverMetrics, oidcAccountSync),
|
||||||
UsernamePasswordAuthenticationFilter.class
|
UsernamePasswordAuthenticationFilter.class
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package io.cameleer.server.app.security;
|
||||||
|
|
||||||
|
import io.cameleer.server.app.license.LicenseCapExceededException;
|
||||||
|
import io.cameleer.server.app.license.LicenseEnforcer;
|
||||||
|
import io.cameleer.server.core.security.OidcConfig;
|
||||||
|
import io.cameleer.server.core.security.OidcConfigRepository;
|
||||||
|
import io.cameleer.server.core.security.UserInfo;
|
||||||
|
import io.cameleer.server.core.security.UserRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class OidcAccountSyncServiceTest {
|
||||||
|
|
||||||
|
private UserRepository userRepository;
|
||||||
|
private OidcConfigRepository oidcConfigRepository;
|
||||||
|
private LicenseEnforcer licenseEnforcer;
|
||||||
|
private OidcAccountSyncService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
userRepository = mock(UserRepository.class);
|
||||||
|
oidcConfigRepository = mock(OidcConfigRepository.class);
|
||||||
|
licenseEnforcer = mock(LicenseEnforcer.class);
|
||||||
|
service = new OidcAccountSyncService(userRepository, oidcConfigRepository, licenseEnforcer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void existingUser_returnsExistingId_skipsUpsert() {
|
||||||
|
when(userRepository.findById("oidc:abc")).thenReturn(Optional.of(existingUser("oidc:abc")));
|
||||||
|
|
||||||
|
Optional<String> result = service.ensureProvisioned(jwt("abc", "https://logto.example", "x@y.com", "Display"));
|
||||||
|
|
||||||
|
assertThat(result).contains("oidc:abc");
|
||||||
|
verify(userRepository, never()).upsert(any());
|
||||||
|
verify(licenseEnforcer, never()).assertWithinCap(any(), anyLong(), anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void newUser_autoSignupTrue_upsertsBareUserId() {
|
||||||
|
when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
|
||||||
|
when(oidcConfigRepository.find()).thenReturn(Optional.of(oidcConfig(true)));
|
||||||
|
when(userRepository.count()).thenReturn(0L);
|
||||||
|
|
||||||
|
Optional<String> result = service.ensureProvisioned(
|
||||||
|
jwt("abc", "https://logto.example", "x@y.com", "Display"));
|
||||||
|
|
||||||
|
assertThat(result).contains("oidc:abc");
|
||||||
|
verify(licenseEnforcer).assertWithinCap("max_users", 0L, 1L);
|
||||||
|
verify(userRepository).upsert(argThat(u ->
|
||||||
|
"oidc:abc".equals(u.userId())
|
||||||
|
&& "oidc:logto.example".equals(u.provider())
|
||||||
|
&& "x@y.com".equals(u.email())
|
||||||
|
&& "Display".equals(u.displayName())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void newUser_oidcConfigAbsent_defaultsToAutoSignupTrue() {
|
||||||
|
when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
|
||||||
|
when(oidcConfigRepository.find()).thenReturn(Optional.empty());
|
||||||
|
when(userRepository.count()).thenReturn(0L);
|
||||||
|
|
||||||
|
Optional<String> result = service.ensureProvisioned(
|
||||||
|
jwt("abc", "https://logto.example", null, null));
|
||||||
|
|
||||||
|
assertThat(result).contains("oidc:abc");
|
||||||
|
verify(userRepository).upsert(argThat(u ->
|
||||||
|
"oidc:abc".equals(u.userId()) && u.email().isEmpty() && u.displayName().isEmpty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void newUser_autoSignupFalse_returnsEmpty_noUpsert() {
|
||||||
|
when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
|
||||||
|
when(oidcConfigRepository.find()).thenReturn(Optional.of(oidcConfig(false)));
|
||||||
|
|
||||||
|
Optional<String> result = service.ensureProvisioned(
|
||||||
|
jwt("abc", "https://logto.example", "x@y.com", "Display"));
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
verify(userRepository, never()).upsert(any());
|
||||||
|
verify(licenseEnforcer, never()).assertWithinCap(any(), anyLong(), anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void newUser_capExceeded_returnsEmpty_noUpsert() {
|
||||||
|
when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
|
||||||
|
when(oidcConfigRepository.find()).thenReturn(Optional.of(oidcConfig(true)));
|
||||||
|
when(userRepository.count()).thenReturn(10L);
|
||||||
|
doThrow(new LicenseCapExceededException("max_users", 10, 5))
|
||||||
|
.when(licenseEnforcer).assertWithinCap(eq("max_users"), eq(10L), eq(1L));
|
||||||
|
|
||||||
|
Optional<String> result = service.ensureProvisioned(
|
||||||
|
jwt("abc", "https://logto.example", "x@y.com", "Display"));
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
verify(userRepository, never()).upsert(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void newUser_displayNameFallback_prefersPreferredUsernameOverEmail() {
|
||||||
|
when(userRepository.findById("oidc:abc")).thenReturn(Optional.empty());
|
||||||
|
when(oidcConfigRepository.find()).thenReturn(Optional.of(oidcConfig(true)));
|
||||||
|
when(userRepository.count()).thenReturn(0L);
|
||||||
|
|
||||||
|
Jwt jwt = Jwt.withTokenValue("t")
|
||||||
|
.header("alg", "ES384")
|
||||||
|
.subject("abc")
|
||||||
|
.issuer("https://logto.example")
|
||||||
|
.claim("preferred_username", "alice42")
|
||||||
|
.claim("email", "alice@example.com")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
service.ensureProvisioned(jwt);
|
||||||
|
|
||||||
|
verify(userRepository).upsert(argThat(u ->
|
||||||
|
"alice42".equals(u.displayName()) && "alice@example.com".equals(u.email())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OidcConfig oidcConfig(boolean autoSignup) {
|
||||||
|
return new OidcConfig(
|
||||||
|
true, "https://logto.example", "client", "secret",
|
||||||
|
"roles", List.of("VIEWER"), autoSignup,
|
||||||
|
"name", "sub", null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserInfo existingUser(String userId) {
|
||||||
|
return new UserInfo(userId, "oidc:logto.example", "", "", Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Jwt jwt(String sub, String issuer, String email, String name) {
|
||||||
|
Jwt.Builder b = Jwt.withTokenValue("t")
|
||||||
|
.header("alg", "ES384")
|
||||||
|
.subject(sub)
|
||||||
|
.issuer(issuer);
|
||||||
|
if (email != null) b.claim("email", email);
|
||||||
|
if (name != null) b.claim("name", name);
|
||||||
|
return b.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user