Update login pipeline. Limit login to only password
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user