Fix OIDC login immediate logout — rename JWT subject prefix ui: → user:
OIDC tokens had subject "oidc:<sub>" which didn't match the "ui:" prefix check in JwtAuthenticationFilter, causing every post-login API call to return 401 and trigger automatic logout. Renamed the prefix from "ui:" to "user:" across all auth code for clarity (it covers both browser and API clients, not just UI). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
JwtValidationResult result = jwtService.validateAccessToken(token);
|
JwtValidationResult result = jwtService.validateAccessToken(token);
|
||||||
String subject = result.subject();
|
String subject = result.subject();
|
||||||
|
|
||||||
if (subject.startsWith("ui:")) {
|
if (subject.startsWith("user:")) {
|
||||||
// UI user token — authenticate with roles from JWT
|
// UI user token — authenticate with roles from JWT
|
||||||
List<GrantedAuthority> authorities = toAuthorities(result.roles());
|
List<GrantedAuthority> authorities = toAuthorities(result.roles());
|
||||||
UsernamePasswordAuthenticationToken auth =
|
UsernamePasswordAuthenticationToken auth =
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ public class OidcAuthController {
|
|||||||
OidcTokenExchanger.OidcUserInfo oidcUser =
|
OidcTokenExchanger.OidcUserInfo oidcUser =
|
||||||
tokenExchanger.exchange(request.code(), request.redirectUri());
|
tokenExchanger.exchange(request.code(), request.redirectUri());
|
||||||
|
|
||||||
String userId = "oidc:" + oidcUser.subject();
|
String userId = "user:oidc:" + oidcUser.subject();
|
||||||
String issuerHost = URI.create(config.get().issuerUri()).getHost();
|
String issuerHost = URI.create(config.get().issuerUri()).getHost();
|
||||||
String provider = "oidc:" + issuerHost;
|
String provider = "oidc:" + issuerHost;
|
||||||
|
|
||||||
@@ -127,8 +127,8 @@ public class OidcAuthController {
|
|||||||
userRepository.upsert(new UserInfo(
|
userRepository.upsert(new UserInfo(
|
||||||
userId, provider, oidcUser.email(), oidcUser.name(), roles, Instant.now()));
|
userId, provider, oidcUser.email(), oidcUser.name(), roles, Instant.now()));
|
||||||
|
|
||||||
String accessToken = jwtService.createAccessToken(userId, "ui", roles);
|
String accessToken = jwtService.createAccessToken(userId, "user", roles);
|
||||||
String refreshToken = jwtService.createRefreshToken(userId, "ui", roles);
|
String refreshToken = jwtService.createRefreshToken(userId, "user", roles);
|
||||||
|
|
||||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
||||||
} catch (ResponseStatusException e) {
|
} catch (ResponseStatusException e) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.util.List;
|
|||||||
* Authentication endpoints for the UI (local credentials).
|
* Authentication endpoints for the UI (local credentials).
|
||||||
* <p>
|
* <p>
|
||||||
* Validates credentials against environment-configured username/password,
|
* Validates credentials against environment-configured username/password,
|
||||||
* then issues JWTs with {@code ui:} prefixed subjects and ADMIN roles.
|
* then issues JWTs with {@code user:} prefixed subjects and ADMIN roles.
|
||||||
* Upserts the user into the user store on login.
|
* Upserts the user into the user store on login.
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@@ -70,7 +70,7 @@ public class UiAuthController {
|
|||||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
String subject = "ui:" + request.username();
|
String subject = "user:" + request.username();
|
||||||
List<String> roles = List.of("ADMIN");
|
List<String> roles = List.of("ADMIN");
|
||||||
|
|
||||||
// Upsert local user into store
|
// Upsert local user into store
|
||||||
@@ -81,8 +81,8 @@ public class UiAuthController {
|
|||||||
log.warn("Failed to upsert local user to store (login continues): {}", e.getMessage());
|
log.warn("Failed to upsert local user to store (login continues): {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
String accessToken = jwtService.createAccessToken(subject, "ui", roles);
|
String accessToken = jwtService.createAccessToken(subject, "user", roles);
|
||||||
String refreshToken = jwtService.createRefreshToken(subject, "ui", roles);
|
String refreshToken = jwtService.createRefreshToken(subject, "user", roles);
|
||||||
|
|
||||||
log.info("UI user logged in: {}", request.username());
|
log.info("UI user logged in: {}", request.username());
|
||||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
||||||
@@ -96,14 +96,14 @@ public class UiAuthController {
|
|||||||
public ResponseEntity<AuthTokenResponse> refresh(@RequestBody RefreshRequest request) {
|
public ResponseEntity<AuthTokenResponse> refresh(@RequestBody RefreshRequest request) {
|
||||||
try {
|
try {
|
||||||
JwtValidationResult result = jwtService.validateRefreshToken(request.refreshToken());
|
JwtValidationResult result = jwtService.validateRefreshToken(request.refreshToken());
|
||||||
if (!result.subject().startsWith("ui:")) {
|
if (!result.subject().startsWith("user:")) {
|
||||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Not a UI token");
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Not a UI token");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve roles from the refresh token
|
// Preserve roles from the refresh token
|
||||||
List<String> roles = result.roles();
|
List<String> roles = result.roles();
|
||||||
String accessToken = jwtService.createAccessToken(result.subject(), "ui", roles);
|
String accessToken = jwtService.createAccessToken(result.subject(), "user", roles);
|
||||||
String refreshToken = jwtService.createRefreshToken(result.subject(), "ui", roles);
|
String refreshToken = jwtService.createRefreshToken(result.subject(), "user", roles);
|
||||||
|
|
||||||
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken));
|
||||||
} catch (ResponseStatusException e) {
|
} catch (ResponseStatusException e) {
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ class JwtServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void accessToken_rolesRoundTrip() {
|
void accessToken_rolesRoundTrip() {
|
||||||
List<String> roles = List.of("ADMIN", "OPERATOR");
|
List<String> roles = List.of("ADMIN", "OPERATOR");
|
||||||
String token = jwtService.createAccessToken("ui:admin", "ui", roles);
|
String token = jwtService.createAccessToken("user:admin", "user", roles);
|
||||||
JwtService.JwtValidationResult result = jwtService.validateAccessToken(token);
|
JwtService.JwtValidationResult result = jwtService.validateAccessToken(token);
|
||||||
assertEquals("ui:admin", result.subject());
|
assertEquals("user:admin", result.subject());
|
||||||
assertEquals("ui", result.group());
|
assertEquals("user", result.group());
|
||||||
assertEquals(roles, result.roles());
|
assertEquals(roles, result.roles());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public interface JwtService {
|
|||||||
/**
|
/**
|
||||||
* Validated JWT payload.
|
* Validated JWT payload.
|
||||||
*
|
*
|
||||||
* @param subject the {@code sub} claim (agent ID or {@code ui:<username>})
|
* @param subject the {@code sub} claim (agent ID or {@code user:<username>})
|
||||||
* @param group the {@code group} claim
|
* @param group the {@code group} claim
|
||||||
* @param roles the {@code roles} claim (e.g. {@code ["AGENT"]}, {@code ["ADMIN"]})
|
* @param roles the {@code roles} claim (e.g. {@code ["AGENT"]}, {@code ["ADMIN"]})
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* Represents a persisted user in the system.
|
* Represents a persisted user in the system.
|
||||||
*
|
*
|
||||||
* @param userId unique identifier (e.g. OIDC {@code sub} or {@code ui:<username>})
|
* @param userId unique identifier (e.g. OIDC {@code sub} or {@code user:<username>})
|
||||||
* @param provider authentication provider ({@code "local"}, {@code "oidc:<issuer-host>"})
|
* @param provider authentication provider ({@code "local"}, {@code "oidc:<issuer-host>"})
|
||||||
* @param email user email (may be empty)
|
* @param email user email (may be empty)
|
||||||
* @param displayName display name (may be empty)
|
* @param displayName display name (may be empty)
|
||||||
|
|||||||
Reference in New Issue
Block a user