From 5ad260a4651d01ceb7b4ada414188774ddfb984e Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sun, 28 Dec 2025 02:08:34 +0100 Subject: [PATCH] feat(auth, oidc): add user avatar URL to auth response and update redirect URLs to use frontend URL --- backend/src/routes/auth.ts | 1 + backend/src/routes/oidc.ts | 44 ++++++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 64c40d3..2164c37 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -215,6 +215,7 @@ export async function authRoutes(app: FastifyInstance) { user: { id: user.id, username: user.username, + avatarUrl: user.avatarUrl, }, }); }); diff --git a/backend/src/routes/oidc.ts b/backend/src/routes/oidc.ts index c1e804b..ca46ff1 100644 --- a/backend/src/routes/oidc.ts +++ b/backend/src/routes/oidc.ts @@ -42,6 +42,13 @@ function generateState(): string { return randomBytes(16).toString("hex"); } +// ============================================================================= +// Helpers +// ============================================================================= +function getFrontendUrl(): string { + return env.CORS_ORIGINS.split(",")[0] || "http://localhost:5173"; +} + // ============================================================================= // OIDC Routes // ============================================================================= @@ -103,7 +110,7 @@ export async function oidcRoutes(app: FastifyInstance) { return reply.redirect(authUrl.href); } catch (err: any) { console.error("[OIDC] Login error:", err); - return reply.redirect("/?error=oidc_init_failed"); + return reply.redirect(`${getFrontendUrl()}/?error=oidc_init_failed`); } }); @@ -118,25 +125,25 @@ export async function oidcRoutes(app: FastifyInstance) { // Handle OIDC provider errors if (error) { console.error(`[OIDC] Provider error: ${error} - ${error_description}`); - return reply.redirect(`/?error=oidc_${error}`); + return reply.redirect(`${getFrontendUrl()}/?error=oidc_${error}`); } if (!code || !state) { - return reply.redirect("/?error=oidc_missing_params"); + return reply.redirect(`${getFrontendUrl()}/?error=oidc_missing_params`); } // Verify state const storedState = request.unsignCookie(request.cookies.oidc_state || ""); if (!storedState.valid || storedState.value !== state) { console.error("[OIDC] State mismatch"); - return reply.redirect("/?error=oidc_state_mismatch"); + return reply.redirect(`${getFrontendUrl()}/?error=oidc_state_mismatch`); } // Get code verifier const storedVerifier = request.unsignCookie(request.cookies.oidc_code_verifier || ""); if (!storedVerifier.valid || !storedVerifier.value) { console.error("[OIDC] Missing code verifier"); - return reply.redirect("/?error=oidc_missing_verifier"); + return reply.redirect(`${getFrontendUrl()}/?error=oidc_missing_verifier`); } try { @@ -159,7 +166,7 @@ export async function oidcRoutes(app: FastifyInstance) { if (!username || !oidcSubject) { console.error("[OIDC] Missing required user info:", { username, oidcSubject }); - return reply.redirect("/?error=oidc_missing_user_info"); + return reply.redirect(`${getFrontendUrl()}/?error=oidc_missing_user_info`); } // Clean cookies @@ -170,7 +177,7 @@ export async function oidcRoutes(app: FastifyInstance) { let user = await findOrCreateOIDCUser(username, oidcSubject, reply); if (!user) { - return reply.redirect("/?error=oidc_user_creation_failed"); + return reply.redirect(`${getFrontendUrl()}/?error=oidc_user_creation_failed`); } // Update last login @@ -192,12 +199,14 @@ export async function oidcRoutes(app: FastifyInstance) { // Set cookies setAuthCookies(reply, accessToken, refreshToken); - // Redirect to dashboard - return reply.redirect("/dashboard"); + // Redirect to frontend dashboard + // In dev: CORS_ORIGINS contains the frontend URL + const frontendUrl = env.CORS_ORIGINS.split(",")[0] || "http://localhost:5173"; + return reply.redirect(`${frontendUrl}/dashboard`); } catch (err: any) { console.error("[OIDC] Callback error:", err); - return reply.redirect("/?error=oidc_callback_failed"); + return reply.redirect(`${getFrontendUrl()}/?error=oidc_callback_failed`); } } ); @@ -227,11 +236,18 @@ async function findOrCreateOIDCUser( .where(eq(users.username, username)); if (existingByUsername) { - // Username collision! Check if it's a local user - if (existingByUsername.authProvider === "local" && existingByUsername.passwordHash) { - // Local user exists with this username - add suffix + // Username collision! Check if it's a local user without OIDC linked + if (existingByUsername.authProvider === "local" && !existingByUsername.oidcSubject) { + // Local user exists without SSO - link this OIDC account to existing user + await db.update(users) + .set({ oidcSubject: oidcSubject }) + .where(eq(users.id, existingByUsername.id)); + console.log(`[OIDC] Linked OIDC to existing local user: ${username}`); + return { id: existingByUsername.id, username: existingByUsername.username }; + } else if (existingByUsername.oidcSubject && existingByUsername.oidcSubject !== oidcSubject) { + // User already has a DIFFERENT OIDC subject - create new user with suffix username = `${username}_sso`; - console.log(`[OIDC] Username collision, using: ${username}`); + console.log(`[OIDC] Username collision (different OIDC subject), using: ${username}`); } else if (existingByUsername.authProvider === "oidc" && !existingByUsername.oidcSubject) { // Legacy OIDC user without subject - update it await db.update(users)