UNPKG

firebase-auth-cloudflare-workers

Version:

Zero-dependencies firebase auth library for Cloudflare Workers.

259 lines (258 loc) 11.7 kB
import { ApiSettings } from './api-requests'; import { BaseClient } from './client'; import { AuthClientErrorCode, FirebaseAuthError } from './errors'; import { UserRecord } from './user-record'; import { isNonEmptyString, isNumber, isObject, isUid } from './validator'; /** Minimum allowed session cookie duration in seconds (5 minutes). */ const MIN_SESSION_COOKIE_DURATION_SECS = 5 * 60; /** Maximum allowed session cookie duration in seconds (2 weeks). */ const MAX_SESSION_COOKIE_DURATION_SECS = 14 * 24 * 60 * 60; /** List of reserved claims which cannot be provided when creating a custom token. */ const RESERVED_CLAIMS = [ 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat', 'iss', 'jti', 'nbf', 'nonce', 'sub', 'firebase', ]; /** Maximum allowed number of characters in the custom claims payload. */ const MAX_CLAIMS_PAYLOAD_SIZE = 1000; /** * Instantiates the createSessionCookie endpoint settings. * * @internal */ export const FIREBASE_AUTH_CREATE_SESSION_COOKIE = new ApiSettings('v1', ':createSessionCookie', 'POST') // Set request validator. .setRequestValidator((request) => { // Validate the ID token is a non-empty string. if (!isNonEmptyString(request.idToken)) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); } // Validate the custom session cookie duration. if (!isNumber(request.validDuration) || request.validDuration < MIN_SESSION_COOKIE_DURATION_SECS || request.validDuration > MAX_SESSION_COOKIE_DURATION_SECS) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION); } }) // Set response validator. .setResponseValidator((response) => { // Response should always contain the session cookie. if (!isNonEmptyString(response.sessionCookie)) { throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); } }); /** * Instantiates the getAccountInfo endpoint settings. * * @internal */ export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('v1', '/accounts:lookup', 'POST') // Set request validator. .setRequestValidator((request) => { if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) { throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } }) // Set response validator. .setResponseValidator((response) => { if (!response.users || !response.users.length) { throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); } }); /** * Instantiates the revokeRefreshTokens endpoint settings for updating existing accounts. * * @internal * @link https://github.com/firebase/firebase-admin-node/blob/9955bca47249301aa970679ae99fe01d54adf6a8/src/auth/auth-api-request.ts#L746 */ export const FIREBASE_AUTH_REVOKE_REFRESH_TOKENS = new ApiSettings('v1', '/accounts:update', 'POST') // Set request validator. .setRequestValidator((request) => { // localId is a required parameter. if (typeof request.localId === 'undefined') { throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } // validSince should be a number. if (typeof request.validSince !== 'undefined' && !isNumber(request.validSince)) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME); } }) // Set response validator. .setResponseValidator((response) => { // If the localId is not returned, then the request failed. if (!response.localId) { throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); } }); /** * Instantiates the setCustomUserClaims endpoint settings for updating existing accounts. * * @internal * @link https://github.com/firebase/firebase-admin-node/blob/9955bca47249301aa970679ae99fe01d54adf6a8/src/auth/auth-api-request.ts#L746 */ export const FIREBASE_AUTH_SET_CUSTOM_USER_CLAIMS = new ApiSettings('v1', '/accounts:update', 'POST') // Set request validator. .setRequestValidator((request) => { // localId is a required parameter. if (typeof request.localId === 'undefined') { throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } // customAttributes should be stringified JSON with no blacklisted claims. // The payload should not exceed 1KB. if (typeof request.customAttributes !== 'undefined') { let developerClaims; try { developerClaims = JSON.parse(request.customAttributes); } catch (error) { if (error instanceof Error) { // JSON parsing error. This should never happen as we stringify the claims internally. // However, we still need to check since setAccountInfo via edit requests could pass // this field. throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CLAIMS, error.message); } throw error; } const invalidClaims = []; // Check for any invalid claims. RESERVED_CLAIMS.forEach(blacklistedClaim => { if (Object.prototype.hasOwnProperty.call(developerClaims, blacklistedClaim)) { invalidClaims.push(blacklistedClaim); } }); // Throw an error if an invalid claim is detected. if (invalidClaims.length > 0) { throw new FirebaseAuthError(AuthClientErrorCode.FORBIDDEN_CLAIM, invalidClaims.length > 1 ? `Developer claims "${invalidClaims.join('", "')}" are reserved and cannot be specified.` : `Developer claim "${invalidClaims[0]}" is reserved and cannot be specified.`); } // Check claims payload does not exceed maxmimum size. if (request.customAttributes.length > MAX_CLAIMS_PAYLOAD_SIZE) { throw new FirebaseAuthError(AuthClientErrorCode.CLAIMS_TOO_LARGE, `Developer claims payload should not exceed ${MAX_CLAIMS_PAYLOAD_SIZE} characters.`); } } }) // Set response validator. .setResponseValidator((response) => { // If the localId is not returned, then the request failed. if (!response.localId) { throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); } }); export class AuthApiClient extends BaseClient { /** * Creates a new Firebase session cookie with the specified duration that can be used for * session management (set as a server side session cookie with custom cookie policy). * The session cookie JWT will have the same payload claims as the provided ID token. * * @param idToken - The Firebase ID token to exchange for a session cookie. * @param expiresIn - The session cookie duration in milliseconds. * @param env - An optional parameter specifying the environment in which the function is running. * If the function is running in an emulator environment, this should be set to `EmulatorEnv`. * If not specified, the function will assume it is running in a production environment. * * @returns A promise that resolves on success with the created session cookie. */ async createSessionCookie(idToken, expiresIn, env) { const request = { idToken, // Convert to seconds. validDuration: expiresIn / 1000, }; const res = await this.fetch(FIREBASE_AUTH_CREATE_SESSION_COOKIE, request, env); return res.sessionCookie; } /** * Looks up a user by uid. * * @param uid - The uid of the user to lookup. * @param env - An optional parameter specifying the environment in which the function is running. * If the function is running in an emulator environment, this should be set to `EmulatorEnv`. * If not specified, the function will assume it is running in a production environment. * @returns A promise that resolves with the user information. */ async getAccountInfoByUid(uid, env) { if (!isUid(uid)) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); } const request = { localId: [uid], }; const res = await this.fetch(FIREBASE_AUTH_GET_ACCOUNT_INFO, request, env); // Returns the user record populated with server response. return new UserRecord(res.users[0]); } /** * Revokes all refresh tokens for the specified user identified by the uid provided. * In addition to revoking all refresh tokens for a user, all ID tokens issued * before revocation will also be revoked on the Auth backend. Any request with an * ID token generated before revocation will be rejected with a token expired error. * Note that due to the fact that the timestamp is stored in seconds, any tokens minted in * the same second as the revocation will still be valid. If there is a chance that a token * was minted in the last second, delay for 1 second before revoking. * * @param uid - The user whose tokens are to be revoked. * @param env - An optional parameter specifying the environment in which the function is running. * If the function is running in an emulator environment, this should be set to `EmulatorEnv`. * If not specified, the function will assume it is running in a production environment. * @returns A promise that resolves when the operation completes * successfully with the user id of the corresponding user. */ async revokeRefreshTokens(uid, env) { // Validate user UID. if (!isUid(uid)) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); } const request = { localId: uid, // validSince is in UTC seconds. validSince: Math.floor(new Date().getTime() / 1000), }; const res = await this.fetch(FIREBASE_AUTH_REVOKE_REFRESH_TOKENS, request, env); return res.localId; } /** * Sets additional developer claims on an existing user identified by provided UID. * * @param uid - The user to edit. * @param customUserClaims - The developer claims to set. * @param env - An optional parameter specifying the environment in which the function is running. * If the function is running in an emulator environment, this should be set to `EmulatorEnv`. * If not specified, the function will assume it is running in a production environment. * @returns A promise that resolves when the operation completes * with the user id that was edited. */ async setCustomUserClaims(uid, customUserClaims, env) { // Validate user UID. if (!isUid(uid)) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); } else if (!isObject(customUserClaims)) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, 'CustomUserClaims argument must be an object or null.'); } // Delete operation. Replace null with an empty object. if (customUserClaims === null) { customUserClaims = {}; } // Construct custom user attribute editting request. const request = { localId: uid, customAttributes: JSON.stringify(customUserClaims), }; const res = await this.fetch(FIREBASE_AUTH_SET_CUSTOM_USER_CLAIMS, request, env); return res.localId; } }