Update login pipeline. Limit login to only password

This commit is contained in:
2026-09-12 14:20:07 +01:00
parent 4d75af0418
commit 9cfbde9c9a
30 changed files with 145 additions and 1068 deletions
-15
View File
@@ -5,7 +5,6 @@ import grabUsers from "../db/users/grab-users";
import BunSQLite from "@moduletrace/bun-sqlite";
import type {
BUN_SQLITE_WGUI_MEDIA,
BUN_SQLITE_WGUI_SSO_LOGIN_CODES,
BUN_SQLITE_WGUI_USER_TYPES,
BunSQLiteTables,
} from "@/db/types/db";
@@ -112,20 +111,6 @@ export default async function loginUser({
},
]);
const delete_all_user_sso_codes = await BunSQLite.delete<
BUN_SQLITE_WGUI_SSO_LOGIN_CODES,
(typeof BunSQLiteTables)[number]
>({
table: "sso_login_codes",
query: {
query: {
user_id: {
value: new_logged_in_user.id,
},
},
},
});
console.log(
`User #${new_logged_in_user.id} [${new_logged_in_user.first_name} ${new_logged_in_user.last_name}] logged in successfully.`,
);
@@ -1,190 +0,0 @@
type GoogleJWTHeader = {
alg?: string;
kid?: string;
typ?: string;
};
type GoogleJWKS = {
keys?: GoogleJWK[];
};
type GoogleJWK = JsonWebKey & {
kid?: string;
};
type GoogleIDTokenPayload = {
aud?: string | string[];
email?: string;
email_verified?: boolean | string;
exp?: number | string;
iss?: string;
name?: string;
picture?: string;
sub?: string;
};
type Params = {
clientId: string;
idToken: string;
};
type GoogleIDTokenUser = Required<Pick<GoogleIDTokenPayload, "email" | "sub">> &
Pick<GoogleIDTokenPayload, "name" | "picture">;
const GoogleJWKSUrl = "https://www.googleapis.com/oauth2/v3/certs";
let cachedGoogleKeys: {
expiresAt: number;
keys: GoogleJWK[];
} | null = null;
function decodeBase64Url(value: string) {
const normalizedValue = value.replace(/-/g, "+").replace(/_/g, "/");
const padding = normalizedValue.length % 4;
const paddedValue =
padding === 0
? normalizedValue
: normalizedValue.padEnd(
normalizedValue.length + (4 - padding),
"=",
);
return Buffer.from(paddedValue, "base64");
}
function parseGoogleTokenPart<T>(value: string): T {
return JSON.parse(decodeBase64Url(value).toString("utf8")) as T;
}
async function getGoogleSigningKeys() {
if (cachedGoogleKeys && cachedGoogleKeys.expiresAt > Date.now()) {
return cachedGoogleKeys.keys;
}
const response = await fetch(GoogleJWKSUrl);
if (!response.ok) {
throw new Error(`Couldn't fetch Google's signing keys`);
}
const data = (await response.json()) as GoogleJWKS;
if (!data.keys?.length) {
throw new Error(`Google didn't return any signing keys`);
}
const maxAge =
Number(
response.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1],
) || 300;
cachedGoogleKeys = {
expiresAt: Date.now() + maxAge * 1000,
keys: data.keys,
};
return data.keys;
}
function hasMatchingAudience(
aud: GoogleIDTokenPayload["aud"],
clientId: string,
) {
if (Array.isArray(aud)) {
return aud.includes(clientId);
}
return aud === clientId;
}
export default async function verifyGoogleIdToken({
clientId,
idToken,
}: Params): Promise<GoogleIDTokenUser> {
const tokenParts = idToken.split(".");
if (tokenParts.length !== 3) {
throw new Error(`Invalid Google token`);
}
const [encodedHeader, encodedPayload, encodedSignature] = tokenParts;
if (!encodedHeader) {
throw new Error(`No encodedHeader`);
}
if (!encodedPayload) {
throw new Error(`No encodedPayload`);
}
if (!encodedSignature) {
throw new Error(`No encodedSignature`);
}
const header = parseGoogleTokenPart<GoogleJWTHeader>(encodedHeader);
const payload = parseGoogleTokenPart<GoogleIDTokenPayload>(encodedPayload);
if (header.alg !== "RS256" || !header.kid) {
throw new Error(`Unsupported Google token signature`);
}
const signingKeys = await getGoogleSigningKeys();
const signingKey = signingKeys.find((key) => key.kid === header.kid);
if (!signingKey) {
throw new Error(`Unknown Google signing key`);
}
const importedKey = await crypto.subtle.importKey(
"jwk",
signingKey,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
false,
["verify"],
);
const isValidSignature = await crypto.subtle.verify(
"RSASSA-PKCS1-v1_5",
importedKey,
decodeBase64Url(encodedSignature),
new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`),
);
if (!isValidSignature) {
throw new Error(`Invalid Google token signature`);
}
if (!hasMatchingAudience(payload.aud, clientId)) {
throw new Error(`Google token was issued for a different app`);
}
if (
payload.iss !== "https://accounts.google.com" &&
payload.iss !== "accounts.google.com"
) {
throw new Error(`Invalid Google token issuer`);
}
const expiresAt = Number(payload.exp || 0) * 1000;
if (!expiresAt || expiresAt <= Date.now()) {
throw new Error(`Google token has expired`);
}
const emailVerified =
payload.email_verified === true || payload.email_verified === "true";
if (!payload.email || !emailVerified || !payload.sub) {
throw new Error(`Google account email is not verified`);
}
return {
email: payload.email,
sub: payload.sub,
name: payload.name,
picture: payload.picture,
};
}