diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/dto/AuthCapabilitiesResponse.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/dto/AuthCapabilitiesResponse.java
index ef2c4b0a..7466af68 100644
--- a/cameleer-server-app/src/main/java/com/cameleer/server/app/dto/AuthCapabilitiesResponse.java
+++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/dto/AuthCapabilitiesResponse.java
@@ -1,7 +1,6 @@
package com.cameleer.server.app.dto;
import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotNull;
@Schema(description = "Authentication capabilities reported to the SPA so it can render the login page deterministically")
public record AuthCapabilitiesResponse(
@@ -12,7 +11,7 @@ public record AuthCapabilitiesResponse(
@Schema(description = "OIDC interactive login")
public record Oidc(
@Schema(description = "Whether OIDC is configured AND enabled") boolean enabled,
- @Schema(description = "Best-effort display label, e.g. \"Logto\", \"Keycloak\", \"Single Sign-On\"") @NotNull String providerName,
+ @Schema(description = "Best-effort display label, e.g. \"Logto\", \"Keycloak\", \"Single Sign-On\"") String providerName,
@Schema(description = "When true, OIDC is the canonical entry point and the SPA hides the local form unless ?local is set") boolean primary
) {}
diff --git a/ui/src/auth/LoginPage.tsx b/ui/src/auth/LoginPage.tsx
index 881b17aa..515b3de8 100644
--- a/ui/src/auth/LoginPage.tsx
+++ b/ui/src/auth/LoginPage.tsx
@@ -52,14 +52,14 @@ export function LoginPage() {
if (isAuthenticated) return ;
if (capsLoading) return null;
- const oidcEnabled = caps?.oidc?.enabled === true;
+ const oidcPrimary = caps?.oidc?.primary === true;
const adminRecoveryOnly = caps?.localAccounts?.adminRecoveryOnly === true;
const providerName = caps?.oidc?.providerName || 'Single Sign-On';
// Render decisions
- const showSsoPrimary = oidcEnabled && adminRecoveryOnly && !forceLocal;
- const showLocalForm = !oidcEnabled || forceLocal || !adminRecoveryOnly || capsFailed;
- const showAdminRecoveryBanner = oidcEnabled && adminRecoveryOnly && forceLocal;
+ const showSsoPrimary = oidcPrimary && adminRecoveryOnly && !forceLocal;
+ const showLocalForm = !oidcPrimary || forceLocal || !adminRecoveryOnly || capsFailed;
+ const showAdminRecoveryBanner = oidcPrimary && adminRecoveryOnly && forceLocal;
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
diff --git a/ui/src/test/e2e/fixtures.ts b/ui/src/test/e2e/fixtures.ts
index 2a02ead3..f27b9dad 100644
--- a/ui/src/test/e2e/fixtures.ts
+++ b/ui/src/test/e2e/fixtures.ts
@@ -19,8 +19,9 @@ type Fixtures = {
export const test = base.extend({
loggedIn: [
async ({ page }, use) => {
- // `?local` keeps the login page's auto-OIDC-redirect from firing so the
- // form-based login works even when an OIDC config happens to be present.
+ // Navigate to ?local to bypass the SSO-primary page and reach the local
+ // form directly, so the fixture works regardless of whether OIDC is
+ // configured on the test server.
await page.goto('/login?local');
await page.getByLabel(/username/i).fill(ADMIN_USER);
await page.getByLabel(/password/i).fill(ADMIN_PASS);