├── .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 |
34 |
35 |
36 | logout
37 |
38 |
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 |
--------------------------------------------------------------------------------