├── .npmrc ├── .github ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── verify.yml │ └── deploy.yml └── CONTRIBUTING.md ├── .gitignore ├── jest.config.js ├── src ├── constants.ts ├── utils.ts ├── types.ts └── index.ts ├── eslint.config.js ├── tsconfig.json ├── LICENSE ├── tsup.config.ts ├── package.json ├── __test__ ├── index.html └── index.test.ts └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: authorizerdev -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | lib 6 | lib/* 7 | .idea 8 | .history 9 | package-lock.json -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS = 60; 2 | export const CLEANUP_IFRAME_TIMEOUT_IN_SECONDS = 2; 3 | export const AUTHORIZE_IFRAME_TIMEOUT = 5; 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What does this PR do? 2 | 3 | #### Which issue(s) does this PR fix? 4 | 5 | #### If this PR affects any API reference documentation, please share the updated endpoint references 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea or enhancement for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Feature Description** 10 | 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | 17 | **Describe alternatives you've considered** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | branches: [ main, public-package ] 5 | pull_request: 6 | branches: [ main, public-package ] 7 | jobs: 8 | ci: 9 | runs-on: ubuntu-24.04 10 | strategy: 11 | matrix: 12 | node-version: [20.x, 22.x, 24.x] 13 | steps: 14 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install 20 | 21 | - name: test 22 | run: npm test 23 | 24 | - name: size 25 | run: npm run size 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [ public-package ] 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-24.04 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 14 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 15 | with: 16 | node-version: '24.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Update npm 19 | run: npm install -g npm@latest # ensure npm 11.5.1 or later 20 | - run: npm install 21 | - name: Verify 22 | run: npm test 23 | - name: Publish 24 | if: ${{ success() }} 25 | run: npm publish --access public -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const typescriptParser = require('@typescript-eslint/parser'); 2 | const typescriptPlugin = require('@typescript-eslint/eslint-plugin'); 3 | 4 | module.exports = [{ 5 | name: 'global', 6 | ignores: [ 7 | 'lib/', 8 | 'node_modules/', 9 | 'jest.config.js', 10 | 'tsconfig.json', 11 | 'tsup.config.ts', 12 | 'package.json' 13 | ] 14 | }, { 15 | name: 'ts', 16 | files: ["*.ts", "**/*.ts"], 17 | plugins: { 18 | "@typescript-eslint": typescriptPlugin 19 | }, 20 | languageOptions: { 21 | parser: typescriptParser, 22 | parserOptions: { 23 | project: "./tsconfig.json", 24 | sourceType: "module", 25 | ecmaVersion: 2020 26 | } 27 | }, 28 | rules: { 29 | 'semi': ['error', 'always'], 30 | 'quotes': ['error', 'single'], 31 | 'indent': ['error', 2], 32 | '@typescript-eslint/no-explicit-any': 0 33 | } 34 | }]; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es6", 5 | "allowUmdGlobalAccess": true, 6 | "module": "esnext", 7 | "lib": ["dom", "es2015", "es2017", "esnext.asynciterable"], 8 | "sourceMap": true, 9 | "outDir": "lib", 10 | "moduleResolution": "node", 11 | "removeComments": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "noImplicitThis": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "allowSyntheticDefaultImports": true, 21 | "esModuleInterop": true, 22 | "emitDecoratorMetadata": true, 23 | "experimentalDecorators": true, 24 | "resolveJsonModule": true, 25 | "baseUrl": "." 26 | }, 27 | "exclude": ["node_modules", "lib"], 28 | "include": ["./src/**/*.ts", "./__test__/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Version:** x.y.z 10 | 11 | 12 | 13 | **Describe the bug** 14 | 15 | 16 | 17 | **Steps To Reproduce** 18 | 19 | 20 | 21 | **Expected behavior** 22 | 23 | 24 | 25 | **Screenshots** 26 | 27 | 28 | 29 | **Desktop (please complete the following information):** 30 | 31 | - OS: [e.g. iOS] 32 | - Browser [e.g. chrome, safari] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lakhan Samani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | import pkg from './package.json'; 3 | const external = [...Object.keys(pkg.dependencies || {})]; 4 | 5 | export default defineConfig(() => [ 6 | { 7 | entryPoints: ['src/index.ts'], 8 | outDir: 'lib', 9 | target: 'node20', 10 | format: ['esm', 'cjs'], 11 | clean: true, 12 | dts: true, 13 | minify: true, 14 | external, 15 | }, 16 | { 17 | entry: { bundle: 'src/index.ts' }, 18 | outDir: 'lib', 19 | format: ['iife'], 20 | globalName: 'authorizerdev', 21 | clean: false, 22 | minify: true, 23 | platform: 'browser', 24 | dts: false, 25 | name: 'authorizer', 26 | // esbuild `globalName` option generates `var authorizerdev = (() => {})()` 27 | // and var is not guaranteed to assign to the global `window` object so we make sure to assign it 28 | footer: { 29 | js: 'window.__TAURI__ = authorizerdev', 30 | }, 31 | outExtension() { 32 | return { 33 | js: '.min.js', 34 | }; 35 | }, 36 | esbuildOptions(options) { 37 | options.entryNames = 'authorizer'; 38 | }, 39 | }, 40 | ]); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@localnerve/authorizer-js", 3 | "version": "1.0.5", 4 | "author": "Lakhan Samani", 5 | "license": "MIT", 6 | "funding": "https://github.com/sponsors/authorizerdev", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/localnerve/authorizer-js.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/localnerve/authorizer-js/issues" 13 | }, 14 | "exports": { 15 | ".": { 16 | "types": "./lib/index.d.ts", 17 | "require": "./lib/index.js", 18 | "import": "./lib/index.mjs" 19 | } 20 | }, 21 | "main": "lib/index.js", 22 | "module": "lib/index.mjs", 23 | "unpkg": "lib/authorizer.min.js", 24 | "types": "lib/index.d.ts", 25 | "files": [ 26 | "lib" 27 | ], 28 | "engines": { 29 | "node": ">=20" 30 | }, 31 | "size-limit": [ 32 | { 33 | "path": "lib/authorizer.min.js", 34 | "limit": "4 kB" 35 | } 36 | ], 37 | "scripts": { 38 | "ts-types": "tsc --emitDeclarationOnly --outDir lib", 39 | "build": "tsup", 40 | "test": "npm run build && jest --testTimeout=500000 --runInBand", 41 | "prepublishOnly": "npm run build", 42 | "lint": " eslint .", 43 | "size": "size-limit" 44 | }, 45 | "browser": { 46 | "path": "path-browserify" 47 | }, 48 | "devDependencies": { 49 | "@size-limit/preset-small-lib": "^12.0.0", 50 | "@types/jest": "^30.0.0", 51 | "@types/node": "^25.0.0", 52 | "@typescript-eslint/eslint-plugin": "^8.49.0", 53 | "@typescript-eslint/parser": "^8.49.0", 54 | "eslint": "^9.39.1", 55 | "jest": "^30.2.0", 56 | "size-limit": "^12.0.0", 57 | "testcontainers": "^11.10.0", 58 | "ts-jest": "^29.4.6", 59 | "tslib": "^2.8.1", 60 | "tsup": "^8.5.1", 61 | "typescript": "^5.9.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We're so excited you're interested in helping with Authorizer! We are happy to help you get started, even if you don't have any previous open-source experience :blush: 4 | 5 | ## New to Open Source? 6 | 7 | 1. Take a look at [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) 8 | 2. Go through the [Authorizer Code of Conduct](https://github.com/authorizerdev/authorizer-js/blob/main/.github/CODE_OF_CONDUCT.md) 9 | 10 | ## Where to ask questions? 11 | 12 | 1. Check our [Github Issues](https://github.com/authorizerdev/authorizer-js/issues) to see if someone has already answered your question. 13 | 2. Join our community on [Discord](https://discord.gg/Zv2D5h6kkK) and feel free to ask us your questions 14 | 15 | As you gain experience with Authorizer, please help answer other people's questions! :pray: 16 | 17 | ## What to work on? 18 | 19 | You can get started by taking a look at our [Github issues](https://github.com/authorizerdev/authorizer-js/issues) 20 | If you find one that looks interesting and no one else is already working on it, comment on that issue and start contributing 🙂. 21 | 22 | Please ask as many questions as you need, either directly in the issue or on [Discord](https://discord.gg/Zv2D5h6kkK). We're happy to help!:raised_hands: 23 | 24 | ### Contributions that are ALWAYS welcome 25 | 26 | 1. More tests 27 | 2. Improved Docs 28 | 3. Improved error messages 29 | 4. Educational content like blogs, videos, courses 30 | 31 | ## Development Setup 32 | 33 | ### Prerequisites 34 | 35 | - OS: Linux or macOS or windows 36 | - Go: (Golang)(https://golang.org/dl/) >= v1.15 37 | 38 | ### Familiarize yourself with Authorizer 39 | 40 | 1. [Architecture of Authorizer](http://docs.authorizer.dev/) 41 | 2. [authorizer-js](https://docs.authorizer.dev/authorizer-js/getting-started/) 42 | 43 | ### Project Setup for Authorizer JS 44 | 45 | 1. Fork the [authorizer](https://github.com/authorizerdev/authorizer-js) repository (**Skip this step if you have access to repo**) 46 | 2. Clone the repo `git clone https://github.com/authorizerdev/authorizer-js.git` 47 | 3. Change directory `cd authorizer-js` 48 | 4. Install dependencies `npm i` or `yarn` 49 | 5. To test locally you need local `authorizer-instance` 50 | 6. Watch mode for local development: `npm start` 51 | 7. Build the source code: `npm run build` 52 | 8. Test: `npm run test` 53 | -------------------------------------------------------------------------------- /__test__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My Blog 8 | 31 | 32 | 33 | 39 |
40 |

Hello World 👋

41 |

42 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 43 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 44 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 45 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 46 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 47 | cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 48 | est laborum. 49 |

50 |
51 |
52 |

Foo Bar!

53 |

54 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 55 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 56 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 57 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 58 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 59 | cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 60 | est laborum. 61 |

62 | 63 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authorizer.js 2 | 3 | > This is a fork of @authorizerdev/authorizer-js that can be built with modern es bundlers (no cross-fetch), has fewer dependencies, and easier maintain 4 | 5 | [`@authorizerdev/authorizer-js`](https://www.npmjs.com/package/@authorizerdev/authorizer-js) is universal javaScript SDK for Authorizer API. 6 | It supports: 7 | 8 | - [UMD (Universal Module Definition)](https://github.com/umdjs/umd) build for browsers 9 | - [CommonJS(cjs)](https://flaviocopes.com/commonjs/) build for NodeJS version that don't support ES Modules 10 | - [ESM (ES Modules)](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/) build for modern javascript standard, i.e. ES Modules 11 | 12 | # Migration Guide from 1.x -> 2.x 13 | 14 | `2.x` version of `@authorizerdev/authorizer-js` has a uniform response structure that will help your applications to get right error codes and success response. Methods here have `{data, errors}` as response objects for methods of this library. 15 | 16 | For `1.x` version of this library you can get only data in response and error would be thrown so you had to handle that in catch. 17 | 18 | --- 19 | 20 | All the above versions require `Authorizer` instance to be instantiated and used. Instance constructor requires an object with the following keys 21 | 22 | | Key | Description | 23 | | --------------- | ---------------------------------------------------------------------------- | 24 | | `authorizerURL` | Authorizer server endpoint | 25 | | `redirectURL` | URL to which you would like to redirect the user in case of successful login | 26 | 27 | **Example** 28 | 29 | ```js 30 | const authRef = new Authorizer({ 31 | authorizerURL: 'https://app.herokuapp.com', 32 | redirectURL: window.location.origin, 33 | }); 34 | ``` 35 | 36 | ## IIFE 37 | 38 | - Step 1: Load Javascript using CDN 39 | 40 | ```html 41 | 42 | ``` 43 | 44 | - Step 2: Use the library to instantiate `Authorizer` instance and access [various methods](/authorizer-js/functions) 45 | 46 | ```html 47 | 78 | ``` 79 | 80 | ## CommonJS 81 | 82 | - Step 1: Install dependencies 83 | 84 | ```sh 85 | npm i --save @authorizerdev/authorizer-js 86 | OR 87 | yarn add @authorizerdev/authoirzer-js 88 | ``` 89 | 90 | - Step 2: Import and initialize the authorizer instance 91 | 92 | ```js 93 | const { Authorizer } = require('@authorizerdev/authoirzer-js'); 94 | 95 | const authRef = new Authorizer({ 96 | authorizerURL: 'https://app.heroku.com', 97 | redirectURL: 'http://app.heroku.com/app', 98 | }); 99 | 100 | async function main() { 101 | await authRef.login({ 102 | email: 'foo@bar.com', 103 | password: 'test', 104 | }); 105 | } 106 | ``` 107 | 108 | ## ES Modules 109 | 110 | - Step 1: Install dependencies 111 | 112 | ```sh 113 | npm i --save @authorizerdev/authorizer-js 114 | OR 115 | yarn add @authorizerdev/authorizer-js 116 | ``` 117 | 118 | - Step 2: Import and initialize the authorizer instance 119 | 120 | ```js 121 | import { Authorizer } from '@authorizerdev/authorizer-js'; 122 | 123 | const authRef = new Authorizer({ 124 | authorizerURL: 'https://app.heroku.com', 125 | redirectURL: 'http://app.heroku.com/app', 126 | }); 127 | 128 | async function main() { 129 | await authRef.login({ 130 | email: 'foo@bar.com', 131 | password: 'test', 132 | }); 133 | } 134 | ``` 135 | 136 | ## Local Development Setup 137 | 138 | ### Prerequisites 139 | 140 | - [Pnpm](https://pnpm.io/installation) 141 | - [NodeJS](https://nodejs.org/en/download/) 142 | 143 | ### Setup 144 | 145 | - Clone the repository 146 | - Install dependencies using `pnpm install` 147 | - Run `pnpm build` to build the library 148 | - Run `pnpm test` to run the tests 149 | 150 | ### Release 151 | 152 | - Run `pnpm release` to release a new version of the library 153 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CLEANUP_IFRAME_TIMEOUT_IN_SECONDS, 3 | DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS, 4 | } from './constants'; 5 | import { AuthorizeResponse } from './types'; 6 | 7 | export const hasWindow = (): boolean => typeof window !== 'undefined'; 8 | 9 | export const trimURL = (url: string): string => { 10 | let trimmedData = url.trim(); 11 | const lastChar = trimmedData[trimmedData.length - 1]; 12 | if (lastChar === '/') 13 | trimmedData = trimmedData.slice(0, -1); 14 | 15 | return trimmedData; 16 | }; 17 | 18 | export const getCrypto = () => { 19 | // ie 11.x uses msCrypto 20 | return hasWindow() 21 | ? ((window.crypto || (window as any).msCrypto) as Crypto) 22 | : null; 23 | }; 24 | 25 | export const getCryptoSubtle = () => { 26 | const crypto = getCrypto(); 27 | // safari 10.x uses webkitSubtle 28 | return (crypto && crypto.subtle) || (crypto as any).webkitSubtle; 29 | }; 30 | 31 | export const createRandomString = () => { 32 | const charset 33 | = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.'; 34 | let random = ''; 35 | const crypto = getCrypto(); 36 | if (crypto) { 37 | const randomValues = Array.from(crypto.getRandomValues(new Uint8Array(43))); 38 | randomValues.forEach(v => (random += charset[v % charset.length])); 39 | } 40 | return random; 41 | }; 42 | 43 | export const encode = (value: string) => 44 | hasWindow() ? btoa(value) : Buffer.from(value).toString('base64'); 45 | export const decode = (value: string) => 46 | hasWindow() ? atob(value) : Buffer.from(value, 'base64').toString('ascii'); 47 | 48 | export const createQueryParams = (params: any) => { 49 | return Object.keys(params) 50 | .filter(k => typeof params[k] !== 'undefined') 51 | .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) 52 | .join('&'); 53 | }; 54 | 55 | export const sha256 = async (s: string) => { 56 | const digestOp: any = getCryptoSubtle().digest( 57 | { name: 'SHA-256' }, 58 | new TextEncoder().encode(s), 59 | ); 60 | 61 | // msCrypto (IE11) uses the old spec, which is not Promise based 62 | // https://msdn.microsoft.com/en-us/expression/dn904640(v=vs.71) 63 | if ((window as any).msCrypto) { 64 | return new Promise((resolve, reject) => { 65 | digestOp.oncomplete = (e: any) => { 66 | resolve(e.target.result); 67 | }; 68 | 69 | digestOp.onerror = (e: ErrorEvent) => { 70 | reject(e.error); 71 | }; 72 | 73 | digestOp.onabort = () => { 74 | reject(new Error('The digest operation was aborted')); 75 | }; 76 | }); 77 | } 78 | 79 | return await digestOp; 80 | }; 81 | 82 | const urlEncodeB64 = (input: string) => { 83 | const b64Chars: { [index: string]: string } = { '+': '-', '/': '_', '=': '' }; 84 | return input.replace(/[+/=]/g, (m: string) => b64Chars[m]); 85 | }; 86 | 87 | // https://stackoverflow.com/questions/30106476/ 88 | const decodeB64 = (input: string) => 89 | decodeURIComponent( 90 | atob(input) 91 | .split('') 92 | .map((c) => { 93 | return `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`; 94 | }) 95 | .join(''), 96 | ); 97 | 98 | export const urlDecodeB64 = (input: string) => 99 | decodeB64(input.replace(/_/g, '/').replace(/-/g, '+')); 100 | 101 | export const bufferToBase64UrlEncoded = (input: number[] | Uint8Array) => { 102 | const ie11SafeInput = new Uint8Array(input); 103 | return urlEncodeB64( 104 | window.btoa(String.fromCharCode(...Array.from(ie11SafeInput))), 105 | ); 106 | }; 107 | 108 | export const executeIframe = ( 109 | authorizeUrl: string, 110 | eventOrigin: string, 111 | timeoutInSeconds: number = DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS, 112 | ) => { 113 | return new Promise((resolve, reject) => { 114 | const iframe = window.document.createElement('iframe'); 115 | iframe.setAttribute('id', 'authorizer-iframe'); 116 | iframe.setAttribute('width', '0'); 117 | iframe.setAttribute('height', '0'); 118 | iframe.style.display = 'none'; 119 | const removeIframe = () => { 120 | if (window.document.body.contains(iframe)) { 121 | window.document.body.removeChild(iframe); 122 | window.removeEventListener('message', iframeEventHandler, false); 123 | } 124 | }; 125 | 126 | const timeoutSetTimeoutId = setTimeout(() => { 127 | removeIframe(); 128 | }, timeoutInSeconds * 1000); 129 | 130 | const iframeEventHandler: (e: MessageEvent) => void = function (e: MessageEvent) { 131 | if (e.origin !== eventOrigin) 132 | return; 133 | if (!e.data || !e.data.response) 134 | return; 135 | 136 | const eventSource = e.source; 137 | 138 | if (eventSource) 139 | (eventSource as any).close(); 140 | 141 | e.data.response.error 142 | ? reject(e.data.response) 143 | : resolve(e.data.response); 144 | 145 | clearTimeout(timeoutSetTimeoutId); 146 | window.removeEventListener('message', iframeEventHandler, false); 147 | setTimeout(removeIframe, CLEANUP_IFRAME_TIMEOUT_IN_SECONDS * 1000); 148 | }; 149 | 150 | window.addEventListener('message', iframeEventHandler, false); 151 | window.document.body.appendChild(iframe); 152 | iframe.setAttribute('src', authorizeUrl); 153 | }); 154 | }; 155 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface GrapQlResponseType { 2 | data: any | undefined; 3 | errors: Error[]; 4 | } 5 | export interface ApiResponse { 6 | errors: Error[]; 7 | data: T | undefined; 8 | } 9 | export interface ConfigType { 10 | authorizerURL: string; 11 | redirectURL: string; 12 | clientID?: string; 13 | extraHeaders?: Record; 14 | } 15 | 16 | export interface User { 17 | id: string; 18 | email: string; 19 | preferred_username: string; 20 | email_verified: boolean; 21 | signup_methods: string; 22 | given_name?: string | null; 23 | family_name?: string | null; 24 | middle_name?: string | null; 25 | nickname?: string | null; 26 | picture?: string | null; 27 | gender?: string | null; 28 | birthdate?: string | null; 29 | phone_number?: string | null; 30 | phone_number_verified?: boolean | null; 31 | roles?: string[]; 32 | created_at: number; 33 | updated_at: number; 34 | is_multi_factor_auth_enabled?: boolean; 35 | app_data?: Record; 36 | } 37 | 38 | export interface AuthToken { 39 | message?: string; 40 | access_token: string; 41 | expires_in: number; 42 | id_token: string; 43 | refresh_token?: string; 44 | user?: User; 45 | should_show_email_otp_screen?: boolean; 46 | should_show_mobile_otp_screen?: boolean; 47 | should_show_totp_screen?: boolean; 48 | authenticator_scanner_image?: string; 49 | authenticator_secret?: string; 50 | authenticator_recovery_codes?: string[]; 51 | } 52 | 53 | export interface GenericResponse { 54 | message: string; 55 | } 56 | 57 | export type Headers = Record; 58 | 59 | export interface LoginInput { 60 | email?: string; 61 | phone_number?: string; 62 | password: string; 63 | roles?: string[]; 64 | scope?: string[]; 65 | state?: string; 66 | } 67 | 68 | export interface SignupInput { 69 | email?: string; 70 | password: string; 71 | confirm_password: string; 72 | given_name?: string; 73 | family_name?: string; 74 | middle_name?: string; 75 | nickname?: string; 76 | picture?: string; 77 | gender?: string; 78 | birthdate?: string; 79 | phone_number?: string; 80 | roles?: string[]; 81 | scope?: string[]; 82 | redirect_uri?: string; 83 | is_multi_factor_auth_enabled?: boolean; 84 | state?: string; 85 | app_data?: Record; 86 | } 87 | 88 | export interface MagicLinkLoginInput { 89 | email: string; 90 | roles?: string[]; 91 | scopes?: string[]; 92 | state?: string; 93 | redirect_uri?: string; 94 | } 95 | 96 | export interface VerifyEmailInput { 97 | token: string; 98 | state?: string; 99 | } 100 | 101 | export interface ResendVerifyEmailInput { 102 | email: string; 103 | identifier: string; 104 | } 105 | 106 | export interface VerifyOtpInput { 107 | email?: string; 108 | phone_number?: string; 109 | otp: string; 110 | state?: string; 111 | is_totp?: boolean; 112 | } 113 | 114 | export interface ResendOtpInput { 115 | email?: string; 116 | phone_number?: string; 117 | } 118 | 119 | export interface GraphqlQueryInput { 120 | query: string; 121 | variables?: Record; 122 | headers?: Headers; 123 | } 124 | 125 | export interface MetaData { 126 | version: string; 127 | client_id: string; 128 | is_google_login_enabled: boolean; 129 | is_facebook_login_enabled: boolean; 130 | is_github_login_enabled: boolean; 131 | is_linkedin_login_enabled: boolean; 132 | is_apple_login_enabled: boolean; 133 | is_twitter_login_enabled: boolean; 134 | is_microsoft_login_enabled: boolean; 135 | is_twitch_login_enabled: boolean; 136 | is_roblox_login_enabled: boolean; 137 | is_email_verification_enabled: boolean; 138 | is_basic_authentication_enabled: boolean; 139 | is_magic_link_login_enabled: boolean; 140 | is_sign_up_enabled: boolean; 141 | is_strong_password_enabled: boolean; 142 | is_multi_factor_auth_enabled: boolean; 143 | is_mobile_basic_authentication_enabled: boolean; 144 | is_phone_verification_enabled: boolean; 145 | } 146 | 147 | export interface UpdateProfileInput { 148 | old_password?: string; 149 | new_password?: string; 150 | confirm_new_password?: string; 151 | email?: string; 152 | given_name?: string; 153 | family_name?: string; 154 | middle_name?: string; 155 | nickname?: string; 156 | gender?: string; 157 | birthdate?: string; 158 | phone_number?: string; 159 | picture?: string; 160 | is_multi_factor_auth_enabled?: boolean; 161 | app_data?: Record; 162 | } 163 | 164 | export interface ForgotPasswordInput { 165 | email?: string; 166 | phone_number?: string; 167 | state?: string; 168 | redirect_uri?: string; 169 | } 170 | 171 | export interface ForgotPasswordResponse { 172 | message: string; 173 | should_show_mobile_otp_screen?: boolean; 174 | } 175 | 176 | export interface ResetPasswordInput { 177 | token?: string; 178 | otp?: string; 179 | phone_number?: string; 180 | password: string; 181 | confirm_password: string; 182 | } 183 | 184 | export interface SessionQueryInput { 185 | roles?: string[]; 186 | } 187 | 188 | export interface IsValidJWTQueryInput { 189 | jwt: string; 190 | roles?: string[]; 191 | } 192 | 193 | export interface ValidJWTResponse { 194 | valid: string; 195 | message: string; 196 | } 197 | 198 | export enum OAuthProviders { 199 | Apple = 'apple', 200 | Github = 'github', 201 | Google = 'google', 202 | Facebook = 'facebook', 203 | LinkedIn = 'linkedin', 204 | Twitter = 'twitter', 205 | Microsoft = 'microsoft', 206 | Twitch = 'twitch', 207 | Roblox = 'roblox', 208 | } 209 | 210 | export enum ResponseTypes { 211 | Code = 'code', 212 | Token = 'token', 213 | } 214 | 215 | export interface AuthorizeInput { 216 | response_type: ResponseTypes; 217 | use_refresh_token?: boolean; 218 | response_mode?: string; 219 | } 220 | 221 | export interface AuthorizeResponse { 222 | state: string; 223 | code?: string; 224 | error?: string; 225 | error_description?: string; 226 | } 227 | 228 | export interface RevokeTokenInput { 229 | refresh_token: string; 230 | } 231 | 232 | export interface GetTokenInput { 233 | code?: string; 234 | grant_type?: string; 235 | refresh_token?: string; 236 | } 237 | 238 | export interface GetTokenResponse { 239 | access_token: string; 240 | expires_in: number; 241 | id_token: string; 242 | refresh_token?: string; 243 | } 244 | 245 | export interface ValidateJWTTokenInput { 246 | token_type: 'access_token' | 'id_token' | 'refresh_token'; 247 | token: string; 248 | roles?: string[]; 249 | } 250 | 251 | export interface ValidateJWTTokenResponse { 252 | is_valid: boolean; 253 | claims: Record; 254 | } 255 | 256 | export interface ValidateSessionInput { 257 | cookie?: string; 258 | roles?: string[]; 259 | } 260 | 261 | export interface ValidateSessionResponse { 262 | is_valid: boolean; 263 | user: User; 264 | } 265 | -------------------------------------------------------------------------------- /__test__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { GenericContainer, StartedTestContainer, Wait } from 'testcontainers'; 2 | import { ApiResponse, AuthToken, Authorizer } from '../lib'; 3 | 4 | const authorizerConfig = { 5 | authorizerURL: 'http://localhost:8080', 6 | redirectURL: 'http://localhost:8080/app', 7 | // clientID: '3fab5e58-5693-46f2-8123-83db8133cd22', 8 | adminSecret: 'secret', 9 | }; 10 | 11 | const testConfig = { 12 | email: 'test@test.com', 13 | webHookId: '', 14 | webHookUrl: 'https://webhook.site/c28a87c1-f061-44e0-9f7a-508bc554576f', 15 | userId: '', 16 | emailTemplateId: '', 17 | password: 'Test@123#', 18 | maginLinkLoginEmail: 'test_magic_link@test.com', 19 | }; 20 | 21 | // Using etheral.email for email sink: https://ethereal.email/create 22 | const authorizerENV = { 23 | ENV: 'production', 24 | DATABASE_URL: 'data.db', 25 | DATABASE_TYPE: 'sqlite', 26 | CUSTOM_ACCESS_TOKEN_SCRIPT: 27 | 'function(user,tokenPayload){var data = tokenPayload;data.extra = {\'x-extra-id\': user.id};return data;}', 28 | DISABLE_PLAYGROUND: 'true', 29 | SMTP_HOST: 'smtp.ethereal.email', 30 | SMTP_PASSWORD: 'WncNxwVFqb6nBjKDQJ', 31 | SMTP_USERNAME: 'sydnee.lesch77@ethereal.email', 32 | LOG_LELVEL: 'debug', 33 | SMTP_PORT: '587', 34 | SENDER_EMAIL: 'test@authorizer.dev', 35 | ADMIN_SECRET: 'secret', 36 | }; 37 | 38 | // const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 39 | 40 | const verificationRequests = 41 | 'query {_verification_requests { verification_requests { id token email expires identifier } } }'; 42 | 43 | describe('Integration Tests - authorizer-js', () => { 44 | let container: StartedTestContainer; 45 | 46 | let authorizer: Authorizer; 47 | 48 | beforeAll(async () => { 49 | container = await new GenericContainer('lakhansamani/authorizer:latest') 50 | .withEnvironment(authorizerENV) 51 | .withExposedPorts(8080) 52 | .withWaitStrategy(Wait.forHttp('/health', 8080).forStatusCode(200)) 53 | .start(); 54 | 55 | authorizerConfig.authorizerURL = `http://${container.getHost()}:${container.getMappedPort( 56 | 8080, 57 | )}`; 58 | authorizerConfig.redirectURL = `http://${container.getHost()}:${container.getMappedPort( 59 | 8080, 60 | )}/app`; 61 | console.log('Authorizer URL:', authorizerConfig.authorizerURL); 62 | authorizer = new Authorizer(authorizerConfig); 63 | // get metadata 64 | const metadataRes = await authorizer.getMetaData(); 65 | // await sleep(50000); 66 | expect(metadataRes?.data).toBeDefined(); 67 | if (metadataRes?.data?.client_id) { 68 | authorizer.config.clientID = metadataRes?.data?.client_id; 69 | } 70 | }); 71 | 72 | afterAll(async () => { 73 | await container.stop(); 74 | }); 75 | 76 | it('should signup with email verification enabled', async () => { 77 | const signupRes = await authorizer.signup({ 78 | email: testConfig.email, 79 | password: testConfig.password, 80 | confirm_password: testConfig.password, 81 | }); 82 | expect(signupRes?.data).toBeDefined(); 83 | expect(signupRes?.errors).toHaveLength(0); 84 | expect(signupRes?.data?.message?.length).not.toEqual(0); 85 | }); 86 | 87 | it('should verify email', async () => { 88 | const verificationRequestsRes = await authorizer.graphqlQuery({ 89 | query: verificationRequests, 90 | variables: {}, 91 | headers: { 92 | 'x-authorizer-admin-secret': authorizerConfig.adminSecret, 93 | }, 94 | }); 95 | const requests = 96 | verificationRequestsRes?.data?._verification_requests 97 | .verification_requests; 98 | expect(verificationRequestsRes?.data).toBeDefined(); 99 | expect(verificationRequestsRes?.errors).toHaveLength(0); 100 | const item = requests.find( 101 | (i: { email: string }) => i.email === testConfig.email, 102 | ); 103 | expect(item).not.toBeNull(); 104 | 105 | const verifyEmailRes = await authorizer.verifyEmail({ token: item.token }); 106 | expect(verifyEmailRes?.data).toBeDefined(); 107 | expect(verifyEmailRes?.errors).toHaveLength(0); 108 | expect(verifyEmailRes?.data?.access_token?.length).toBeGreaterThan(0); 109 | }); 110 | 111 | let loginRes: ApiResponse | null; 112 | it('should log in successfully', async () => { 113 | loginRes = await authorizer.login({ 114 | email: testConfig.email, 115 | password: testConfig.password, 116 | scope: ['openid', 'profile', 'email', 'offline_access'], 117 | }); 118 | expect(loginRes?.data).toBeDefined(); 119 | expect(loginRes?.errors).toHaveLength(0); 120 | expect(loginRes?.data?.access_token.length).not.toEqual(0); 121 | expect(loginRes?.data?.refresh_token?.length).not.toEqual(0); 122 | expect(loginRes?.data?.expires_in).not.toEqual(0); 123 | expect(loginRes?.data?.id_token.length).not.toEqual(0); 124 | }); 125 | 126 | it('should validate jwt token', async () => { 127 | const validateRes = await authorizer.validateJWTToken({ 128 | token_type: 'access_token', 129 | token: loginRes?.data?.access_token || '', 130 | }); 131 | expect(validateRes?.data).toBeDefined(); 132 | expect(validateRes?.errors).toHaveLength(0); 133 | expect(validateRes?.data?.is_valid).toEqual(true); 134 | }); 135 | 136 | it('should update profile successfully', async () => { 137 | const updateProfileRes = await authorizer.updateProfile( 138 | { 139 | given_name: 'bob', 140 | }, 141 | { 142 | Authorization: `Bearer ${loginRes?.data?.access_token}`, 143 | }, 144 | ); 145 | expect(updateProfileRes?.data).toBeDefined(); 146 | expect(updateProfileRes?.errors).toHaveLength(0); 147 | }); 148 | 149 | it('should fetch profile successfully', async () => { 150 | const profileRes = await authorizer.getProfile({ 151 | Authorization: `Bearer ${loginRes?.data?.access_token}`, 152 | }); 153 | expect(profileRes?.data).toBeDefined(); 154 | expect(profileRes?.errors).toHaveLength(0); 155 | expect(profileRes?.data?.given_name).toMatch('bob'); 156 | }); 157 | 158 | it('should get access_token using refresh_token', async () => { 159 | const tokenRes = await authorizer.getToken({ 160 | grant_type: 'refresh_token', 161 | refresh_token: loginRes?.data?.refresh_token, 162 | }); 163 | expect(tokenRes?.data).toBeDefined(); 164 | expect(tokenRes?.errors).toHaveLength(0); 165 | expect(tokenRes?.data?.access_token.length).not.toEqual(0); 166 | if (loginRes && loginRes.data) { 167 | loginRes.data.access_token = tokenRes?.data?.access_token || ''; 168 | } 169 | }); 170 | 171 | it('should deactivate account', async () => { 172 | const deactivateRes = await authorizer.deactivateAccount({ 173 | Authorization: `Bearer ${loginRes?.data?.access_token}`, 174 | }); 175 | expect(deactivateRes?.data).toBeDefined(); 176 | expect(deactivateRes?.errors).toHaveLength(0); 177 | }); 178 | 179 | it('should throw error while accessing profile after deactivation', async () => { 180 | const resp = await authorizer.getProfile({ 181 | Authorization: `Bearer ${loginRes?.data?.access_token}`, 182 | }); 183 | expect(resp?.data).toBeUndefined(); 184 | expect(resp?.errors).toHaveLength(1); 185 | }); 186 | 187 | describe('magic link login', () => { 188 | it('should login with magic link', async () => { 189 | const magicLinkLoginRes = await authorizer.magicLinkLogin({ 190 | email: testConfig.maginLinkLoginEmail, 191 | }); 192 | expect(magicLinkLoginRes?.data).toBeDefined(); 193 | expect(magicLinkLoginRes?.errors).toHaveLength(0); 194 | }); 195 | it('should verify email', async () => { 196 | const verificationRequestsRes = await authorizer.graphqlQuery({ 197 | query: verificationRequests, 198 | headers: { 199 | 'x-authorizer-admin-secret': authorizerConfig.adminSecret, 200 | }, 201 | }); 202 | const requests = 203 | verificationRequestsRes?.data?._verification_requests 204 | .verification_requests; 205 | const item = requests.find( 206 | (i: { email: string }) => i.email === testConfig.maginLinkLoginEmail, 207 | ); 208 | expect(item).not.toBeNull(); 209 | const verifyEmailRes = await authorizer.verifyEmail({ 210 | token: item.token, 211 | }); 212 | expect(verifyEmailRes?.data).toBeDefined(); 213 | expect(verifyEmailRes?.errors).toHaveLength(0); 214 | expect(verifyEmailRes?.data?.user?.signup_methods).toContain( 215 | 'magic_link_login', 216 | ); 217 | }); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Note: write gql query in single line to reduce bundle size 2 | import { DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS } from './constants'; 3 | import * as Types from './types'; 4 | import { 5 | bufferToBase64UrlEncoded, 6 | createQueryParams, 7 | createRandomString, 8 | encode, 9 | executeIframe, 10 | hasWindow, 11 | sha256, 12 | trimURL, 13 | } from './utils'; 14 | import type { 15 | ApiResponse, 16 | AuthToken, 17 | AuthorizeResponse, 18 | ConfigType, 19 | GenericResponse, 20 | GetTokenResponse, 21 | GrapQlResponseType, 22 | MetaData, 23 | ResendVerifyEmailInput, 24 | User, 25 | ValidateJWTTokenResponse, 26 | ValidateSessionResponse, 27 | ForgotPasswordResponse, 28 | } from './types'; 29 | 30 | // re-usable gql response fragment 31 | const userFragment = 32 | 'id email email_verified given_name family_name middle_name nickname preferred_username picture signup_methods gender birthdate phone_number phone_number_verified roles created_at updated_at is_multi_factor_auth_enabled app_data'; 33 | const authTokenFragment = `message access_token expires_in refresh_token id_token should_show_email_otp_screen should_show_mobile_otp_screen should_show_totp_screen authenticator_scanner_image authenticator_secret authenticator_recovery_codes user { ${userFragment} }`; 34 | 35 | // set fetch based on window object. Cross fetch have issues with umd build 36 | const getFetcher = () => fetch; 37 | 38 | export * from './types'; 39 | 40 | export class Authorizer { 41 | // class variable 42 | config: ConfigType; 43 | codeVerifier: string; 44 | 45 | // constructor 46 | constructor(config: ConfigType) { 47 | if (!config) throw new Error('Configuration is required'); 48 | 49 | this.config = config; 50 | if (!config.authorizerURL && !config.authorizerURL.trim()) 51 | throw new Error('Invalid authorizerURL'); 52 | 53 | if (config.authorizerURL) 54 | this.config.authorizerURL = trimURL(config.authorizerURL); 55 | 56 | if (!config.redirectURL && !config.redirectURL.trim()) 57 | throw new Error('Invalid redirectURL'); 58 | else this.config.redirectURL = trimURL(config.redirectURL); 59 | 60 | this.config.extraHeaders = { 61 | ...(config.extraHeaders || {}), 62 | 'x-authorizer-url': this.config.authorizerURL, 63 | 'x-authorizer-client-id': this.config.clientID || '', 64 | 'Content-Type': 'application/json', 65 | }; 66 | this.config.clientID = (config?.clientID || '').trim(); 67 | } 68 | 69 | authorize = async ( 70 | data: Types.AuthorizeInput, 71 | ): Promise< 72 | ApiResponse | ApiResponse 73 | > => { 74 | if (!hasWindow()) 75 | return this.errorResponse([ 76 | new Error('this feature is only supported in browser'), 77 | ]); 78 | 79 | const scopes = ['openid', 'profile', 'email']; 80 | if (data.use_refresh_token) scopes.push('offline_access'); 81 | 82 | const requestData: Record = { 83 | redirect_uri: this.config.redirectURL, 84 | response_mode: data.response_mode || 'web_message', 85 | state: encode(createRandomString()), 86 | nonce: encode(createRandomString()), 87 | response_type: data.response_type, 88 | scope: scopes.join(' '), 89 | client_id: this.config?.clientID || '', 90 | }; 91 | 92 | if (data.response_type === Types.ResponseTypes.Code) { 93 | this.codeVerifier = createRandomString(); 94 | const sha = await sha256(this.codeVerifier); 95 | const codeChallenge = bufferToBase64UrlEncoded(sha); 96 | requestData.code_challenge = codeChallenge; 97 | } 98 | 99 | const authorizeURL = `${ 100 | this.config.authorizerURL 101 | }/authorize?${createQueryParams(requestData)}`; 102 | 103 | if (requestData.response_mode !== 'web_message') { 104 | window.location.replace(authorizeURL); 105 | return this.okResponse(undefined); 106 | } 107 | 108 | try { 109 | const iframeRes = await executeIframe( 110 | authorizeURL, 111 | this.config.authorizerURL, 112 | DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS, 113 | ); 114 | 115 | if (data.response_type === Types.ResponseTypes.Code) { 116 | // get token and return it 117 | const tokenResp: ApiResponse = await this.getToken({ 118 | code: iframeRes.code, 119 | }); 120 | return tokenResp.errors.length 121 | ? this.errorResponse(tokenResp.errors) 122 | : this.okResponse(tokenResp.data); 123 | } 124 | 125 | // this includes access_token, id_token & refresh_token(optionally) 126 | return this.okResponse(iframeRes); 127 | } catch (err) { 128 | if (err.error) { 129 | window.location.replace( 130 | `${this.config.authorizerURL}/app?state=${encode( 131 | JSON.stringify(this.config), 132 | )}&redirect_uri=${this.config.redirectURL}`, 133 | ); 134 | } 135 | 136 | return this.errorResponse(err); 137 | } 138 | }; 139 | 140 | browserLogin = async (): Promise> => { 141 | try { 142 | const tokenResp: ApiResponse = await this.getSession(); 143 | return tokenResp.errors.length 144 | ? this.errorResponse(tokenResp.errors) 145 | : this.okResponse(tokenResp.data); 146 | } catch (err) { 147 | if (!hasWindow()) { 148 | return { 149 | data: undefined, 150 | errors: [new Error('browserLogin is only supported for browsers')], 151 | }; 152 | } 153 | 154 | window.location.replace( 155 | `${this.config.authorizerURL}/app?state=${encode( 156 | JSON.stringify(this.config), 157 | )}&redirect_uri=${this.config.redirectURL}`, 158 | ); 159 | return this.errorResponse(err); 160 | } 161 | }; 162 | 163 | forgotPassword = async ( 164 | data: Types.ForgotPasswordInput, 165 | ): Promise> => { 166 | if (!data.state) data.state = encode(createRandomString()); 167 | 168 | if (!data.redirect_uri) data.redirect_uri = this.config.redirectURL; 169 | 170 | try { 171 | const forgotPasswordResp = await this.graphqlQuery({ 172 | query: 173 | 'mutation forgotPassword($data: ForgotPasswordInput!) { forgot_password(params: $data) { message should_show_mobile_otp_screen } }', 174 | variables: { 175 | data, 176 | }, 177 | }); 178 | return forgotPasswordResp?.errors?.length 179 | ? this.errorResponse(forgotPasswordResp.errors) 180 | : this.okResponse(forgotPasswordResp?.data.forgot_password); 181 | } catch (error) { 182 | return this.errorResponse([error]); 183 | } 184 | }; 185 | 186 | getMetaData = async (): Promise> => { 187 | try { 188 | const res = await this.graphqlQuery({ 189 | query: 190 | 'query { meta { version client_id is_google_login_enabled is_facebook_login_enabled is_github_login_enabled is_linkedin_login_enabled is_apple_login_enabled is_twitter_login_enabled is_microsoft_login_enabled is_twitch_login_enabled is_roblox_login_enabled is_email_verification_enabled is_basic_authentication_enabled is_magic_link_login_enabled is_sign_up_enabled is_strong_password_enabled is_multi_factor_auth_enabled is_mobile_basic_authentication_enabled is_phone_verification_enabled } }', 191 | }); 192 | 193 | return res?.errors?.length 194 | ? this.errorResponse(res.errors) 195 | : this.okResponse(res.data.meta); 196 | } catch (error) { 197 | return this.errorResponse([error]); 198 | } 199 | }; 200 | 201 | getProfile = async (headers?: Types.Headers): Promise> => { 202 | try { 203 | const profileRes = await this.graphqlQuery({ 204 | query: `query { profile { ${userFragment} } }`, 205 | headers, 206 | }); 207 | 208 | return profileRes?.errors?.length 209 | ? this.errorResponse(profileRes.errors) 210 | : this.okResponse(profileRes.data.profile); 211 | } catch (error) { 212 | return this.errorResponse([error]); 213 | } 214 | }; 215 | 216 | // this is used to verify / get session using cookie by default. If using node.js pass authorization header 217 | getSession = async ( 218 | headers?: Types.Headers, 219 | params?: Types.SessionQueryInput, 220 | ): Promise> => { 221 | try { 222 | const res = await this.graphqlQuery({ 223 | query: `query getSession($params: SessionQueryInput){session(params: $params) { ${authTokenFragment} } }`, 224 | headers, 225 | variables: { 226 | params, 227 | }, 228 | }); 229 | return res?.errors?.length 230 | ? this.errorResponse(res.errors) 231 | : this.okResponse(res.data?.session); 232 | } catch (err) { 233 | return this.errorResponse(err); 234 | } 235 | }; 236 | 237 | getToken = async ( 238 | data: Types.GetTokenInput, 239 | ): Promise> => { 240 | if (!data.grant_type) data.grant_type = 'authorization_code'; 241 | 242 | if (data.grant_type === 'refresh_token' && !data.refresh_token) 243 | return this.errorResponse([new Error('Invalid refresh_token')]); 244 | 245 | if (data.grant_type === 'authorization_code' && !this.codeVerifier) 246 | return this.errorResponse([new Error('Invalid code verifier')]); 247 | 248 | const requestData = { 249 | client_id: this.config.clientID, 250 | code: data.code || '', 251 | code_verifier: this.codeVerifier || '', 252 | grant_type: data.grant_type || '', 253 | refresh_token: data.refresh_token || '', 254 | }; 255 | 256 | try { 257 | const fetcher = getFetcher(); 258 | const res = await fetcher(`${this.config.authorizerURL}/oauth/token`, { 259 | method: 'POST', 260 | body: JSON.stringify(requestData), 261 | headers: { 262 | ...this.config.extraHeaders, 263 | }, 264 | credentials: 'include', 265 | }); 266 | 267 | const json = await res.json(); 268 | if (res.status >= 400) 269 | return this.errorResponse([ 270 | new Error(json.error_description || json.error), 271 | ]); 272 | 273 | return this.okResponse(json); 274 | } catch (err) { 275 | return this.errorResponse(err); 276 | } 277 | }; 278 | 279 | login = async (data: Types.LoginInput): Promise> => { 280 | try { 281 | const res = await this.graphqlQuery({ 282 | query: ` 283 | mutation login($data: LoginInput!) { login(params: $data) { ${authTokenFragment}}} 284 | `, 285 | variables: { data }, 286 | }); 287 | 288 | return res?.errors?.length 289 | ? this.errorResponse(res.errors) 290 | : this.okResponse(res.data?.login); 291 | } catch (err) { 292 | return this.errorResponse([new Error(err)]); 293 | } 294 | }; 295 | 296 | logout = async ( 297 | headers?: Types.Headers, 298 | ): Promise> => { 299 | try { 300 | const res = await this.graphqlQuery({ 301 | query: ' mutation { logout { message } } ', 302 | headers, 303 | }); 304 | return res?.errors?.length 305 | ? this.errorResponse(res.errors) 306 | : this.okResponse(res.data?.response); 307 | } catch (err) { 308 | return this.errorResponse([err]); 309 | } 310 | }; 311 | 312 | magicLinkLogin = async ( 313 | data: Types.MagicLinkLoginInput, 314 | ): Promise> => { 315 | try { 316 | if (!data.state) data.state = encode(createRandomString()); 317 | 318 | if (!data.redirect_uri) data.redirect_uri = this.config.redirectURL; 319 | 320 | const res = await this.graphqlQuery({ 321 | query: ` 322 | mutation magicLinkLogin($data: MagicLinkLoginInput!) { magic_link_login(params: $data) { message }} 323 | `, 324 | variables: { data }, 325 | }); 326 | 327 | return res?.errors?.length 328 | ? this.errorResponse(res.errors) 329 | : this.okResponse(res.data?.magic_link_login); 330 | } catch (err) { 331 | return this.errorResponse([err]); 332 | } 333 | }; 334 | 335 | oauthLogin = async ( 336 | oauthProvider: string, 337 | roles?: string[], 338 | redirect_uri?: string, 339 | state?: string, 340 | ): Promise => { 341 | let urlState = state; 342 | if (!urlState) { 343 | urlState = encode(createRandomString()); 344 | } 345 | 346 | // @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0. 347 | if (!Object.values(Types.OAuthProviders).includes(oauthProvider)) { 348 | throw new Error( 349 | `only following oauth providers are supported: ${Object.values( 350 | oauthProvider, 351 | ).toString()}`, 352 | ); 353 | } 354 | if (!hasWindow()) 355 | throw new Error('oauthLogin is only supported for browsers'); 356 | 357 | if (roles && roles.length) urlState += `&roles=${roles.join(',')}`; 358 | 359 | window.location.replace( 360 | `${this.config.authorizerURL}/oauth_login/${oauthProvider}?redirect_uri=${ 361 | redirect_uri || this.config.redirectURL 362 | }&state=${urlState}`, 363 | ); 364 | }; 365 | 366 | resendOtp = async ( 367 | data: Types.ResendOtpInput, 368 | ): Promise> => { 369 | try { 370 | const res = await this.graphqlQuery({ 371 | query: ` 372 | mutation resendOtp($data: ResendOTPRequest!) { resend_otp(params: $data) { message }} 373 | `, 374 | variables: { data }, 375 | }); 376 | 377 | return res?.errors?.length 378 | ? this.errorResponse(res.errors) 379 | : this.okResponse(res.data?.resend_otp); 380 | } catch (err) { 381 | return this.errorResponse([err]); 382 | } 383 | }; 384 | 385 | resetPassword = async ( 386 | data: Types.ResetPasswordInput, 387 | ): Promise> => { 388 | try { 389 | const resetPasswordRes = await this.graphqlQuery({ 390 | query: 391 | 'mutation resetPassword($data: ResetPasswordInput!) { reset_password(params: $data) { message } }', 392 | variables: { 393 | data, 394 | }, 395 | }); 396 | return resetPasswordRes?.errors?.length 397 | ? this.errorResponse(resetPasswordRes.errors) 398 | : this.okResponse(resetPasswordRes.data?.reset_password); 399 | } catch (error) { 400 | return this.errorResponse([error]); 401 | } 402 | }; 403 | 404 | revokeToken = async (data: { refresh_token: string }) => { 405 | if (!data.refresh_token && !data.refresh_token.trim()) 406 | return this.errorResponse([new Error('Invalid refresh_token')]); 407 | 408 | const fetcher = getFetcher(); 409 | const res = await fetcher(`${this.config.authorizerURL}/oauth/revoke`, { 410 | method: 'POST', 411 | headers: { 412 | ...this.config.extraHeaders, 413 | }, 414 | body: JSON.stringify({ 415 | refresh_token: data.refresh_token, 416 | client_id: this.config.clientID, 417 | }), 418 | }); 419 | 420 | const responseData = await res.json(); 421 | return this.okResponse(responseData); 422 | }; 423 | 424 | signup = async (data: Types.SignupInput): Promise> => { 425 | try { 426 | const res = await this.graphqlQuery({ 427 | query: ` 428 | mutation signup($data: SignUpInput!) { signup(params: $data) { ${authTokenFragment}}} 429 | `, 430 | variables: { data }, 431 | }); 432 | 433 | return res?.errors?.length 434 | ? this.errorResponse(res.errors) 435 | : this.okResponse(res.data?.signup); 436 | } catch (err) { 437 | return this.errorResponse([err]); 438 | } 439 | }; 440 | 441 | updateProfile = async ( 442 | data: Types.UpdateProfileInput, 443 | headers?: Types.Headers, 444 | ): Promise> => { 445 | try { 446 | const updateProfileRes = await this.graphqlQuery({ 447 | query: 448 | 'mutation updateProfile($data: UpdateProfileInput!) { update_profile(params: $data) { message } }', 449 | headers, 450 | variables: { 451 | data, 452 | }, 453 | }); 454 | 455 | return updateProfileRes?.errors?.length 456 | ? this.errorResponse(updateProfileRes.errors) 457 | : this.okResponse(updateProfileRes.data?.update_profile); 458 | } catch (error) { 459 | return this.errorResponse([error]); 460 | } 461 | }; 462 | 463 | deactivateAccount = async ( 464 | headers?: Types.Headers, 465 | ): Promise> => { 466 | try { 467 | const res = await this.graphqlQuery({ 468 | query: 'mutation deactivateAccount { deactivate_account { message } }', 469 | headers, 470 | }); 471 | return res?.errors?.length 472 | ? this.errorResponse(res.errors) 473 | : this.okResponse(res.data?.deactivate_account); 474 | } catch (error) { 475 | return this.errorResponse([error]); 476 | } 477 | }; 478 | 479 | validateJWTToken = async ( 480 | params?: Types.ValidateJWTTokenInput, 481 | ): Promise> => { 482 | try { 483 | const res = await this.graphqlQuery({ 484 | query: 485 | 'query validateJWTToken($params: ValidateJWTTokenInput!){validate_jwt_token(params: $params) { is_valid claims } }', 486 | variables: { 487 | params, 488 | }, 489 | }); 490 | 491 | return res?.errors?.length 492 | ? this.errorResponse(res.errors) 493 | : this.okResponse(res.data?.validate_jwt_token); 494 | } catch (error) { 495 | return this.errorResponse([error]); 496 | } 497 | }; 498 | 499 | validateSession = async ( 500 | params?: Types.ValidateSessionInput, 501 | ): Promise> => { 502 | try { 503 | const res = await this.graphqlQuery({ 504 | query: `query validateSession($params: ValidateSessionInput){validate_session(params: $params) { is_valid user { ${userFragment} } } }`, 505 | variables: { 506 | params, 507 | }, 508 | }); 509 | 510 | return res?.errors?.length 511 | ? this.errorResponse(res.errors) 512 | : this.okResponse(res.data?.validate_session); 513 | } catch (error) { 514 | return this.errorResponse([error]); 515 | } 516 | }; 517 | 518 | verifyEmail = async ( 519 | data: Types.VerifyEmailInput, 520 | ): Promise> => { 521 | try { 522 | const res = await this.graphqlQuery({ 523 | query: ` 524 | mutation verifyEmail($data: VerifyEmailInput!) { verify_email(params: $data) { ${authTokenFragment}}} 525 | `, 526 | variables: { data }, 527 | }); 528 | 529 | return res?.errors?.length 530 | ? this.errorResponse(res.errors) 531 | : this.okResponse(res.data?.verify_email); 532 | } catch (err) { 533 | return this.errorResponse([err]); 534 | } 535 | }; 536 | 537 | resendVerifyEmail = async ( 538 | data: ResendVerifyEmailInput, 539 | ): Promise> => { 540 | try { 541 | const res = await this.graphqlQuery({ 542 | query: ` 543 | mutation resendVerifyEmail($data: ResendVerifyEmailInput!) { resend_verify_email(params: $data) { message }} 544 | `, 545 | variables: { data }, 546 | }); 547 | 548 | return res?.errors?.length 549 | ? this.errorResponse(res.errors) 550 | : this.okResponse(res.data?.verify_email); 551 | } catch (err) { 552 | return this.errorResponse([err]); 553 | } 554 | }; 555 | 556 | verifyOtp = async ( 557 | data: Types.VerifyOtpInput, 558 | ): Promise> => { 559 | try { 560 | const res = await this.graphqlQuery({ 561 | query: ` 562 | mutation verifyOtp($data: VerifyOTPRequest!) { verify_otp(params: $data) { ${authTokenFragment}}} 563 | `, 564 | variables: { data }, 565 | }); 566 | 567 | return res?.errors?.length 568 | ? this.errorResponse(res.errors) 569 | : this.okResponse(res.data?.verify_otp); 570 | } catch (err) { 571 | return this.errorResponse([err]); 572 | } 573 | }; 574 | 575 | // helper to execute graphql queries 576 | // takes in any query or mutation string as input 577 | graphqlQuery = async ( 578 | data: Types.GraphqlQueryInput, 579 | ): Promise => { 580 | const fetcher = getFetcher(); 581 | const res = await fetcher(`${this.config.authorizerURL}/graphql`, { 582 | method: 'POST', 583 | body: JSON.stringify({ 584 | query: data.query, 585 | variables: data.variables || {}, 586 | }), 587 | headers: { 588 | ...this.config.extraHeaders, 589 | ...(data.headers || {}), 590 | }, 591 | credentials: 'include', 592 | }); 593 | 594 | const json = await res.json(); 595 | 596 | if (json?.errors?.length) { 597 | return { data: undefined, errors: json.errors }; 598 | } 599 | 600 | return { data: json.data, errors: [] }; 601 | }; 602 | 603 | errorResponse = (errors: Error[]): ApiResponse => { 604 | return { 605 | data: undefined, 606 | errors, 607 | }; 608 | }; 609 | 610 | okResponse = (data: any): ApiResponse => { 611 | return { 612 | data, 613 | errors: [], 614 | }; 615 | }; 616 | } 617 | --------------------------------------------------------------------------------