This commit is contained in:
2026-02-18 22:50:37 +01:00
parent f218552d48
commit b3db7cbd7b
2 changed files with 47 additions and 27 deletions

View File

@@ -103,35 +103,61 @@ export async function handleCallback(
return tokenSet;
}
// For native app flows the provider may return only an access token (no ID token)
// when the redirect_uri uses a custom scheme. client.grant() calls the token
// endpoint directly and does not attempt ID token validation.
export async function exchangeNativeCode(
code: string,
codeVerifier: string,
redirectUri: string,
): Promise<TokenSet> {
const client = getOIDCClient();
const tokenSet = await client.grant({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
});
return tokenSet;
}
export async function getUserInfo(tokenSet: TokenSet): Promise<AuthenticatedUser> {
const client = getOIDCClient();
const claims = tokenSet.claims();
// Try to get more detailed userinfo if available
// ID token claims (only available in web/full OIDC flow)
const idTokenClaims = tokenSet.id_token ? tokenSet.claims() : undefined;
// Always attempt userinfo; for native flows this is the sole source of claims.
let userInfo: Record<string, unknown> = {};
try {
userInfo = await client.userinfo(tokenSet);
} catch {
// Some providers don't support userinfo endpoint
// We'll use the claims from the ID token
if (!idTokenClaims) {
// No ID token and no userinfo — nothing to work with.
throw new Error('Unable to retrieve user info: userinfo endpoint failed and no ID token present');
}
// Web flow: fall back to ID token claims only
}
const id = String(claims.sub);
const username = String(userInfo.preferred_username || claims.preferred_username || claims.name || id);
const email = String(userInfo.email || claims.email || '');
const fullName = String(userInfo.name || claims.name || '') || null;
const sub = String(userInfo.sub || idTokenClaims?.sub);
const id = sub;
const username = String(
userInfo.preferred_username ||
idTokenClaims?.preferred_username ||
userInfo.name ||
idTokenClaims?.name ||
id
);
const email = String(userInfo.email || idTokenClaims?.email || '');
const fullName = String(userInfo.name || idTokenClaims?.name || '') || null;
if (!email) {
throw new Error('Email not provided by OIDC provider');
}
return {
id,
username,
fullName,
email,
};
return { id, username, fullName, email };
}
export async function verifyToken(tokenSet: TokenSet): Promise<boolean> {

View File

@@ -4,6 +4,7 @@ import {
createAuthSession,
getAuthorizationUrl,
handleCallback,
exchangeNativeCode,
getUserInfo,
} from "../auth/oidc";
import { requireAuth, syncUser } from "../middleware/auth";
@@ -138,14 +139,7 @@ router.post("/token", async (req, res) => {
return;
}
const session = {
codeVerifier: oidcSession.codeVerifier,
state: oidcSession.state,
nonce: oidcSession.nonce,
redirectUri: redirect_uri,
};
const tokenSet = await handleCallback({ code, state }, session);
const tokenSet = await exchangeNativeCode(code, oidcSession.codeVerifier, redirect_uri);
const user = await getUserInfo(tokenSet);
await syncUser(user);