├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── pull_request.yml │ └── pull_request_lint.yml ├── .gitignore ├── .prettierignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .yarn └── releases │ └── yarn-4.0.0-rc.39.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── core ├── env.ts ├── error.ts ├── jwt.test.ts ├── jwt.ts └── utils.ts ├── google ├── accessToken.test.ts ├── accessToken.ts ├── credentials.test.ts ├── credentials.ts ├── customToken.test.ts ├── customToken.ts ├── idToken.test.ts ├── idToken.ts └── index.ts ├── index.ts ├── jest.config.js ├── package.json ├── test ├── env.ts ├── setup.ts └── test.env ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kriasoft 4 | patreon: koistya 5 | open_collective: react-starter-kit 6 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow for Pull Requests 2 | # https://help.github.com/actions 3 | 4 | name: PR 5 | 6 | on: [pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 19 16 | cache: "yarn" 17 | 18 | # Install dependencies 19 | - run: yarn config set enableGlobalCache false 20 | - run: yarn install 21 | 22 | # Analyze code for potential problems 23 | - run: yarn prettier --check . 24 | - run: yarn lint 25 | - run: yarn tsc 26 | 27 | # Test 28 | - run: yarn test 29 | env: 30 | GOOGLE_CLOUD_CREDENTIALS: ${{ secrets.GOOGLE_CLOUD_CREDENTIALS }} 31 | FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} 32 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_lint.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow 2 | # https://help.github.com/actions 3 | 4 | name: "conventionalcommits.org" 5 | 6 | on: 7 | pull_request: 8 | types: 9 | - opened 10 | - edited 11 | - synchronize 12 | 13 | jobs: 14 | main: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | # Cache and compiled output 5 | /.cache/ 6 | /dist/ 7 | 8 | # Yarn package manager 9 | # https://yarnpkg.com/getting-started/qa/#which-files-should-be-gitignored 10 | /node_modules 11 | .pnp.* 12 | .yarn/* 13 | !.yarn/patches 14 | !.yarn/plugins 15 | !.yarn/releases 16 | !.yarn/sdks 17 | !.yarn/versions 18 | 19 | # Logs 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # Overrides for *.env files 24 | *.override.env 25 | 26 | # Visual Studio Code 27 | # https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | 34 | # macOS 35 | # https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 36 | .DS_Store 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.yarn 2 | /dist 3 | /node_modules 4 | /tsconfig.json 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests.v2", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand", 13 | "--watchAll=false", 14 | "--testNamePattern", 15 | "${jest.testNamePattern}", 16 | "--runTestsByPath", 17 | "${jest.testFile}" 18 | ], 19 | "cwd": "${workspaceFolder}", 20 | "console": "integratedTerminal", 21 | "internalConsoleOptions": "neverOpen", 22 | "disableOptimisticBPs": true, 23 | "runtimeExecutable": "node", 24 | "runtimeArgs": [ 25 | "--experimental-vm-modules", 26 | "${workspaceFolder}/node_modules/.bin/jest" 27 | ], 28 | "windows": { 29 | "runtimeArgs": [ 30 | "--experimental-vm-modules", 31 | "${workspaceFolder}/node_modules/jest/bin/jest" 32 | ] 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports": true 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.tabSize": 2, 8 | "typescript.enablePromptUseWorkspaceTsdk": true, 9 | "jest.jestCommandLine": "yarn node --experimental-vm-modules $(yarn bin jest)", 10 | "jestrunner.jestCommand": "yarn node --experimental-vm-modules $(yarn bin jest)", 11 | "jestrunner.debugOptions": { 12 | "env": { 13 | "NODE_OPTIONS": "--experimental-vm-modules" 14 | } 15 | }, 16 | "files.exclude": { 17 | "**/.cache": true, 18 | "**/.DS_Store": true, 19 | "**/.editorconfig": true, 20 | "**/.eslintcache": true, 21 | "**/.git": true, 22 | "**/.gitattributes": true, 23 | "**/.pnp.cjs": true, 24 | "**/.pnp.loader.mjs": true, 25 | "**/.prettierignore": true, 26 | "**/node_modules": true 27 | }, 28 | "search.exclude": { 29 | "**/dist/": true, 30 | "**/yarn-error.log": true, 31 | "**/yarn.lock": true, 32 | "**/.yarn": true, 33 | "**/.pnp.*": true 34 | }, 35 | "terminal.integrated.env.linux": { 36 | "CACHE_DIR": "${workspaceFolder}/.cache" 37 | }, 38 | "terminal.integrated.env.osx": { 39 | "CACHE_DIR": "${workspaceFolder}/.cache" 40 | }, 41 | "terminal.integrated.env.windows": { 42 | "CACHE_DIR": "${workspaceFolder}\\.cache" 43 | }, 44 | "cSpell.words": [ 45 | "async", 46 | "await", 47 | "cjs", 48 | "endregion", 49 | "esm", 50 | "hono", 51 | "identitytoolkit", 52 | "jest", 53 | "mjs", 54 | "node", 55 | "ts", 56 | "tsconfig", 57 | "tslib", 58 | "tslint", 59 | "tsnode", 60 | "tsv", 61 | "tsx", 62 | "yarn" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.0.0-rc.39.cjs 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-present Kriasoft 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Auth Library 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/web-auth-library?style=flat-square)](https://www.npmjs.com/package/web-auth-library) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/web-auth-library?style=flat-square)](https://www.npmjs.com/package/web-auth-library) 5 | [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg?style=flat-square)](http://www.typescriptlang.org/) 6 | [![Donate](https://img.shields.io/badge/dynamic/json?color=%23ff424d&label=Patreon&style=flat-square&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F233228)](http://patreon.com/koistya) 7 | [![Discord](https://img.shields.io/discord/643523529131950086?label=Chat&style=flat-square)](https://discord.gg/bSsv7XM) 8 | 9 | Authentication library for Google Cloud, Firebase, and other cloud providers that uses standard [Web Crypto API](https://developer.mozilla.org/docs/Web/API/Web_Crypto_API) and runs in different environments and runtimes, including but not limited to: 10 | 11 | - [Bun](https://bun.sh/) 12 | - [Browsers](https://developer.mozilla.org/docs/Web/API/Web_Crypto_API) 13 | - [Cloudflare Workers](https://workers.cloudflare.com/) 14 | - [Deno](https://deno.land/) 15 | - [Electron](https://www.electronjs.org/) 16 | - [Node.js](https://nodejs.org/) 17 | - [Vercel's Edge Runtime](https://edge-runtime.vercel.app/) 18 | 19 | It has minimum dependencies, small bundle size, and optimized for speed and performance. 20 | 21 | ## Getting Stated 22 | 23 | ```bash 24 | # Install using NPM 25 | $ npm install web-auth-library --save 26 | 27 | # Install using Yarn 28 | $ yarn add web-auth-library 29 | ``` 30 | 31 | ## Usage Examples 32 | 33 | ### Verify the user ID Token issued by Google or Firebase 34 | 35 | **NOTE**: The `credentials` argument in the examples below is expected to be a serialized JSON string of a [Google Cloud service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys), `apiKey` is Google Cloud API Key (Firebase API Key), and `projectId` is a Google Cloud project ID. 36 | 37 | ```ts 38 | import { verifyIdToken } from "web-auth-library/google"; 39 | 40 | const token = await verifyIdToken({ 41 | idToken, 42 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 43 | }); 44 | 45 | // => { 46 | // iss: 'https://securetoken.google.com/example', 47 | // aud: 'example', 48 | // auth_time: 1677525930, 49 | // user_id: 'temp', 50 | // sub: 'temp', 51 | // iat: 1677525930, 52 | // exp: 1677529530, 53 | // firebase: {} 54 | // } 55 | ``` 56 | 57 | ### Create an access token for accessing [Google Cloud APIs](https://developers.google.com/apis-explorer) 58 | 59 | ```ts 60 | import { getAccessToken } from "web-auth-library/google"; 61 | 62 | // Generate a short lived access token from the service account key credentials 63 | const accessToken = await getAccessToken({ 64 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 65 | scope: "https://www.googleapis.com/auth/cloud-platform", 66 | }); 67 | 68 | // Make a request to one of the Google's APIs using that token 69 | const res = await fetch( 70 | "https://cloudresourcemanager.googleapis.com/v1/projects", 71 | { 72 | headers: { Authorization: `Bearer ${accessToken}` }, 73 | } 74 | ); 75 | ``` 76 | 77 | ## Create a custom ID token using Service Account credentials 78 | 79 | ```ts 80 | import { getIdToken } from "web-auth-library/google"; 81 | 82 | const idToken = await getIdToken({ 83 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 84 | audience: "https://example.com", 85 | }); 86 | ``` 87 | 88 | ## An alternative way passing credentials 89 | 90 | Instead of passing credentials via `options.credentials` argument, you can also let the library pick up credentials from the list of environment variables using standard names such as `GOOGLE_CLOUD_CREDENTIALS`, `GOOGLE_CLOUD_PROJECT`, `FIREBASE_API_KEY`, for example: 91 | 92 | ```ts 93 | import { verifyIdToken } from "web-auth-library/google"; 94 | 95 | const env = { GOOGLE_CLOUD_CREDENTIALS: "..." }; 96 | const token = await verifyIdToken({ idToken, env }); 97 | ``` 98 | 99 | ## Optimize cache renewal background tasks 100 | 101 | Pass the optional `waitUntil(promise)` function provided by the target runtime to optimize the way authentication tokens are being renewed in background. For example, using Cloudflare Workers and [Hono.js](https://hono.dev/): 102 | 103 | ```ts 104 | import { Hono } from "hono"; 105 | import { verifyIdToken } from "web-auth-library/google"; 106 | 107 | const app = new Hono(); 108 | 109 | app.get("/", ({ env, executionCtx, json }) => { 110 | const idToken = await verifyIdToken({ 111 | idToken: "...", 112 | waitUntil: executionCtx.waitUntil, 113 | env, 114 | }); 115 | 116 | return json({ ... }); 117 | }) 118 | ``` 119 | 120 | ## Backers 💰 121 | 122 |                123 | 124 | ## Related Projects 125 | 126 | - [React Starter Kit](https://github.com/kriasoft/react-starter-kit) — front-end template for React and Relay using Jamstack architecture 127 | - [GraphQL API and Relay Starter Kit](https://github.com/kriasoft/graphql-starter) — monorepo template, pre-configured with GraphQL API, React, and Relay 128 | - [Cloudflare Workers Starter Kit](https://github.com/kriasoft/cloudflare-starter-kit) — TypeScript project template for Cloudflare Workers 129 | 130 | ## How to Contribute 131 | 132 | You're very welcome to [create a PR](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) 133 | or send me a message on [Discord](https://discord.gg/bSsv7XM). 134 | 135 | In order to unit test this library locally you will need [Node.js](https://nodejs.org/) v18+ with [corepack enabled](https://nodejs.org/api/corepack.html), a Google Cloud [service account key](https://cloud.google.com/iam/docs/keys-create-delete) ([here](https://console.cloud.google.com/iam-admin/serviceaccounts)) and Firebase API Key ([here](https://console.cloud.google.com/apis/credentials)) that you can save into the [`test/test.override.env`](./test/test.env) file, for example: 136 | 137 | ``` 138 | GOOGLE_CLOUD_PROJECT=example 139 | GOOGLE_CLOUD_CREDENTIALS={"type":"service_account","project_id":"example",...} 140 | FIREBASE_API_KEY=AIzaSyAZEmdfRWvEYgZpwm6EBLkYJf6ySIMF3Hy 141 | ``` 142 | 143 | Then run unit tests via `yarn test [--watch]`. 144 | 145 | ## License 146 | 147 | Copyright © 2022-present Kriasoft. This source code is licensed under the MIT license found in the 148 | [LICENSE](https://github.com/kriasoft/web-auth-library/blob/main/LICENSE) file. 149 | 150 | --- 151 | 152 | Made with ♥ by Konstantin Tarkus ([@koistya](https://twitter.com/koistya), [blog](https://medium.com/@koistya)) 153 | and [contributors](https://github.com/kriasoft/web-auth-library/graphs/contributors). 154 | -------------------------------------------------------------------------------- /core/env.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | export const canUseDefaultCache = 5 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 6 | typeof (globalThis as any).caches?.default?.put === "function"; 7 | -------------------------------------------------------------------------------- /core/error.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | export class FetchError extends Error { 5 | readonly name: string = "FetchError"; 6 | readonly response: Response; 7 | 8 | constructor( 9 | message: string, 10 | options: { response: Response; cause?: unknown } 11 | ) { 12 | super(message, { cause: options?.cause }); 13 | this.response = options.response; 14 | 15 | if (Error.captureStackTrace) { 16 | Error.captureStackTrace(this, Error); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/jwt.test.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { jwt } from "../index.js"; 5 | 6 | test("jwt.decode(token)", () => { 7 | const token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJleHAiOjEzOTMyODY4OTMsImlhdCI6MTM5MzI2ODg5M30.4-iaDojEVl0pJQMjrbM1EzUIfAZgsbK_kgnVyVxFSVo"; // prettier-ignore 8 | const result = jwt.decode(token); 9 | 10 | expect(result).toMatchInlineSnapshot(` 11 | { 12 | "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJleHAiOjEzOTMyODY4OTMsImlhdCI6MTM5MzI2ODg5M30", 13 | "header": { 14 | "alg": "HS256", 15 | "typ": "JWT", 16 | }, 17 | "payload": { 18 | "exp": 1393286893, 19 | "foo": "bar", 20 | "iat": 1393268893, 21 | }, 22 | "signature": "4-iaDojEVl0pJQMjrbM1EzUIfAZgsbK_kgnVyVxFSVo", 23 | } 24 | `); 25 | }); 26 | 27 | test("jwt.decode(unicodeToken)", () => { 28 | const unicodeToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9zw6kiLCJpYXQiOjE0MjU2NDQ5NjZ9.1CfFtdGUPs6q8kT3OGQSVlhEMdbuX0HfNSqum0023a0"; // prettier-ignore 29 | const result = jwt.decode(unicodeToken); 30 | 31 | expect(result).toMatchInlineSnapshot(` 32 | { 33 | "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9zw6kiLCJpYXQiOjE0MjU2NDQ5NjZ9", 34 | "header": { 35 | "alg": "HS256", 36 | "typ": "JWT", 37 | }, 38 | "payload": { 39 | "iat": 1425644966, 40 | "name": "José", 41 | }, 42 | "signature": "1CfFtdGUPs6q8kT3OGQSVlhEMdbuX0HfNSqum0023a0", 43 | } 44 | `); 45 | }); 46 | 47 | test("jwt.decode(binaryToken)", () => { 48 | const binaryToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9z6SIsImlhdCI6MTQyNTY0NDk2Nn0.cpnplCBxiw7Xqz5thkqs4Mo_dymvztnI0CI4BN0d1t8"; // prettier-ignore 49 | const result = jwt.decode(binaryToken); 50 | 51 | expect(result).toMatchInlineSnapshot(` 52 | { 53 | "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9z6SIsImlhdCI6MTQyNTY0NDk2Nn0", 54 | "header": { 55 | "alg": "HS256", 56 | "typ": "JWT", 57 | }, 58 | "payload": { 59 | "iat": 1425644966, 60 | "name": "Jos�", 61 | }, 62 | "signature": "cpnplCBxiw7Xqz5thkqs4Mo_dymvztnI0CI4BN0d1t8", 63 | } 64 | `); 65 | }); 66 | -------------------------------------------------------------------------------- /core/jwt.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { base64url } from "rfc4648"; 5 | 6 | /** 7 | * Converts the given JSON Web Token string into a `Jwt` object. 8 | */ 9 | function decode(token: string): Jwt { 10 | const segments = token.split("."); 11 | const dec = new TextDecoder(); 12 | 13 | if (segments.length !== 3) { 14 | throw new Error(); 15 | } 16 | 17 | return { 18 | header: JSON.parse( 19 | dec.decode(base64url.parse(segments[0], { loose: true })) 20 | ), 21 | 22 | payload: JSON.parse( 23 | dec.decode(base64url.parse(segments[1], { loose: true })) 24 | ), 25 | 26 | data: `${segments[0]}.${segments[1]}`, 27 | signature: segments[2], 28 | }; 29 | } 30 | 31 | async function verify( 32 | token: Jwt | string, 33 | options: VerifyOptions 34 | ): Promise { 35 | const enc = new TextEncoder(); 36 | const jwt = typeof token === "string" ? decode(token) : token; 37 | const aud = (jwt.payload as { aud?: string }).aud; 38 | 39 | if ( 40 | options.audience && 41 | (!aud || 42 | (Array.isArray(options.audience) && !options.audience.includes(aud)) || 43 | options.audience !== aud) 44 | ) { 45 | return; 46 | } 47 | 48 | const verified = await crypto.subtle.verify( 49 | options.key.algorithm, 50 | options.key, 51 | base64url.parse(jwt.signature, { loose: true }), 52 | enc.encode(jwt.data) 53 | ); 54 | 55 | return verified ? jwt.payload : undefined; 56 | } 57 | 58 | /* ------------------------------------------------------------------------------- * 59 | * TypeScript definitions 60 | * ------------------------------------------------------------------------------- */ 61 | 62 | /** 63 | * Identifies which algorithm is used to generate the signature. 64 | */ 65 | interface JwtHeader { 66 | /** Token type */ 67 | typ?: string; 68 | /** Content type*/ 69 | cty?: string; 70 | /** Message authentication code algorithm */ 71 | alg?: string; 72 | /** Key ID */ 73 | kid?: string; 74 | /** x.509 Certificate Chain */ 75 | x5c?: string; 76 | /** x.509 Certificate Chain URL */ 77 | x5u?: string; 78 | /** Critical */ 79 | crit?: string; 80 | } 81 | 82 | /** 83 | * Contains a set of claims. 84 | */ 85 | interface JwtPayload { 86 | /** Issuer */ 87 | iss?: string; 88 | /** Subject */ 89 | sub?: string; 90 | /** Audience */ 91 | aud?: string; 92 | /** Authorized party */ 93 | azp?: string; 94 | /** Expiration time */ 95 | exp?: number; 96 | /** Not before */ 97 | nbf?: number; 98 | /** Issued at */ 99 | iat?: number; 100 | /** JWT ID */ 101 | jti?: string; 102 | } 103 | 104 | /** 105 | * JSON Web Token (JWT) 106 | */ 107 | type Jwt = { 108 | header: H; 109 | payload: T; 110 | data: string; 111 | signature: string; 112 | }; 113 | 114 | type VerifyOptions = { 115 | key: CryptoKey; 116 | audience?: string[] | string; 117 | }; 118 | 119 | export { 120 | decode, 121 | verify, 122 | type Jwt, 123 | type JwtHeader, 124 | type JwtPayload, 125 | type VerifyOptions, 126 | }; 127 | -------------------------------------------------------------------------------- /core/utils.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | const logOnceKeys = new Set(); 5 | type Severity = "log" | "warn" | "error"; 6 | 7 | export function logOnce(severity: Severity, key: string, message: string) { 8 | if (!logOnceKeys.has(key)) { 9 | logOnceKeys.add(key); 10 | console[severity](message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /google/accessToken.test.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { decodeJwt } from "jose"; 5 | import env from "../test/env.js"; 6 | import { getAccessToken } from "./accessToken.js"; 7 | 8 | test("getAccessToken({ credentials, scope })", async () => { 9 | const accessToken = await getAccessToken({ 10 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 11 | scope: "https://www.googleapis.com/auth/cloud-platform", 12 | }); 13 | 14 | expect(accessToken?.substring(0, 30)).toEqual( 15 | expect.stringContaining("ya29.c.") 16 | ); 17 | }); 18 | 19 | test("getAccessToken({ credentials, audience })", async () => { 20 | const idToken = await getAccessToken({ 21 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 22 | audience: "https://example.com", 23 | }); 24 | 25 | expect(idToken?.substring(0, 30)).toEqual( 26 | expect.stringContaining("eyJhbGciOi") 27 | ); 28 | 29 | expect(decodeJwt(idToken)).toEqual( 30 | expect.objectContaining({ 31 | aud: "https://example.com", 32 | email_verified: true, 33 | iss: "https://accounts.google.com", 34 | }) 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /google/accessToken.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { decodeJwt } from "jose"; 5 | import { canUseDefaultCache } from "../core/env.js"; 6 | import { FetchError } from "../core/error.js"; 7 | import { logOnce } from "../core/utils.js"; 8 | import { getCredentials, type Credentials } from "./credentials.js"; 9 | import { createCustomToken } from "./customToken.js"; 10 | 11 | const defaultCache = new Map(); 12 | 13 | /** 14 | * Fetches an access token from Google Cloud API using the provided 15 | * service account credentials. 16 | * 17 | * @throws {FetchError} — If the access token could not be fetched. 18 | */ 19 | export async function getAccessToken(options: Options) { 20 | if (!options?.waitUntil && canUseDefaultCache) { 21 | logOnce("warn", "verifyIdToken", "Missing `waitUntil` option."); 22 | } 23 | 24 | let credentials: Credentials; 25 | 26 | // Normalize service account credentials 27 | // using env.GOOGLE_CLOUD_CREDENTIALS as a fallback 28 | if (options?.credentials) { 29 | credentials = getCredentials(options.credentials); 30 | } else { 31 | if (!options?.env?.GOOGLE_CLOUD_CREDENTIALS) { 32 | throw new TypeError("Missing credentials"); 33 | } 34 | credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS); 35 | } 36 | 37 | // Normalize authentication scope and audience values 38 | const scope = Array.isArray(options.scope) 39 | ? options.scope.join(",") 40 | : options.scope; 41 | const audience = Array.isArray(options.audience) 42 | ? options.audience.join(",") 43 | : options.audience; 44 | 45 | const tokenUrl = credentials.token_uri; 46 | 47 | // Create a cache key that can be used with Cloudflare Cache API 48 | const cacheKeyUrl = new URL(tokenUrl); 49 | cacheKeyUrl.searchParams.set("scope", scope ?? ""); 50 | cacheKeyUrl.searchParams.set("aud", audience ?? ""); 51 | cacheKeyUrl.searchParams.set("key", credentials.private_key_id); 52 | const cacheKey = cacheKeyUrl.toString(); 53 | 54 | // Attempt to retrieve the token from the cache 55 | const cache: Map = options.cache ?? defaultCache; 56 | const cacheValue = cache.get(cacheKey); 57 | let now = Math.floor(Date.now() / 1000); 58 | 59 | if (cacheValue) { 60 | if (cacheValue.created > now - 60 * 60) { 61 | let token = await cacheValue.promise; 62 | 63 | if (token.expires > now) { 64 | return token.token; 65 | } else { 66 | const nextValue = cache.get(cacheKey); 67 | 68 | if (nextValue && nextValue !== cacheValue) { 69 | token = await nextValue.promise; 70 | if (token.expires > now) { 71 | return token.token; 72 | } else { 73 | cache.delete(cacheKey); 74 | } 75 | } 76 | } 77 | } else { 78 | cache.delete(cacheKey); 79 | } 80 | } 81 | 82 | const promise = (async () => { 83 | let res: Response | undefined; 84 | 85 | // Attempt to retrieve the token from Cloudflare cache 86 | // if the code is running in Cloudflare Workers environment 87 | if (canUseDefaultCache) { 88 | res = await caches.default.match(cacheKey); 89 | } 90 | 91 | if (!res) { 92 | now = Math.floor(Date.now() / 1000); 93 | 94 | // Request a new token from the Google Cloud API 95 | const jwt = await createCustomToken({ 96 | credentials, 97 | scope: options.audience ?? options.scope, 98 | }); 99 | const body = new URLSearchParams(); 100 | body.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"); 101 | body.append("assertion", jwt); 102 | res = await fetch(tokenUrl, { 103 | method: "POST", 104 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 105 | body, 106 | }); 107 | 108 | if (!res.ok) { 109 | const error = await res 110 | .json<{ error_description?: string }>() 111 | .then((data) => data?.error_description) 112 | .catch(() => undefined); 113 | throw new FetchError(error ?? "Failed to fetch an access token.", { 114 | response: res, 115 | }); 116 | } 117 | 118 | if (canUseDefaultCache) { 119 | let cacheRes = res.clone(); 120 | cacheRes = new Response(cacheRes.body, cacheRes); 121 | cacheRes.headers.set("Cache-Control", `max-age=3590, public`); 122 | cacheRes.headers.set("Last-Modified", new Date().toUTCString()); 123 | const cachePromise = caches.default.put(cacheKey, cacheRes); 124 | 125 | if (options.waitUntil) { 126 | options.waitUntil(cachePromise); 127 | } 128 | } 129 | } 130 | 131 | const data = await res.json(); 132 | 133 | if ("id_token" in data) { 134 | const claims = decodeJwt(data.id_token); 135 | return { token: data.id_token, expires: claims.exp as number }; 136 | } 137 | 138 | const lastModified = res.headers.get("last-modified"); 139 | const expires = lastModified 140 | ? Math.floor(new Date(lastModified).valueOf() / 1000) + data.expires_in 141 | : now + data.expires_in; 142 | 143 | return { expires, token: data.access_token }; 144 | })(); 145 | 146 | cache.set(cacheKey, { created: now, promise }); 147 | return await promise.then((data) => data.token); 148 | } 149 | 150 | // #region Types 151 | 152 | type Options = { 153 | /** 154 | * Google Cloud service account credentials. 155 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys 156 | * @default env.GOOGLE_CLOUD_PROJECT 157 | */ 158 | credentials: Credentials | string; 159 | /** 160 | * Authentication scope(s). 161 | */ 162 | scope?: string[] | string; 163 | /** 164 | * Recipients that the ID token should be issued for. 165 | */ 166 | audience?: string[] | string; 167 | env?: { 168 | /** 169 | * Google Cloud project ID. 170 | */ 171 | GOOGLE_CLOUD_PROJECT?: string; 172 | /** 173 | * Google Cloud service account credentials. 174 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys 175 | */ 176 | GOOGLE_CLOUD_CREDENTIALS: string; 177 | }; 178 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 179 | waitUntil?: (promise: Promise) => void; 180 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 181 | cache?: Map; 182 | }; 183 | 184 | type TokenResponse = 185 | | { 186 | access_token: string; 187 | expires_in: number; 188 | token_type: string; 189 | } 190 | | { 191 | id_token: string; 192 | }; 193 | 194 | type CacheValue = { 195 | created: number; 196 | promise: Promise<{ token: string; expires: number }>; 197 | }; 198 | 199 | // #endregion 200 | -------------------------------------------------------------------------------- /google/credentials.test.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import env from "../test/env.js"; 5 | import { 6 | getCredentials, 7 | getPrivateKey, 8 | importPublicKey, 9 | } from "./credentials.js"; 10 | 11 | test("getPrivateKey({ credentials })", async () => { 12 | const privateKey = await getPrivateKey({ 13 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 14 | }); 15 | 16 | expect(privateKey).toEqual( 17 | expect.objectContaining({ 18 | algorithm: expect.objectContaining({ 19 | hash: { name: "SHA-256" }, 20 | modulusLength: 2048, 21 | name: "RSASSA-PKCS1-v1_5", 22 | }), 23 | }) 24 | ); 25 | }); 26 | 27 | test("importPublicKey({ keyId, certificateURL })", async () => { 28 | const credentials = getCredentials(env.GOOGLE_CLOUD_CREDENTIALS); 29 | const privateKey = await importPublicKey({ 30 | keyId: credentials.private_key_id, 31 | certificateURL: credentials.client_x509_cert_url, 32 | }); 33 | 34 | expect(privateKey).toEqual( 35 | expect.objectContaining({ 36 | algorithm: expect.objectContaining({ 37 | hash: { name: "SHA-256" }, 38 | modulusLength: 2048, 39 | name: "RSASSA-PKCS1-v1_5", 40 | }), 41 | }) 42 | ); 43 | }); 44 | -------------------------------------------------------------------------------- /google/credentials.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { importPKCS8, importX509, KeyLike } from "jose"; 5 | import { FetchError } from "../core/error.js"; 6 | 7 | const inFlight = new Map>(); 8 | const cache = new Map(); 9 | 10 | /** 11 | * Normalizes Google Cloud Platform (GCP) service account credentials. 12 | */ 13 | export function getCredentials(credentials: Credentials | string): Credentials { 14 | return typeof credentials === "string" || credentials instanceof String 15 | ? Object.freeze(JSON.parse(credentials as string)) 16 | : Object.isFrozen(credentials) 17 | ? credentials 18 | : Object.freeze(credentials); 19 | } 20 | 21 | /** 22 | * Imports a private key from the provided Google Cloud (GCP) 23 | * service account credentials. 24 | */ 25 | export function getPrivateKey(options: { credentials: Credentials | string }) { 26 | const credentials = getCredentials(options.credentials); 27 | return importPKCS8(credentials.private_key, "RS256"); 28 | } 29 | 30 | /** 31 | * Imports a public key for the provided Google Cloud (GCP) 32 | * service account credentials. 33 | * 34 | * @throws {FetchError} - If the X.509 certificate could not be fetched. 35 | */ 36 | export async function importPublicKey(options: { 37 | /** 38 | * Public key ID (kid). 39 | */ 40 | keyId: string; 41 | /** 42 | * The X.509 certificate URL. 43 | * @default "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com" 44 | */ 45 | certificateURL?: string; 46 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 47 | waitUntil?: (promise: Promise) => void; 48 | }) { 49 | const keyId = options.keyId; 50 | const certificateURL = options.certificateURL ?? "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"; // prettier-ignore 51 | const cacheKey = `${certificateURL}?key=${keyId}`; 52 | const value = cache.get(cacheKey); 53 | const now = Date.now(); 54 | 55 | async function fetchKey() { 56 | // Fetch the public key from Google's servers 57 | const res = await fetch(certificateURL); 58 | 59 | if (!res.ok) { 60 | const error = await res 61 | .json<{ error: { message: string } }>() 62 | .then((data) => data.error.message) 63 | .catch(() => undefined); 64 | throw new FetchError(error ?? "Failed to fetch the public key", { 65 | response: res, 66 | }); 67 | } 68 | 69 | const data = await res.json>(); 70 | const x509 = data[keyId]; 71 | 72 | if (!x509) { 73 | throw new FetchError(`Public key "${keyId}" not found.`, { 74 | response: res, 75 | }); 76 | } 77 | 78 | const key = await importX509(x509, "RS256"); 79 | 80 | // Resolve the expiration time of the key 81 | const maxAge = res.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1]; // prettier-ignore 82 | const expires = Date.now() + Number(maxAge ?? "3600") * 1000; 83 | 84 | // Update the local cache 85 | cache.set(cacheKey, { key, expires }); 86 | inFlight.delete(keyId); 87 | 88 | return key; 89 | } 90 | 91 | // Attempt to read the key from the local cache 92 | if (value) { 93 | if (value.expires > now + 10_000) { 94 | // If the key is about to expire, start a new request in the background 95 | if (value.expires - now < 600_000) { 96 | const promise = fetchKey(); 97 | inFlight.set(cacheKey, promise); 98 | if (options.waitUntil) { 99 | options.waitUntil(promise); 100 | } 101 | } 102 | return value.key; 103 | } else { 104 | cache.delete(cacheKey); 105 | } 106 | } 107 | 108 | // Check if there is an in-flight request for the same key ID 109 | let promise = inFlight.get(cacheKey); 110 | 111 | // If not, start a new request 112 | if (!promise) { 113 | promise = fetchKey(); 114 | inFlight.set(cacheKey, promise); 115 | } 116 | 117 | return await promise; 118 | } 119 | 120 | /** 121 | * Service account credentials for Google Cloud Platform (GCP). 122 | * 123 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys 124 | */ 125 | export type Credentials = { 126 | type: string; 127 | project_id: string; 128 | private_key_id: string; 129 | private_key: string; 130 | client_id: string; 131 | client_email: string; 132 | auth_uri: string; 133 | token_uri: string; 134 | auth_provider_x509_cert_url: string; 135 | client_x509_cert_url: string; 136 | }; 137 | -------------------------------------------------------------------------------- /google/customToken.test.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { decodeJwt } from "jose"; 5 | import env from "../test/env.js"; 6 | import { createCustomToken } from "./customToken.js"; 7 | 8 | test("createCustomToken({ credentials, scope })", async () => { 9 | const customToken = await createCustomToken({ 10 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 11 | scope: "https://www.example.com", 12 | }); 13 | 14 | expect(customToken?.substring(0, 30)).toEqual( 15 | expect.stringContaining("eyJhbGciOi") 16 | ); 17 | 18 | expect(decodeJwt(customToken)).toEqual( 19 | expect.objectContaining({ 20 | iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), 21 | aud: "https://oauth2.googleapis.com/token", 22 | scope: "https://www.example.com", 23 | iat: expect.any(Number), 24 | exp: expect.any(Number), 25 | }) 26 | ); 27 | }); 28 | 29 | test("createCustomToken({ credentials, scope: scopes })", async () => { 30 | const customToken = await createCustomToken({ 31 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 32 | scope: ["https://www.example.com", "https://beta.example.com"], 33 | }); 34 | 35 | expect(customToken?.substring(0, 30)).toEqual( 36 | expect.stringContaining("eyJhbGciOi") 37 | ); 38 | 39 | expect(decodeJwt(customToken)).toEqual( 40 | expect.objectContaining({ 41 | iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), 42 | aud: "https://oauth2.googleapis.com/token", 43 | scope: "https://www.example.com https://beta.example.com", 44 | iat: expect.any(Number), 45 | exp: expect.any(Number), 46 | }) 47 | ); 48 | }); 49 | 50 | test("createCustomToken({ env, scope })", async () => { 51 | const customToken = await createCustomToken({ 52 | scope: "https://www.googleapis.com/auth/cloud-platform", 53 | env: { GOOGLE_CLOUD_CREDENTIALS: env.GOOGLE_CLOUD_CREDENTIALS }, 54 | }); 55 | 56 | expect(customToken?.substring(0, 30)).toEqual( 57 | expect.stringContaining("eyJhbGciOi") 58 | ); 59 | 60 | expect(decodeJwt(customToken)).toEqual( 61 | expect.objectContaining({ 62 | iss: expect.stringMatching(/\.iam\.gserviceaccount\.com$/), 63 | aud: "https://oauth2.googleapis.com/token", 64 | scope: "https://www.googleapis.com/auth/cloud-platform", 65 | iat: expect.any(Number), 66 | exp: expect.any(Number), 67 | }) 68 | ); 69 | }); 70 | 71 | test("createCustomToken({ env, scope })", async () => { 72 | const promise = createCustomToken({ 73 | scope: "https://www.googleapis.com/auth/cloud-platform", 74 | }); 75 | expect(promise).rejects.toThrow(new TypeError("Missing credentials")); 76 | }); 77 | -------------------------------------------------------------------------------- /google/customToken.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { SignJWT } from "jose"; 5 | import { 6 | getCredentials, 7 | getPrivateKey, 8 | type Credentials, 9 | } from "./credentials.js"; 10 | 11 | /** 12 | * Generates a custom authentication token (JWT) 13 | * from a Google Cloud (GCP) service account key. 14 | * 15 | * @example 16 | * const customToken = await createCustomToken({ 17 | * credentials: env.GOOGLE_CLOUD_CREDENTIALS, 18 | * scope: "https://www.googleapis.com/auth/cloud-platform", 19 | * }); 20 | * 21 | * @example 22 | * const customToken = await createCustomToken({ 23 | * env: { GOOGLE_CLOUD_CREDENTIALS: "..." }, 24 | * scope: "https://www.example.com", 25 | * }); 26 | */ 27 | export async function createCustomToken(options: { 28 | /** 29 | * Google Cloud service account credentials. 30 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys 31 | * @default env.GOOGLE_CLOUD_PROJECT 32 | */ 33 | credentials?: Credentials | string; 34 | /** 35 | * Authentication scope. 36 | * @example "https://www.googleapis.com/auth/cloud-platform" 37 | */ 38 | scope?: string | string[]; 39 | /** 40 | * The principal that is the subject of the JWT. 41 | */ 42 | subject?: string; 43 | /** 44 | * The recipient(s) that the JWT is intended for. 45 | */ 46 | audience?: string | string[]; 47 | /** 48 | * Any other JWT clams. 49 | */ 50 | [propName: string]: unknown; 51 | /** 52 | * Alternatively, you can pass credentials via the environment variable. 53 | */ 54 | env?: { 55 | /** 56 | * Google Cloud service account credentials. 57 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys 58 | */ 59 | GOOGLE_CLOUD_CREDENTIALS: string; 60 | }; 61 | }) { 62 | /* eslint-disable-next-line prefer-const */ 63 | let { credentials, scope, subject, audience, env, ...payload } = options; 64 | 65 | // Normalize credentials using env.GOOGLE_CLOUD_CREDENTIALS as a fallback 66 | if (credentials) { 67 | credentials = getCredentials(credentials); 68 | } else { 69 | if (!env?.GOOGLE_CLOUD_CREDENTIALS) { 70 | throw new TypeError("Missing credentials"); 71 | } 72 | credentials = getCredentials(env.GOOGLE_CLOUD_CREDENTIALS); 73 | } 74 | 75 | // Normalize authentication scope (needs to be a string) 76 | scope = Array.isArray(scope) ? scope.join(" ") : scope; 77 | 78 | // Generate and sign a custom JWT token 79 | const privateKey = await getPrivateKey({ credentials }); 80 | const customToken = await new SignJWT({ scope, ...payload }) 81 | .setIssuer(credentials.client_email) 82 | .setAudience(audience ?? credentials.token_uri) 83 | .setSubject(subject ?? credentials.client_email) 84 | .setProtectedHeader({ alg: "RS256" }) 85 | .setIssuedAt() 86 | .setExpirationTime("1h") 87 | .sign(privateKey); 88 | 89 | return customToken; 90 | } 91 | -------------------------------------------------------------------------------- /google/idToken.test.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { decodeJwt } from "jose"; 5 | import env from "../test/env.js"; 6 | import { getIdToken, verifyIdToken } from "./idToken.js"; 7 | 8 | test("getIdToken({ uid, apiKey, projectId, credentials })", async () => { 9 | const token = await getIdToken({ 10 | uid: "temp", 11 | claims: { foo: "bar" }, 12 | apiKey: env.FIREBASE_API_KEY, 13 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 14 | }); 15 | 16 | expect(token).toEqual( 17 | expect.objectContaining({ 18 | kind: "identitytoolkit#VerifyCustomTokenResponse", 19 | idToken: expect.stringMatching(/^eyJhbGciOiJSUzI1NiIs/), 20 | refreshToken: expect.any(String), 21 | expiresIn: "3600", 22 | isNewUser: expect.any(Boolean), 23 | }) 24 | ); 25 | 26 | expect(decodeJwt(token.idToken)).toEqual( 27 | expect.objectContaining({ 28 | sub: "temp", 29 | user_id: "temp", 30 | aud: env.GOOGLE_CLOUD_PROJECT, 31 | iss: `https://securetoken.google.com/${env.GOOGLE_CLOUD_PROJECT}`, 32 | iat: expect.any(Number), 33 | exp: expect.any(Number), 34 | auth_time: expect.any(Number), 35 | }) 36 | ); 37 | }); 38 | 39 | test("verifyIdToken({ idToken })", async () => { 40 | const { idToken } = await getIdToken({ 41 | uid: "temp", 42 | apiKey: env.FIREBASE_API_KEY, 43 | credentials: env.GOOGLE_CLOUD_CREDENTIALS, 44 | }); 45 | const token = await verifyIdToken({ idToken, env }); 46 | 47 | expect(token).toEqual( 48 | expect.objectContaining({ 49 | aud: env.GOOGLE_CLOUD_PROJECT, 50 | iss: `https://securetoken.google.com/${env.GOOGLE_CLOUD_PROJECT}`, 51 | sub: "temp", 52 | user_id: "temp", 53 | iat: expect.any(Number), 54 | exp: expect.any(Number), 55 | }) 56 | ); 57 | }); 58 | -------------------------------------------------------------------------------- /google/idToken.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { decodeProtectedHeader, errors, jwtVerify } from "jose"; 5 | import { canUseDefaultCache } from "../core/env.js"; 6 | import { FetchError } from "../core/error.js"; 7 | import { logOnce } from "../core/utils.js"; 8 | import { Credentials, getCredentials, importPublicKey } from "./credentials.js"; 9 | import { createCustomToken } from "./customToken.js"; 10 | 11 | /** 12 | * Creates a User ID token using Google Cloud service account credentials. 13 | */ 14 | export async function getIdToken(options: { 15 | /** 16 | * User ID. 17 | */ 18 | uid: string; 19 | /** 20 | * Additional user claims. 21 | */ 22 | claims?: Record; 23 | /** 24 | * Google Cloud API key. 25 | * @see https://console.cloud.google.com/apis/credentials 26 | * @default env.FIREBASE_API_KEY 27 | */ 28 | apiKey?: string; 29 | /** 30 | * Google Cloud project ID. 31 | * @default env.GOOGLE_CLOUD_PROJECT; 32 | */ 33 | projectId?: string; 34 | /** 35 | * Google Cloud service account credentials. 36 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys 37 | * @default env.GOOGLE_CLOUD_PROJECT 38 | */ 39 | credentials?: Credentials | string; 40 | /** 41 | * Alternatively, you can pass credentials via the environment variable. 42 | */ 43 | env?: { 44 | /** 45 | * Google Cloud API key. 46 | * @see https://console.cloud.google.com/apis/credentials 47 | */ 48 | FIREBASE_API_KEY: string; 49 | /** 50 | * Google Cloud project ID. 51 | */ 52 | GOOGLE_CLOUD_PROJECT: string; 53 | /** 54 | * Google Cloud service account credentials. 55 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys 56 | */ 57 | GOOGLE_CLOUD_CREDENTIALS: string; 58 | }; 59 | }) { 60 | const uid = options?.uid; 61 | 62 | if (!uid) { 63 | throw new TypeError("Missing uid"); 64 | } 65 | 66 | let apiKey = options?.apiKey; 67 | 68 | if (!apiKey) { 69 | if (options?.env?.FIREBASE_API_KEY) { 70 | apiKey = options.env.FIREBASE_API_KEY; 71 | } else { 72 | throw new TypeError("Missing apiKey"); 73 | } 74 | } 75 | 76 | let credentials = options?.credentials; 77 | 78 | if (credentials) { 79 | credentials = getCredentials(credentials); 80 | } else { 81 | if (options?.env?.GOOGLE_CLOUD_CREDENTIALS) { 82 | credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS); 83 | } else { 84 | throw new TypeError("Missing credentials"); 85 | } 86 | } 87 | 88 | let projectId = options?.projectId; 89 | 90 | if (!projectId && options?.env?.GOOGLE_CLOUD_PROJECT) { 91 | projectId = options.env.GOOGLE_CLOUD_PROJECT; 92 | } 93 | 94 | if (!projectId) { 95 | projectId = credentials.project_id; 96 | } 97 | 98 | if (!projectId) { 99 | throw new TypeError("Missing projectId"); 100 | } 101 | 102 | const customToken = await createCustomToken({ 103 | ...options.claims, 104 | credentials, 105 | audience: 106 | "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit", 107 | uid: options.uid, 108 | }); 109 | 110 | const url = new URL("https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"); // prettier-ignore 111 | url.searchParams.set("key", apiKey); 112 | 113 | const res = await fetch(url, { 114 | method: "POST", 115 | headers: { "Content-Type": "application/json" }, 116 | body: JSON.stringify({ 117 | token: customToken, 118 | returnSecureToken: true, 119 | }), 120 | }); 121 | 122 | if (!res.ok) { 123 | const message = await res 124 | .json<{ error: { message: string } }>() 125 | .then((body) => body?.error?.message) 126 | .catch(() => undefined); 127 | throw new FetchError(message ?? "Failed to verify custom token", { 128 | response: res, 129 | }); 130 | } 131 | 132 | return await res.json(); 133 | } 134 | 135 | /** 136 | * Verifies the authenticity of an ID token issued by Google. 137 | * 138 | * @example 139 | * const token = await verifyIdToken({ 140 | * idToken: "eyJhbGciOiJSUzI1NiIsImtpZC...yXQ" 141 | * projectId: "my-project" 142 | * waitUntil: ctx.waitUntil, 143 | * }); 144 | * 145 | * @example 146 | * const token = await verifyIdToken({ 147 | * idToken: "eyJhbGciOiJSUzI1NiIsImtpZC...yXQ" 148 | * waitUntil: ctx.waitUntil, 149 | * env: { GOOGLE_CLOUD_PROJECT: "my-project" } 150 | * }); 151 | * 152 | * @see https://firebase.google.com/docs/auth/admin/verify-id-tokens 153 | * 154 | * @throws {TypeError} if the ID token is missing 155 | * @throws {FetchError} if unable to fetch the public key 156 | * @throws {JWTClaimValidationFailed} if the token is invalid 157 | * @throws {JWTExpired} if the token has expired 158 | */ 159 | export async function verifyIdToken(options: { 160 | /** 161 | * The ID token to verify. 162 | */ 163 | idToken: string; 164 | /** 165 | * Google Cloud project ID. Set to `null` to disable the check. 166 | * @default env.GOOGLE_CLOUD_PROJECT 167 | */ 168 | projectId?: string | null; 169 | /** 170 | * Alternatively, you can provide the following environment variables: 171 | */ 172 | env?: { 173 | /** 174 | * Google Cloud project ID. 175 | */ 176 | GOOGLE_CLOUD_PROJECT?: string; 177 | /** 178 | * Google Cloud service account credentials. 179 | * @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys 180 | */ 181 | GOOGLE_CLOUD_CREDENTIALS?: string; 182 | }; 183 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 184 | waitUntil?: (promise: Promise) => void; 185 | }): Promise { 186 | if (!options?.idToken) { 187 | throw new TypeError(`Missing "idToken"`); 188 | } 189 | 190 | let projectId = options?.projectId; 191 | 192 | if (projectId === undefined) { 193 | projectId = options?.env?.GOOGLE_CLOUD_PROJECT; 194 | } 195 | 196 | if (projectId === undefined && options?.env?.GOOGLE_CLOUD_CREDENTIALS) { 197 | const credentials = getCredentials(options.env.GOOGLE_CLOUD_CREDENTIALS); 198 | projectId = credentials?.project_id; 199 | } 200 | 201 | if (projectId === undefined) { 202 | throw new TypeError(`Missing "projectId"`); 203 | } 204 | 205 | if (!options.waitUntil && canUseDefaultCache) { 206 | logOnce("warn", "verifyIdToken", "Missing `waitUntil` option."); 207 | } 208 | 209 | // Import the public key from the Google Cloud project 210 | const header = decodeProtectedHeader(options.idToken); 211 | const now = Math.floor(Date.now() / 1000); 212 | const key = await importPublicKey({ 213 | keyId: header.kid as string, 214 | certificateURL: "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com", // prettier-ignore 215 | waitUntil: options.waitUntil, 216 | }); 217 | 218 | const { payload } = await jwtVerify(options.idToken, key, { 219 | audience: projectId == null ? undefined : projectId, 220 | issuer: 221 | projectId == null 222 | ? undefined 223 | : `https://securetoken.google.com/${projectId}`, 224 | maxTokenAge: "1h", 225 | }); 226 | 227 | if (!payload.sub) { 228 | throw new errors.JWTClaimValidationFailed(`Missing "sub" claim`, "sub"); 229 | } 230 | 231 | if (typeof payload.auth_time === "number" && payload.auth_time > now) { 232 | throw new errors.JWTClaimValidationFailed( 233 | `Unexpected "auth_time" claim value`, 234 | "auth_time" 235 | ); 236 | } 237 | 238 | return payload as UserToken; 239 | } 240 | 241 | type VerifyCustomTokenResponse = { 242 | kind: "identitytoolkit#VerifyCustomTokenResponse"; 243 | idToken: string; 244 | refreshToken: string; 245 | expiresIn: string; 246 | isNewUser: boolean; 247 | }; 248 | 249 | export interface UserToken { 250 | /** 251 | * Always set to https://securetoken.google.com/GOOGLE_CLOUD_PROJECT 252 | */ 253 | iss: string; 254 | 255 | /** 256 | * Always set to GOOGLE_CLOUD_PROJECT 257 | */ 258 | aud: string; 259 | 260 | /** 261 | * The user's unique ID 262 | */ 263 | sub: string; 264 | 265 | /** 266 | * The token issue time, in seconds since epoch 267 | */ 268 | iat: number; 269 | 270 | /** 271 | * The token expiry time, normally 'iat' + 3600 272 | */ 273 | exp: number; 274 | 275 | /** 276 | * The user's unique ID. Must be equal to 'sub' 277 | */ 278 | user_id: string; 279 | 280 | /** 281 | * The time the user authenticated, normally 'iat' 282 | */ 283 | auth_time: number; 284 | 285 | /** 286 | * The sign in provider, only set when the provider is 'anonymous' 287 | */ 288 | provider_id?: "anonymous"; 289 | 290 | /** 291 | * The user's primary email 292 | */ 293 | email?: string; 294 | 295 | /** 296 | * The user's email verification status 297 | */ 298 | email_verified?: boolean; 299 | 300 | /** 301 | * The user's primary phone number 302 | */ 303 | phone_number?: string; 304 | 305 | /** 306 | * The user's display name 307 | */ 308 | name?: string; 309 | 310 | /** 311 | * The user's profile photo URL 312 | */ 313 | picture?: string; 314 | 315 | /** 316 | * Information on all identities linked to this user 317 | */ 318 | firebase: { 319 | /** 320 | * The primary sign-in provider 321 | */ 322 | sign_in_provider: SignInProvider; 323 | 324 | /** 325 | * A map of providers to the user's list of unique identifiers from 326 | * each provider 327 | */ 328 | identities?: { [provider in SignInProvider]?: string[] }; 329 | }; 330 | 331 | /** 332 | * Custom claims set by the developer 333 | */ 334 | [claim: string]: unknown; 335 | 336 | /** 337 | * @deprecated use `sub` instead 338 | */ 339 | uid?: never; 340 | } 341 | 342 | export type SignInProvider = 343 | | "custom" 344 | | "email" 345 | | "password" 346 | | "phone" 347 | | "anonymous" 348 | | "google.com" 349 | | "facebook.com" 350 | | "github.com" 351 | | "twitter.com" 352 | | "microsoft.com" 353 | | "apple.com"; 354 | -------------------------------------------------------------------------------- /google/index.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | export * from "./accessToken.js"; 5 | export * from "./credentials.js"; 6 | export * from "./customToken.js"; 7 | export * from "./idToken.js"; 8 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | export * as jwt from "./core/jwt.js"; 5 | export * as google from "./google/index.js"; 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | /** 5 | * Jest configuration 6 | * https://jestjs.io/docs/configuration 7 | * 8 | * @type {import("@jest/types").Config.InitialOptions} 9 | */ 10 | export default { 11 | testEnvironment: "miniflare", 12 | 13 | testPathIgnorePatterns: [ 14 | "/.git/", 15 | "/.yarn/", 16 | "/dist/", 17 | ], 18 | 19 | moduleFileExtensions: [ 20 | "ts", 21 | "js", 22 | "mjs", 23 | "cjs", 24 | "jsx", 25 | "ts", 26 | "tsx", 27 | "json", 28 | "node", 29 | ], 30 | 31 | modulePathIgnorePatterns: ["/dist/"], 32 | 33 | setupFiles: ["/test/setup.ts"], 34 | 35 | transform: { 36 | "\\.ts$": "babel-jest", 37 | }, 38 | 39 | extensionsToTreatAsEsm: [".ts"], 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-auth-library", 3 | "version": "1.0.3", 4 | "packageManager": "yarn@4.0.0-rc.39", 5 | "description": "Authentication library for the browser environment using Web Crypto API", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Kriasoft", 9 | "email": "hello@kriasoft.com", 10 | "url": "https://github.com/kriasoft" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "Konstantin Tarkus", 15 | "email": "hello@tarkus.me", 16 | "url": "https://github.com/koistya" 17 | } 18 | ], 19 | "funding": [ 20 | { 21 | "type": "github", 22 | "url": "https://github.com/sponsors/kriasoft" 23 | }, 24 | { 25 | "type": "patreon", 26 | "url": "https://www.patreon.com/koistya" 27 | } 28 | ], 29 | "repository": "github:kriasoft/web-auth-library", 30 | "keywords": [ 31 | "auth", 32 | "authentication", 33 | "authorization", 34 | "bearer", 35 | "browser", 36 | "bun", 37 | "cloudflare-workers", 38 | "cloudflare", 39 | "crypto", 40 | "decrypt", 41 | "deno", 42 | "encrypt", 43 | "hono", 44 | "jsonwebtoken", 45 | "jwk", 46 | "jwt", 47 | "keys", 48 | "oauth", 49 | "oauth2", 50 | "sign", 51 | "subtlecrypto", 52 | "token", 53 | "typescript", 54 | "web", 55 | "webcrypto" 56 | ], 57 | "files": [ 58 | "dist" 59 | ], 60 | "type": "module", 61 | "exports": { 62 | ".": "./dist/index.js", 63 | "./jwt": "./dist/core/jwt.js", 64 | "./google": { 65 | "types": "./dist/google/index.d.ts", 66 | "import": "./dist/google/index.js", 67 | "default": "./dist/google/index.js" 68 | }, 69 | "./package.json": "./package.json" 70 | }, 71 | "scripts": { 72 | "lint": "eslint --report-unused-disable-directives .", 73 | "test": "node --experimental-vm-modules $(yarn bin jest)", 74 | "build": "rm -rf ./dist && yarn tsc" 75 | }, 76 | "dependencies": { 77 | "jose": ">= 4.12.0 < 5.0.0", 78 | "rfc4648": "^1.5.2" 79 | }, 80 | "devDependencies": { 81 | "@babel/cli": "^7.21.0", 82 | "@babel/core": "^7.21.0", 83 | "@babel/preset-env": "^7.20.2", 84 | "@babel/preset-typescript": "^7.21.0", 85 | "@cloudflare/workers-types": "^4.20230228.0", 86 | "@types/jest": "^29.4.0", 87 | "@typescript-eslint/eslint-plugin": "^5.54.0", 88 | "@typescript-eslint/parser": "^5.54.0", 89 | "babel-jest": "^29.4.3", 90 | "babel-plugin-replace-import-extension": "^1.1.3", 91 | "dotenv": "^16.0.3", 92 | "envalid": "^7.3.1", 93 | "eslint": "^8.35.0", 94 | "jest": "^29.4.3", 95 | "jest-environment-miniflare": "^2.12.1", 96 | "prettier": "^2.8.4", 97 | "typescript": "^4.9.5" 98 | }, 99 | "babel": { 100 | "presets": [ 101 | [ 102 | "@babel/preset-env", 103 | { 104 | "targets": "last 2 Chrome versions", 105 | "modules": false 106 | } 107 | ], 108 | "@babel/preset-typescript" 109 | ], 110 | "plugins": [ 111 | [ 112 | "replace-import-extension", 113 | { 114 | "extMapping": { 115 | ".js": ".ts" 116 | } 117 | } 118 | ] 119 | ] 120 | }, 121 | "eslintConfig": { 122 | "root": true, 123 | "env": { 124 | "browser": true 125 | }, 126 | "parser": "@typescript-eslint/parser", 127 | "plugins": [ 128 | "@typescript-eslint" 129 | ], 130 | "extends": [ 131 | "eslint:recommended", 132 | "plugin:@typescript-eslint/recommended" 133 | ], 134 | "overrides": [ 135 | { 136 | "files": [ 137 | "*.test.ts", 138 | "*.test.js" 139 | ], 140 | "env": { 141 | "jest": true 142 | } 143 | } 144 | ], 145 | "ignorePatterns": [ 146 | "/.yarn/**", 147 | "/dist/**", 148 | "/node_modules/**" 149 | ] 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /test/env.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import { cleanEnv, str } from "envalid"; 5 | 6 | export default cleanEnv(process.env, { 7 | GOOGLE_CLOUD_PROJECT: str(), 8 | GOOGLE_CLOUD_CREDENTIALS: str(), 9 | FIREBASE_API_KEY: str(), 10 | }); 11 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2022-present Kriasoft */ 2 | /* SPDX-License-Identifier: MIT */ 3 | 4 | import dotenv from "dotenv"; 5 | 6 | dotenv.config({ path: "./test/test.override.env" }); 7 | dotenv.config({ path: "./test/test.env" }); 8 | -------------------------------------------------------------------------------- /test/test.env: -------------------------------------------------------------------------------- 1 | # Environment variables to be used for testing 2 | # NOTE: You can override them locally in test.override.env file 3 | 4 | # Google Cloud 5 | # https://cloud.google.com/iam/docs/creating-managing-service-account-keys 6 | GOOGLE_CLOUD_PROJECT=kriasoft 7 | GOOGLE_CLOUD_CREDENTIALS={"type":"service_account","project_id":"example","private_key_id":"25809e59963e2cbbe616cc9dd2feedcc4b620da5","private_key":"-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC407XFlKSwvNbQ\nVnmgZK4zkUOgGARwXpdvTZXvPwCaM1jA+g6WxAv5sIqj9JVEQnwBmre+S5uqJ7tD\nfLlMei7Vbb1FuVLlWfYu6STAkuzU5JC13b+AwRqYDNNtIeNngl9FEnfofHmoCaox\n8r4UAALr2/R3KPoUUGq5Reb70XUwuw2cyLDUiQEwUoLm3HUcIbHFAwIJ/W+s9ghy\n9FBBPAYVUMLudutlb8yIna0qW84rreqBgSGFCmU/R/zeNcNbWIasWBkqgjV661ci\nqXViBuOC9BWeJ0cGIa2JV/IeHrjqROsWo8h2ev1NWjZ7s1LP33ylgGInMgxuDvRI\nfUh8z7nHAgMBAAECggEAMSLfjUKCMhZSCZsjxJdflIgG8XXRIEqVedqnhK48K8KA\n0vTnkf9Wq6/ae9IXKMmADDEkriuNm8PqTfvHi2RkNQtyqSmmtyCeiUQkKConWkXV\njvP/6Gvt9QRb5QSAX1FSoJtTU3RcJ2dCXvsIu2pxXGDichdrvKDQbqb9zG6X+Dce\nmO7lu/xBStR/Q4aD2nC6TF799gSPR3yI+XyfHdBzBaN35RqVfIONByy4VH2ArIYt\npxXdsevDR4HYxV7hciSIehXTDL0x9+zUXRzFslUGY/E1c2fTSJOb7IwAEyeMzB0t\n/6i+aIZrMdVPcwxmVVG90Y0CZE+OGoQq1nm6DjWdqQKBgQDjAFiKFYP4KXijPzfm\n/1Idvht5Ol2Tr2+BQ7trkdkLbLrxcmIyUzVlBMU82uQVhTqCXZAy8eYuzsXMHp9J\nibgg4YqtdX+wIYaO8tELhAEtO3tr/nMsVe8su5RQlhiCTCO4HR0OyOMhnnudYwUn\ncO+rbX3cixOMxADMS/FTWQ/r7QKBgQDQcCF5WmpThSg3boxVy0jJVSB6/8+BTZdy\nqF+wcb2BqJe8OtnHnbXZTakau+f13Jy4ttJYB/moe/trjCbl8ihcDmMGbuAg3Eir\nax2mm4ZetUPVrgZ+Y/wwjA6lO1THWdI8cIQJE4mNEhXeqd3FFK0X445FHdiroBbd\nNAji4H0OAwKBgA9vLpX08IwnBbTTz5E9OvAaxPNxLHumKga3/D5MJF3Kfst744FY\ndwDvWhnRKEDuVhQXGH7eQ7BbDsfaLSpq2sIhk7RHkO8A2I1PpTcLOqlAqhulqV8S\nWLjJ6EOycOgrFSKnmBoxPoBCrlT9LpSH8UPOpgggzKt9iDBb2YS5QYPhAoGAVmJy\nbRHcyRqBjV+ih5gFZXODT5afUC5xGtLPPZgV+xt9L0SQp1skV5gJAoxn2QyCY0dZ\nq6Q6gupHS848/MW8llJcFflzqArDj0+IbVk9ehjTsUY7aLxVc2VIWJBbVXdTWzsi\nbYSMWEvrhmmOALTN+/2SI/D3sEFb2HdNS4HQMjMCgYBuEBqRQNMofnzDgrKN8AwQ\n4/SAVq4ER1lqs4QTixIvFkfeGAH/AzcfsvwYjzi6Pwu8TMxvrmQhrDyvjsrfB204\nU+JhsYJje/wBTWucVEuaWZ/H5gUiFnMV8SytmvfMszRhWX+sG3ucQOLC4J9Enbh5\nsDSQTShw4U1Wd03w55TrZg==\n-----END PRIVATE KEY-----\n","client_email":"example@example.iam.gserviceaccount.com","client_id":"118360562778253889493","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/example%40example.iam.gserviceaccount.com"} 8 | FIREBASE_API_KEY=xxxxx 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ESNext", /* Specify what module code is generated. */ 29 | "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["@cloudflare/workers-types", "jest"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | --------------------------------------------------------------------------------