Add displayName to auth response and configurable display name claim for OIDC
- Add displayName field to AuthTokenResponse so the UI shows human-readable names instead of internal JWT subjects (e.g. user:oidc:<hash>) - Add displayNameClaim to OIDC config (default: "name") allowing admins to configure which ID token claim contains the user's display name - Support dot-separated claim paths (e.g. profile.display_name) like rolesClaim - Add admin UI field for Display Name Claim on the OIDC config page - ClickHouse migration: ALTER TABLE adds display_name_claim column Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1393,6 +1393,9 @@
|
||||
},
|
||||
"autoSignup": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"displayNameClaim": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1438,6 +1441,9 @@
|
||||
},
|
||||
"autoSignup": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"displayNameClaim": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1593,11 +1599,15 @@
|
||||
},
|
||||
"refreshToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"accessToken",
|
||||
"refreshToken"
|
||||
"refreshToken",
|
||||
"displayName"
|
||||
]
|
||||
},
|
||||
"CallbackRequest": {
|
||||
|
||||
3
ui/src/api/schema.d.ts
vendored
3
ui/src/api/schema.d.ts
vendored
@@ -522,6 +522,7 @@ export interface components {
|
||||
rolesClaim?: string;
|
||||
defaultRoles?: string[];
|
||||
autoSignup?: boolean;
|
||||
displayNameClaim?: string;
|
||||
};
|
||||
/** @description Error response */
|
||||
ErrorResponse: {
|
||||
@@ -537,6 +538,7 @@ export interface components {
|
||||
rolesClaim?: string;
|
||||
defaultRoles?: string[];
|
||||
autoSignup?: boolean;
|
||||
displayNameClaim?: string;
|
||||
};
|
||||
SearchRequest: {
|
||||
status?: string;
|
||||
@@ -592,6 +594,7 @@ export interface components {
|
||||
AuthTokenResponse: {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
displayName: string;
|
||||
};
|
||||
CallbackRequest: {
|
||||
code?: string;
|
||||
|
||||
@@ -64,13 +64,14 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
if (error || !data) {
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
const { accessToken, refreshToken } = data;
|
||||
const { accessToken, refreshToken, displayName } = data;
|
||||
localStorage.removeItem('cameleer-oidc-end-session');
|
||||
persistTokens(accessToken, refreshToken, username);
|
||||
const name = displayName ?? username;
|
||||
persistTokens(accessToken, refreshToken, name);
|
||||
set({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
username,
|
||||
username: name,
|
||||
roles: parseRolesFromJwt(accessToken),
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
@@ -92,9 +93,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
if (error || !data) {
|
||||
throw new Error('OIDC login failed');
|
||||
}
|
||||
const { accessToken, refreshToken } = data;
|
||||
const payload = JSON.parse(atob(accessToken.split('.')[1]));
|
||||
const username = payload.sub ?? 'oidc-user';
|
||||
const { accessToken, refreshToken, displayName } = data;
|
||||
const username = displayName ?? 'oidc-user';
|
||||
persistTokens(accessToken, refreshToken, username);
|
||||
set({
|
||||
accessToken,
|
||||
@@ -120,11 +120,12 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
body: { refreshToken },
|
||||
});
|
||||
if (error || !data) return false;
|
||||
const username = get().username ?? '';
|
||||
const username = data.displayName ?? get().username ?? '';
|
||||
persistTokens(data.accessToken, data.refreshToken, username);
|
||||
set({
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
username,
|
||||
roles: parseRolesFromJwt(data.accessToken),
|
||||
isAuthenticated: true,
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ interface FormData {
|
||||
clientSecret: string;
|
||||
rolesClaim: string;
|
||||
defaultRoles: string[];
|
||||
displayNameClaim: string;
|
||||
}
|
||||
|
||||
const emptyForm: FormData = {
|
||||
@@ -27,6 +28,7 @@ const emptyForm: FormData = {
|
||||
clientSecret: '',
|
||||
rolesClaim: 'realm_access.roles',
|
||||
defaultRoles: ['VIEWER'],
|
||||
displayNameClaim: 'name',
|
||||
};
|
||||
|
||||
export function OidcAdminPage() {
|
||||
@@ -69,6 +71,7 @@ function OidcAdminForm() {
|
||||
clientSecret: '',
|
||||
rolesClaim: data.rolesClaim ?? 'realm_access.roles',
|
||||
defaultRoles: data.defaultRoles ?? ['VIEWER'],
|
||||
displayNameClaim: data.displayNameClaim ?? 'name',
|
||||
});
|
||||
setSecretTouched(false);
|
||||
} else {
|
||||
@@ -237,6 +240,20 @@ function OidcAdminForm() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Display Name Claim</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
type="text"
|
||||
value={form.displayNameClaim}
|
||||
onChange={(e) => updateField('displayNameClaim', e.target.value)}
|
||||
placeholder="name"
|
||||
/>
|
||||
<div className={styles.hint}>
|
||||
Dot-separated path to the user's display name in the ID token (e.g. name, preferred_username, profile.display_name)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Default Roles</label>
|
||||
<div className={styles.tags}>
|
||||
|
||||
Reference in New Issue
Block a user