Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 35 additions & 21 deletions src/lib/server/auth/google.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHash } from 'node:crypto';

import { CodeChallengeMethod, OAuth2Client } from 'google-auth-library';
import { CodeChallengeMethod, type LoginTicket, OAuth2Client } from 'google-auth-library';

import { env } from '$env/dynamic/private';

Expand Down Expand Up @@ -124,36 +124,50 @@ export async function exchangeCodeForIdToken({
}

/**
* Verifies the Google ID token and extracts the profile information from the
* Google OAuth2 ID token.
* Verifies the Google ID token and extracts the profile information.
*
* @param idToken - The Google ID token to verify.
* @returns The Google profile or `null` if the ID token is invalid.
* Throws `InvalidIdTokenError` when the token cannot be trusted — bad
* signature, expired, missing claims, or hosted-domain mismatch.
*
* @example
* ```ts
* const profile = await verifyIdToken(idToken);
* ```
* @param idToken - The Google ID token to verify.
* @returns The Google profile.
* @throws InvalidIdTokenError
*/
export async function verifyIdToken(idToken: string): Promise<GoogleProfile | null> {
export async function verifyIdToken(idToken: string): Promise<GoogleProfile> {
const client = getOAuth2Client();

const ticket = await client.verifyIdToken({ idToken });
let ticket: LoginTicket;
try {
ticket = await client.verifyIdToken({ idToken });
} catch {
throw new InvalidIdTokenError(`Google ID token rejected by verifier`);
}

const payload = ticket.getPayload();
if (!payload || !payload.sub || !payload.email || !payload.name || !payload.picture) {
return null;
if (!payload) {
throw new InvalidIdTokenError('Google ID token payload missing');
}
const { sub, email, name, picture } = payload;
if (!sub || !email || !name || !picture) {
const missing = !sub ? 'sub' : !email ? 'email' : !name ? 'name' : 'picture';
throw new InvalidIdTokenError(`Google ID token missing claim: ${missing}`);
}

// If the Google hosted domain is set, make sure the `hd` claim matches the hosted domain.
if (env.GOOGLE_HOSTED_DOMAIN && payload.hd !== env.GOOGLE_HOSTED_DOMAIN) {
return null;
throw new InvalidIdTokenError(
'Google ID token hosted domain does not match the configured hosted domain',
);
}

return {
id: payload.sub,
email: payload.email,
name: payload.name,
picture: payload.picture,
};
return { id: sub, email, name, picture };
}

/**
* Thrown when token verification fails.
*/
export class InvalidIdTokenError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
9 changes: 2 additions & 7 deletions src/routes/(main)/auth/google/callback/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,11 @@ export const GET: RequestHandler = async (event) => {
return redirect(302, '/login?error=oauth2_callback_failed');
}

let profile: GoogleProfile | null = null;
let profile: GoogleProfile;
try {
profile = await verifyIdToken(idToken);

if (!profile) {
logger.error('Invalid Google profile');
return redirect(302, '/login?error=oauth2_callback_failed');
}
} catch (err) {
logger.error(err, 'Failed to verify ID token');
logger.error({ err }, 'Failed to verify ID token');
return redirect(302, '/login?error=oauth2_callback_failed');
}

Expand Down
9 changes: 2 additions & 7 deletions src/routes/admin/auth/google/callback/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,11 @@ export const GET: RequestHandler = async (event) => {
return redirect(302, '/admin?error=auth_failed');
}

let profile: GoogleProfile | null = null;
let profile: GoogleProfile;
try {
profile = await verifyIdToken(idToken);

if (!profile) {
logger.error('Invalid Google profile');
return redirect(302, '/admin?error=auth_failed');
}
} catch (err) {
logger.error(err, 'Failed to verify ID token');
logger.error({ err }, 'Failed to verify ID token');
return redirect(302, '/admin?error=auth_failed');
}

Expand Down
Loading