From 52e9b16643dd699988a63c3ac54e3c64a3669109 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Tue, 2 Sep 2025 16:55:08 -0500 Subject: [PATCH] Fix OIDC errors for "Failed to get user information" --- src/backend/database/routes/users.ts | 53 +++++++++++++++++++++++++--- src/ui/Admin/AdminSettings.tsx | 32 +++++++++++------ src/ui/Navigation/LeftSidebar.tsx | 8 +++-- src/ui/main-axios.ts | 11 +++--- 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 3e776b7f..3456aee3 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -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'}); } diff --git a/src/ui/Admin/AdminSettings.tsx b/src/ui/Admin/AdminSettings.tsx index bece9c0a..ee375929 100644 --- a/src/ui/Admin/AdminSettings.tsx +++ b/src/ui/Admin/AdminSettings.tsx @@ -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(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.
handleOIDCConfigChange('scopes', (e.target as HTMLInputElement).value)} + onChange={(e) => handleOIDCConfigChange('scopes', e.target.value)} placeholder="openid email profile" required/>
+
+ + handleOIDCConfigChange('userinfo_url', e.target.value)} + placeholder="https://your-provider.com/application/o/userinfo/"/> +
@@ -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
@@ -357,7 +367,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React. className="px-4">{user.is_oidc ? "External" : "Local"}