Fix OIDC web flow redirect URI not being sent to IDP
The /login route was not passing an explicit redirect_uri to the IDP for the web flow, so openid-client would silently pick a default which could resolve to localhost:3001 if OIDC_REDIRECT_URI was not set. - AuthSession.redirectUri is now required (non-optional) - createAuthSession() requires a redirectUri; detects native vs web via the timetracker:// scheme prefix instead of presence/absence of the arg - /login route resolves the URI explicitly: request param for native flows, config.oidc.redirectUri for web flows - getAuthorizationUrl() reads redirect_uri from session, no longer accepts it as a separate argument - handleCallback() uses session.redirectUri directly, removing the fallback to config.oidc.redirectUri
This commit is contained in:
@@ -41,11 +41,11 @@ export interface AuthSession {
|
|||||||
codeVerifier: string;
|
codeVerifier: string;
|
||||||
state: string;
|
state: string;
|
||||||
nonce: string | undefined;
|
nonce: string | undefined;
|
||||||
redirectUri?: string;
|
redirectUri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAuthSession(redirectUri?: string): AuthSession {
|
export function createAuthSession(redirectUri: string): AuthSession {
|
||||||
const isNative = !!redirectUri;
|
const isNative = redirectUri.startsWith('timetracker://');
|
||||||
return {
|
return {
|
||||||
codeVerifier: generators.codeVerifier(),
|
codeVerifier: generators.codeVerifier(),
|
||||||
state: generators.state(),
|
state: generators.state(),
|
||||||
@@ -58,25 +58,22 @@ export function createAuthSession(redirectUri?: string): AuthSession {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthorizationUrl(session: AuthSession, redirectUri?: string): string {
|
export function getAuthorizationUrl(session: AuthSession): string {
|
||||||
const client = getOIDCClient();
|
const client = getOIDCClient();
|
||||||
const codeChallenge = generators.codeChallenge(session.codeVerifier);
|
const codeChallenge = generators.codeChallenge(session.codeVerifier);
|
||||||
|
|
||||||
const params: Record<string, string> = {
|
const params: Record<string, string> = {
|
||||||
scope: 'openid profile email',
|
scope: 'openid profile email',
|
||||||
state: session.state,
|
state: session.state,
|
||||||
code_challenge: codeChallenge,
|
code_challenge: codeChallenge,
|
||||||
code_challenge_method: 'S256',
|
code_challenge_method: 'S256',
|
||||||
|
redirect_uri: session.redirectUri,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (session.nonce) {
|
if (session.nonce) {
|
||||||
params.nonce = session.nonce;
|
params.nonce = session.nonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectUri) {
|
|
||||||
params.redirect_uri = redirectUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.authorizationUrl(params);
|
return client.authorizationUrl(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +82,7 @@ export async function handleCallback(
|
|||||||
session: AuthSession
|
session: AuthSession
|
||||||
): Promise<TokenSet> {
|
): Promise<TokenSet> {
|
||||||
const client = getOIDCClient();
|
const client = getOIDCClient();
|
||||||
|
|
||||||
const redirectUri = session.redirectUri || config.oidc.redirectUri;
|
|
||||||
|
|
||||||
const checks: Record<string, string | undefined> = {
|
const checks: Record<string, string | undefined> = {
|
||||||
code_verifier: session.codeVerifier,
|
code_verifier: session.codeVerifier,
|
||||||
state: session.state,
|
state: session.state,
|
||||||
@@ -98,11 +93,11 @@ export async function handleCallback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tokenSet = await client.callback(
|
const tokenSet = await client.callback(
|
||||||
redirectUri,
|
session.redirectUri,
|
||||||
params,
|
params,
|
||||||
checks,
|
checks,
|
||||||
);
|
);
|
||||||
|
|
||||||
return tokenSet;
|
return tokenSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "../auth/oidc";
|
} from "../auth/oidc";
|
||||||
import { signBackendJwt } from "../auth/jwt";
|
import { signBackendJwt } from "../auth/jwt";
|
||||||
import { requireAuth, syncUser } from "../middleware/auth";
|
import { requireAuth, syncUser } from "../middleware/auth";
|
||||||
|
import { config } from "../config";
|
||||||
import type { AuthenticatedRequest } from "../types";
|
import type { AuthenticatedRequest } from "../types";
|
||||||
import type { AuthSession } from "../auth/oidc";
|
import type { AuthSession } from "../auth/oidc";
|
||||||
|
|
||||||
@@ -49,11 +50,18 @@ router.get("/login", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await ensureOIDC();
|
await ensureOIDC();
|
||||||
|
|
||||||
const redirectUri = req.query.redirect_uri as string | undefined;
|
const isNativeFlow = !!req.query.redirect_uri;
|
||||||
console.log(`[auth/login] initiated (redirect_uri: ${redirectUri ?? '(web flow)'})`);
|
// For the web flow no redirect_uri is supplied in the request — use the
|
||||||
|
// backend-configured value so the IDP always receives an explicit URI
|
||||||
|
// rather than relying on the openid-client library to pick a default.
|
||||||
|
const redirectUri = isNativeFlow
|
||||||
|
? (req.query.redirect_uri as string)
|
||||||
|
: config.oidc.redirectUri;
|
||||||
|
|
||||||
|
console.log(`[auth/login] initiated (redirect_uri: ${redirectUri})`);
|
||||||
const session = createAuthSession(redirectUri);
|
const session = createAuthSession(redirectUri);
|
||||||
|
|
||||||
if (redirectUri) {
|
if (isNativeFlow) {
|
||||||
// Native app flow: store session by state so /auth/token can retrieve it
|
// Native app flow: store session by state so /auth/token can retrieve it
|
||||||
// without relying on the browser cookie jar.
|
// without relying on the browser cookie jar.
|
||||||
storeNativeSession(session);
|
storeNativeSession(session);
|
||||||
@@ -63,7 +71,7 @@ router.get("/login", async (req, res) => {
|
|||||||
req.session.oidc = session;
|
req.session.oidc = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorizationUrl = getAuthorizationUrl(session, redirectUri);
|
const authorizationUrl = getAuthorizationUrl(session);
|
||||||
console.log(`[auth/login] redirecting to IDP`);
|
console.log(`[auth/login] redirecting to IDP`);
|
||||||
res.redirect(authorizationUrl);
|
res.redirect(authorizationUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user