diff --git a/apps/editor/src/app/providers/AuthProvider.tsx b/apps/editor/src/app/providers/AuthProvider.tsx index a82ef6a..0ec4abe 100644 --- a/apps/editor/src/app/providers/AuthProvider.tsx +++ b/apps/editor/src/app/providers/AuthProvider.tsx @@ -16,8 +16,11 @@ type AuthContextValue = { completeSignIn: (_callbackUrl?: string) => Promise signOut: () => Promise getAccessToken: () => Promise - /** Redirect to Keycloak's password change form via kc_action=UPDATE_PASSWORD */ - changePassword: () => Promise + /** + * Redirect to Keycloak's password change form via kc_action=UPDATE_PASSWORD. + * Null for federated users (external IdP) who do not have a Keycloak-managed password. + */ + changePassword: (() => Promise) | null } const STORAGE_KEY_TENANT = 'case-editor:auth:tenantId' @@ -36,6 +39,12 @@ function writeTenantId(tenantId: string) { } } +function isFederatedUser(user: User | null): boolean { + const profile = user?.profile as Record | undefined + const idp = profile?.identity_provider + return typeof idp === 'string' && idp.trim().length > 0 +} + function pickUserName(user: User | null): string | null { const p = user?.profile as Record | undefined if (!p) return null @@ -207,12 +216,14 @@ export function AuthProvider({ children }: Readonly<{ children: ReactNode }>) { return u.access_token }, [userManager]) - const changePassword = useCallback(async () => { + const changePasswordFn = useCallback(async () => { await userManager.signinRedirect({ extraQueryParams: { kc_action: 'UPDATE_PASSWORD' }, }) }, [userManager]) + const changePassword = isFederatedUser(user) ? null : changePasswordFn + const value: AuthContextValue = useMemo( () => ({ status, diff --git a/apps/editor/src/ui/editor/EditorCanvas.tsx b/apps/editor/src/ui/editor/EditorCanvas.tsx index c1920fd..fdec7e3 100644 --- a/apps/editor/src/ui/editor/EditorCanvas.tsx +++ b/apps/editor/src/ui/editor/EditorCanvas.tsx @@ -1124,7 +1124,7 @@ export default function EditorCanvas({ onBack, onSaveToServer, isPublishedToOpen frameworkSubtitle={frameworkInfo.subtitle} userName={userName ?? undefined} tenantId={tenantId ?? undefined} - onChangePassword={authStatus === 'authenticated' ? () => void changePassword() : undefined} + onChangePassword={authStatus === 'authenticated' && changePassword ? () => void changePassword() : undefined} reserveRightForPanel={Boolean(selectedNode || selectedEdge || (selectedNodeIds.length + selectedEdgeIds.length > 1))} showSettings isDirty={isDirty} diff --git a/apps/editor/src/ui/home/HomeScreen.tsx b/apps/editor/src/ui/home/HomeScreen.tsx index a9765ad..31c749f 100644 --- a/apps/editor/src/ui/home/HomeScreen.tsx +++ b/apps/editor/src/ui/home/HomeScreen.tsx @@ -472,7 +472,7 @@ export default function HomeScreen({ {/* User button — top right */} - void signOut() : undefined} onChangePassword={isAuthenticated ? () => void changePassword() : undefined} onApiKeys={isAuthenticated ? () => setApiKeysOpen(true) : undefined} /> + void signOut() : undefined} onChangePassword={isAuthenticated && changePassword ? () => void changePassword() : undefined} onApiKeys={isAuthenticated ? () => setApiKeysOpen(true) : undefined} />
diff --git a/docs/AUTH0_SSO.md b/docs/AUTH0_SSO.md index c5b4ff5..f900ccd 100644 --- a/docs/AUTH0_SSO.md +++ b/docs/AUTH0_SSO.md @@ -156,7 +156,36 @@ Click **Save** after each mapper. --- -## Step 4 -- Configure First Login Flow (Optional) +## Step 4 -- Expose Identity Provider in the Token + +By default Keycloak does not include which identity provider a user authenticated through in the tokens it issues. OpenCASE uses this information to hide the **Change Password** option for federated users — users whose password is managed by Auth0 rather than Keycloak cannot change it through Keycloak, so the menu item is suppressed when this claim is present. + +Add a **User Session Note** mapper to the OIDC client (or to a shared client scope so it applies to all tenants automatically): + +1. In the Keycloak Admin Console, go to **Clients** → `tenant-{id}` → **Client scopes** tab +2. Click the client's dedicated scope (e.g. `tenant-{id}-dedicated`) +3. Go to the **Mappers** tab → **Add mapper** → **By configuration** → **User Session Note** +4. Fill in the following fields: + +| Field | Value | +|---|---| +| **Name** | `identity-provider` | +| **Session note** | `identity_provider` | +| **Token claim name** | `identity_provider` | +| **Claim JSON type** | `String` | +| **Add to ID token** | On | +| **Add to access token** | Off | +| **Add to userinfo** | Off | + +5. Click **Save** + +> **Shared scope alternative:** If you add this mapper to the built-in `profile` client scope instead, it applies to every tenant client without repeating the step for each one. Navigate to **Client scopes** (realm-level, not client-level) → `profile` → **Mappers** → **Add mapper**. + +After this change, federated users will have `"identity_provider": "auth0"` (or the alias of whatever IdP they used) in their ID token. OpenCASE reads this claim to determine whether to offer the Change Password menu item — native Keycloak users have no such claim and will continue to see the option as before. + +--- + +## Step 5 -- Configure First Login Flow (Optional) When a user logs in via Auth0 for the first time, Keycloak's **First Broker Login** flow determines what happens. The default flow: @@ -172,7 +201,7 @@ This works well for most setups. If you want to **skip the review page** and cre --- -## Step 5 -- Test the Integration +## Step 6 -- Test the Integration 1. Open your OpenCASE Editor: `https://YOUR_DOMAIN` 2. Click **Sign in** @@ -185,7 +214,7 @@ This works well for most setups. If you want to **skip the review page** and cre --- -## Step 6 -- Assign Roles to SSO Users +## Step 7 -- Assign Roles to SSO Users Users who log in via Auth0 are created in Keycloak with no roles by default. To give them access to OpenCASE features, you need to assign roles: