Fix OIDC errors for "Failed to get user information"

This commit is contained in:
LukeGus
2025-09-02 16:55:08 -05:00
parent ea792bf95f
commit 52e9b16643
4 changed files with 81 additions and 23 deletions

View File

@@ -222,6 +222,7 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
issuer_url,
authorization_url,
token_url,
userinfo_url,
identifier_path,
name_path,
scopes
@@ -240,6 +241,7 @@ router.post('/oidc-config', authenticateJWT, async (req, res) => {
issuer_url,
authorization_url,
token_url,
userinfo_url: userinfo_url || '',
identifier_path,
name_path,
scopes: scopes || 'openid email profile'
@@ -362,14 +364,38 @@ router.get('/oidc/callback', async (req, res) => {
const tokenData = await tokenResponse.json() as any;
let userInfo: any = null;
const userInfoUrls = [];
let userInfoUrls: string[] = [];
const normalizedIssuerUrl = config.issuer_url.endsWith('/') ? config.issuer_url.slice(0, -1) : config.issuer_url;
const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, '');
userInfoUrls.push(`${baseUrl}/userinfo/`);
userInfoUrls.push(`${normalizedIssuerUrl}/userinfo/`);
userInfoUrls.push(`${normalizedIssuerUrl}/userinfo`);
try {
const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
const discoveryResponse = await fetch(discoveryUrl);
if (discoveryResponse.ok) {
const discovery = await discoveryResponse.json() as any;
if (discovery.userinfo_endpoint) {
userInfoUrls.push(discovery.userinfo_endpoint);
}
}
} catch (discoveryError) {
logger.error(`OIDC discovery failed: ${discoveryError}`);
}
if (config.userinfo_url) {
userInfoUrls.unshift(config.userinfo_url);
}
userInfoUrls.push(
`${baseUrl}/userinfo/`,
`${baseUrl}/userinfo`,
`${normalizedIssuerUrl}/userinfo/`,
`${normalizedIssuerUrl}/userinfo`,
`${baseUrl}/oauth2/userinfo/`,
`${baseUrl}/oauth2/userinfo`,
`${normalizedIssuerUrl}/oauth2/userinfo/`,
`${normalizedIssuerUrl}/oauth2/userinfo`
);
if (tokenData.id_token) {
try {
@@ -391,15 +417,34 @@ router.get('/oidc/callback', async (req, res) => {
if (userInfoResponse.ok) {
userInfo = await userInfoResponse.json();
break;
} else {
logger.error(`Userinfo endpoint ${userInfoUrl} failed with status: ${userInfoResponse.status}`);
}
} catch (error) {
logger.error(`Userinfo endpoint ${userInfoUrl} failed:`, error);
continue;
}
}
}
if (!userInfo && tokenData.id_token) {
try {
const parts = tokenData.id_token.split('.');
if (parts.length === 3) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
userInfo = payload;
}
} catch (error) {
logger.error('Failed to decode ID token payload:', error);
}
}
if (!userInfo) {
logger.error('Failed to get user information from all sources');
logger.error(`Tried userinfo URLs: ${userInfoUrls.join(', ')}`);
logger.error(`Token data keys: ${Object.keys(tokenData).join(', ')}`);
logger.error(`Has id_token: ${!!tokenData.id_token}`);
logger.error(`Has access_token: ${!!tokenData.access_token}`);
return res.status(400).json({error: 'Failed to get user information'});
}

View File

@@ -52,7 +52,8 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
token_url: '',
identifier_path: 'sub',
name_path: 'name',
scopes: 'openid email profile'
scopes: 'openid email profile',
userinfo_url: ''
});
const [oidcLoading, setOidcLoading] = React.useState(false);
const [oidcError, setOidcError] = React.useState<string | null>(null);
@@ -145,7 +146,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
setOidcConfig(prev => ({...prev, [field]: value}));
};
const makeUserAdmin = async (e: React.FormEvent) => {
const handleMakeUserAdmin = async (e: React.FormEvent) => {
e.preventDefault();
if (!newAdminUsername.trim()) return;
setMakeAdminLoading(true);
@@ -164,23 +165,25 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
}
};
const removeAdminStatus = async (username: string) => {
const handleRemoveAdminStatus = async (username: string) => {
if (!confirm(`Remove admin status from ${username}?`)) return;
const jwt = getCookie("jwt");
try {
await removeAdminStatus(username);
fetchUsers();
} catch {
} catch (err: any) {
console.error('Failed to remove admin status:', err);
}
};
const deleteUser = async (username: string) => {
const handleDeleteUser = async (username: string) => {
if (!confirm(`Delete user ${username}? This cannot be undone.`)) return;
const jwt = getCookie("jwt");
try {
await deleteUser(username);
fetchUsers();
} catch {
} catch (err: any) {
console.error('Failed to delete user:', err);
}
};
@@ -296,9 +299,15 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
<div className="space-y-2">
<Label htmlFor="scopes">Scopes</Label>
<Input id="scopes" value={oidcConfig.scopes}
onChange={(e) => handleOIDCConfigChange('scopes', (e.target as HTMLInputElement).value)}
onChange={(e) => handleOIDCConfigChange('scopes', e.target.value)}
placeholder="openid email profile" required/>
</div>
<div className="space-y-2">
<Label htmlFor="userinfo_url">Overide User Info URL (not required)</Label>
<Input id="userinfo_url" value={oidcConfig.userinfo_url}
onChange={(e) => handleOIDCConfigChange('userinfo_url', e.target.value)}
placeholder="https://your-provider.com/application/o/userinfo/"/>
</div>
<div className="flex gap-2 pt-2">
<Button type="submit" className="flex-1"
disabled={oidcLoading}>{oidcLoading ? "Saving..." : "Save Configuration"}</Button>
@@ -310,7 +319,8 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
token_url: '',
identifier_path: 'sub',
name_path: 'name',
scopes: 'openid email profile'
scopes: 'openid email profile',
userinfo_url: ''
})}>Reset</Button>
</div>
@@ -357,7 +367,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
className="px-4">{user.is_oidc ? "External" : "Local"}</TableCell>
<TableCell className="px-4">
<Button variant="ghost" size="sm"
onClick={() => deleteUser(user.username)}
onClick={() => handleDeleteUser(user.username)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
disabled={user.is_admin}>
<Trash2 className="h-4 w-4"/>
@@ -377,7 +387,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
<h3 className="text-lg font-semibold">Admin Management</h3>
<div className="space-y-4 p-6 border rounded-md bg-muted/50">
<h4 className="font-medium">Make User Admin</h4>
<form onSubmit={makeUserAdmin} className="space-y-4">
<form onSubmit={handleMakeUserAdmin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="new-admin-username">Username</Label>
<div className="flex gap-2">
@@ -426,7 +436,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
className="px-4">{admin.is_oidc ? "External" : "Local"}</TableCell>
<TableCell className="px-4">
<Button variant="ghost" size="sm"
onClick={() => removeAdminStatus(admin.username)}
onClick={() => handleRemoveAdminStatus(admin.username)}
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">
<Shield className="h-4 w-4"/>
Remove Admin

View File

@@ -355,7 +355,7 @@ export function LeftSidebar({
}
};
const makeUserAdmin = async (e: React.FormEvent) => {
const handleMakeUserAdmin = async (e: React.FormEvent) => {
e.preventDefault();
if (!newAdminUsername.trim()) return;
@@ -380,7 +380,7 @@ export function LeftSidebar({
}
};
const removeAdminStatus = async (username: string) => {
const handleRemoveAdminStatus = async (username: string) => {
if (!confirm(`Are you sure you want to remove admin status from ${username}?`)) return;
if (!isAdmin) {
@@ -392,10 +392,11 @@ export function LeftSidebar({
await removeAdminStatus(username);
fetchUsers();
} catch (err: any) {
console.error('Failed to remove admin status:', err);
}
};
const deleteUser = async (username: string) => {
const handleDeleteUser = async (username: string) => {
if (!confirm(`Are you sure you want to delete user ${username}? This action cannot be undone.`)) return;
if (!isAdmin) {
@@ -407,6 +408,7 @@ export function LeftSidebar({
await deleteUser(username);
fetchUsers();
} catch (err: any) {
console.error('Failed to delete user:', err);
}
};

View File

@@ -143,6 +143,7 @@ interface UserInfo {
id: string;
username: string;
is_admin: boolean;
is_oidc: boolean;
}
interface UserCount {
@@ -897,7 +898,7 @@ export async function setupTOTP(): Promise<{ secret: string; qr_code: string }>
const response = await authApi.post('/users/totp/setup');
return response.data;
} catch (error) {
handleApiError(error as AxiosError);
handleApiError(error as AxiosError, 'setup TOTP');
throw error;
}
}
@@ -907,7 +908,7 @@ export async function enableTOTP(totp_code: string): Promise<{ message: string;
const response = await authApi.post('/users/totp/enable', { totp_code });
return response.data;
} catch (error) {
handleApiError(error as AxiosError);
handleApiError(error as AxiosError, 'enable TOTP');
throw error;
}
}
@@ -917,7 +918,7 @@ export async function disableTOTP(password?: string, totp_code?: string): Promis
const response = await authApi.post('/users/totp/disable', { password, totp_code });
return response.data;
} catch (error) {
handleApiError(error as AxiosError);
handleApiError(error as AxiosError, 'disable TOTP');
throw error;
}
}
@@ -927,7 +928,7 @@ export async function verifyTOTPLogin(temp_token: string, totp_code: string): Pr
const response = await authApi.post('/users/totp/verify-login', { temp_token, totp_code });
return response.data;
} catch (error) {
handleApiError(error as AxiosError);
handleApiError(error as AxiosError, 'verify TOTP login');
throw error;
}
}
@@ -937,7 +938,7 @@ export async function generateBackupCodes(password?: string, totp_code?: string)
const response = await authApi.post('/users/totp/backup-codes', { password, totp_code });
return response.data;
} catch (error) {
handleApiError(error as AxiosError);
handleApiError(error as AxiosError, 'generate backup codes');
throw error;
}
}