From 7657081b782201ac609add5843be9027fe2aa08f Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:15:47 +0200 Subject: [PATCH] feat: disable local auth when OIDC is configured (resource server mode) - UiAuthController.login returns 404 when OIDC issuer is configured - JwtAuthenticationFilter skips internal user tokens in OIDC mode (agents still work) - UserAdminController.createUser and resetPassword return 400 in OIDC mode Co-Authored-By: Claude Opus 4.6 (1M context) --- .../server/app/controller/UserAdminController.java | 14 +++++++++++++- .../app/security/JwtAuthenticationFilter.java | 6 ++++++ .../server/app/security/UiAuthController.java | 9 +++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java index 6c0a4859..0423521d 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.cameleer3.server.app.security.SecurityProperties; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.time.Instant; @@ -47,12 +48,15 @@ public class UserAdminController { private final RbacService rbacService; private final UserRepository userRepository; private final AuditService auditService; + private final boolean oidcEnabled; public UserAdminController(RbacService rbacService, UserRepository userRepository, - AuditService auditService) { + AuditService auditService, SecurityProperties securityProperties) { this.rbacService = rbacService; this.userRepository = userRepository; this.auditService = auditService; + String issuer = securityProperties.getOidcIssuerUri(); + this.oidcEnabled = issuer != null && !issuer.isBlank(); } @GetMapping @@ -78,8 +82,12 @@ public class UserAdminController { @PostMapping @Operation(summary = "Create a local user") @ApiResponse(responseCode = "200", description = "User created") + @ApiResponse(responseCode = "400", description = "Disabled in OIDC mode") public ResponseEntity createUser(@RequestBody CreateUserRequest request, HttpServletRequest httpRequest) { + if (oidcEnabled) { + return ResponseEntity.badRequest().build(); + } String userId = "user:" + request.username(); UserInfo user = new UserInfo(userId, "local", request.email() != null ? request.email() : "", @@ -178,10 +186,14 @@ public class UserAdminController { @PostMapping("/{userId}/password") @Operation(summary = "Reset user password") @ApiResponse(responseCode = "204", description = "Password reset") + @ApiResponse(responseCode = "400", description = "Disabled in OIDC mode") public ResponseEntity resetPassword( @PathVariable String userId, @Valid @RequestBody SetPasswordRequest request, HttpServletRequest httpRequest) { + if (oidcEnabled) { + return ResponseEntity.badRequest().build(); + } userRepository.setPassword(userId, passwordEncoder.encode(request.password())); auditService.log("reset_password", AuditCategory.USER_MGMT, userId, null, AuditResult.SUCCESS, httpRequest); return ResponseEntity.noContent().build(); diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java index 66d4e231..ad0f74e6 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java @@ -74,6 +74,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { JwtValidationResult result = jwtService.validateAccessToken(token); String subject = result.subject(); + // In OIDC mode, only accept agent tokens via internal validation. + // User tokens must go through the OIDC decoder path. + if (oidcDecoder != null && subject != null && subject.startsWith("user:")) { + return false; + } + List roles = result.roles(); if (!subject.startsWith("user:") && roles.isEmpty()) { roles = List.of("AGENT"); diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java index 6049514b..8938e062 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java @@ -71,6 +71,10 @@ public class UiAuthController { content = @Content(schema = @Schema(implementation = ErrorResponse.class))) public ResponseEntity login(@RequestBody LoginRequest request, HttpServletRequest httpRequest) { + if (isOidcEnabled()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new AuthTokenResponse(null, null, "Local login disabled when OIDC is configured", null)); + } String configuredUser = properties.getUiUser(); String configuredPassword = properties.getUiPassword(); String subject = "user:" + request.username(); @@ -149,6 +153,11 @@ public class UiAuthController { } } + private boolean isOidcEnabled() { + String issuer = properties.getOidcIssuerUri(); + return issuer != null && !issuer.isBlank(); + } + public record LoginRequest(String username, String password) {} public record RefreshRequest(String refreshToken) {} }