├── .github └── workflows │ └── build-test-ci.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .lintstagedrc.yml ├── .prettierignore ├── .prettierrc.yaml ├── .release-it.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── generator-config.yaml ├── kinde-mgmt-api-specs.yaml ├── lib ├── .openapi-generator-ignore ├── __tests__ │ ├── mocks.ts │ └── sdk │ │ ├── oauth2-flows │ │ ├── AuthCodeWithPKCE.spec.ts │ │ ├── AuthorizationCode.spec.ts │ │ └── ClientCredentials.spec.ts │ │ ├── session-managers │ │ └── BrowserSessionManager.browser.spec.ts │ │ └── utilities │ │ ├── feature-flags.spec.ts │ │ ├── generateRandomString.spec.ts │ │ ├── token-claims.spec.ts │ │ ├── token-utils.spec.ts │ │ └── validate-client-secret.spec.ts └── sdk │ ├── clients │ ├── browser │ │ ├── authcode-with-pkce.ts │ │ └── index.ts │ ├── index.ts │ ├── server │ │ ├── authorization-code.ts │ │ ├── client-credentials.ts │ │ ├── index.ts │ │ └── with-auth-utilities.ts │ └── types.ts │ ├── environment.ts │ ├── exceptions.ts │ ├── index.ts │ ├── oauth2-flows │ ├── AuthCodeAbstract.ts │ ├── AuthCodeWithPKCE.ts │ ├── AuthorizationCode.ts │ ├── ClientCredentials.ts │ ├── index.ts │ └── types.ts │ ├── session-managers │ ├── BrowserSessionManager.ts │ ├── index.ts │ └── types.ts │ ├── utilities │ ├── code-challenge.ts │ ├── createPortalUrl.ts │ ├── feature-flags.ts │ ├── index.ts │ ├── random-string.ts │ ├── token-claims.ts │ ├── token-utils.ts │ ├── types.ts │ └── validate-client-secret.ts │ └── version.ts ├── openapitools.json ├── package-cjs.json ├── package-esm.json ├── package.json ├── pnpm-lock.yaml ├── renovate.json ├── sdk-version.js ├── templates └── index.mustache ├── tsconfig.cjs.json ├── tsconfig.config.json ├── tsconfig.json ├── vitest.config.js ├── vitest.setup.js └── vitest.setup.ts /.github/workflows/build-test-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, 2 | # build the source code and run tests. It was created by using the following workflow 3 | # as a template. 4 | # 5 | # https://pnpm.io/next/continuous-integration#github-actions 6 | 7 | name: Build and test TypeScript SDK CI 8 | 9 | on: 10 | push: 11 | branches: [main] 12 | pull_request: 13 | branches: [main] 14 | 15 | jobs: 16 | main: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | node-version: [18.x, 20.x] 21 | steps: 22 | - uses: actions/checkout@v5 23 | - uses: pnpm/action-setup@v4 24 | - name: Setting up Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v5 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'pnpm' 29 | - name: Enabling pre-post scripts 30 | run: pnpm config set enable-pre-post-scripts true 31 | - name: Debug environment 32 | run: | 33 | echo "Node version: $(node --version)" 34 | echo "pnpm version: $(pnpm --version)" 35 | echo "Current directory: $(pwd)" 36 | echo "Files in root: $(ls -la)" 37 | echo "vitest.config.ts exists: $(test -f vitest.config.ts && echo 'yes' || echo 'no')" 38 | - run: pnpm install 39 | - run: pnpm build 40 | - run: pnpm lint 41 | - name: Debug vitest setup 42 | run: | 43 | echo "Checking vitest config..." 44 | npx vitest --version 45 | echo "Config files present:" 46 | ls -la vitest.config.* || echo "No vitest config files found" 47 | - run: pnpm test 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # project dependencies 2 | node_modules 3 | 4 | # sdk builds 5 | /dist 6 | /dist-cjs 7 | lib/models 8 | lib/apis 9 | lib/index.ts 10 | lib/runtime.ts 11 | lib/.openapi-generator 12 | 13 | coverage -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pnpm run lint-staged 3 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pnpm test -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | '**/*.{js,jsx,ts,tsx}': 2 | - 'prettier --write' 3 | - 'eslint --fix' 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md 2 | package.json 3 | node_modules 4 | pnpm-lock.yaml 5 | tsconfig.json 6 | tsconfig.cjs.json 7 | jest.config.json 8 | dist 9 | dist-cjs 10 | .eslintrc.yml 11 | .prettier.yml 12 | .lintstagedrc.yml 13 | lib/apis/** 14 | lib/models/** 15 | lib/index.ts 16 | lib/runtime.ts 17 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: 'es5' 2 | tabWidth: 2 3 | printWidth: 85 4 | semi: true 5 | singleQuote: true 6 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "requireBranch": "main", 4 | "commitMessage": "chore: release v${version}" 5 | }, 6 | "hooks": { 7 | "before:init": ["git pull", "pnpm run lint"], 8 | "after:bump": "npx auto-changelog -p && npm run build" 9 | }, 10 | "github": { 11 | "release": true 12 | }, 13 | "npm": { 14 | "publish": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [2.13.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/4.6.2...2.13.0) 8 | 9 | - chore(deps): update dependency @typescript-eslint/eslint-plugin to v8 [`#99`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/99) 10 | - chore(deps): update dependency prettier to v3.6.2 [`#94`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/94) 11 | - chore(deps): update dependency lint-staged to v16 [`#102`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/102) 12 | - chore(deps): update dependency husky to v9 [`#100`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/100) 13 | - chore(deps): update dependency @tsconfig/node18 to v18 [`#97`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/97) 14 | - chore(deps): update actions/setup-node action to v4 [`#96`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/96) 15 | - chore(deps): update actions/checkout action to v5 [`#95`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/95) 16 | - chore(deps): update dependency @types/node to v22 [`#98`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/98) 17 | - chore(deps): update dependency jose to v6.1.0 [`#93`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/93) 18 | - chore(deps): update dependency eslint-plugin-prettier to v5.5.4 [`#92`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/92) 19 | - chore(deps): update dependency eslint-plugin-n to v17.21.3 [`#91`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/91) 20 | - chore(deps): update dependency eslint-plugin-import to v2.32.0 [`#90`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/90) 21 | - chore(deps): update dependency eslint to v9.34.0 [`#89`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/89) 22 | - chore(deps): update dependency @types/node to v20.19.11 [`#88`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/88) 23 | - fix(deps): update dependency @kinde/js-utils to v0.23.0 [`#86`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/86) 24 | - fix(deps): update dependency @typescript-eslint/parser to v8.41.0 [`#87`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/87) 25 | - chore(deps): update dependency typescript to v5.9.2 [`#85`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/85) 26 | - chore(deps): update dependency @openapitools/openapi-generator-cli to v2.23.1 [`#83`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/83) 27 | - chore(deps): update dependency eslint-config-prettier to v10.1.8 [`#82`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/82) 28 | - [Snyk] Upgrade @kinde/js-utils from 0.19.0 to 0.20.1 [`#78`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/78) 29 | - chore: Configure Renovate [`#81`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/81) 30 | - Feat/migrate jose to kinde jwt validator/decoder [`#76`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/76) 31 | - deps: update @kinde-js/utils [`#73`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/73) 32 | - fix: pull optional parameter up to refreshTokens abstraction [`#69`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/69) 33 | - feat: allow the ability to refreshTokens without also committing them to the session [`#67`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/67) 34 | - fix: add phone to UserType interface [`#68`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/68) 35 | - Update token-utils.ts [`#65`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/65) 36 | - chore: add release-it [`#62`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/62) 37 | - bug: prevent expiry exception when reading id token claims [`#61`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/61) 38 | - fix: logout redirect is not required [`#60`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/60) 39 | - feat: verify JWTs before accessing and storing [`#46`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/46) 40 | - feat: expose refreshTokens method [`#56`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/56) 41 | - feat: TS enhancements [`#53`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/53) 42 | - fix: ts-lint errors [`#51`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/51) 43 | - bug: exclude openid from client credentials flow [`#52`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/52) 44 | - feat: validate client secrets [`#49`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/49) 45 | - fix: generateRandomString returning double length [`#50`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/50) 46 | - fix: no openid scope crash [`#48`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/48) 47 | - Merge pull request #80 from kinde-oss/feat/session-persistence [`057e5c0`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/057e5c0857f104f4f4f9c5f0a5f48deea0af0dce) 48 | - feat: set sessionManager persistent property depending on accessToken flag [`7fe1a6c`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/7fe1a6c61053d804ec1e688602880ec29ce730cb) 49 | - Merge pull request #105 from kinde-oss/renovate/pnpm-10.x [`3c02c6b`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/3c02c6ba6fd500cfe16474ba6635b4cbd75b73a1) 50 | 51 | ### [4.6.2](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.12.0...4.6.2) 52 | 53 | > 24 January 2024 54 | 55 | #### [v2.12.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.11.1...v2.12.0) 56 | 57 | > 3 July 2025 58 | 59 | - Feat/migrate jose to kinde jwt validator/decoder [`#76`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/76) 60 | - feat: add JWT validation on JWT retrieval [`1d55abf`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/1d55abf2c52d2fb49ab588e8bc4002e56ef97aef) 61 | - feat: migrate from jose to @kinde/jwt-validator packages [`b21411e`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/b21411e166f0f31f4f718a0d8ea2a57d7ac08070) 62 | - fix: update function signatures and move jose to devDependencies and Delete remote-jwks-cache.ts [`d7cae17`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/d7cae1765186b7ef0d84a464939e405c0055c857) 63 | 64 | #### [v2.11.1](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.11.0...v2.11.1) 65 | 66 | > 23 June 2025 67 | 68 | - deps: update @kinde-js/utils [`#73`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/73) 69 | - chore: release v2.11.1 [`f31bd0a`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/f31bd0acb8504eb2e98c048cb0d7c06c0609f31a) 70 | 71 | #### [v2.11.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.10.1...v2.11.0) 72 | 73 | > 4 June 2025 74 | 75 | - chore: release v2.11.0 [`b043a65`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/b043a659821f2b57f2763294b42a60a2099cde31) 76 | - Merge pull request #70 from murbanowicz/bump-jose [`e5935b3`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/e5935b3a513a23b2c47a3f4da918681a9a53f255) 77 | - feat: createPortalUrl [`e358d4c`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/e358d4c893b5867dc142c7a3b6c722939e455e9a) 78 | 79 | #### [v2.10.1](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.10.0...v2.10.1) 80 | 81 | > 4 April 2025 82 | 83 | - fix: pull optional parameter up to refreshTokens abstraction [`#69`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/69) 84 | - chore: release v2.10.1 [`5a1aeec`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/5a1aeec03bc74d8e4cdd393d9f84a9a9bfd161d6) 85 | 86 | #### [v2.10.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.9.1...v2.10.0) 87 | 88 | > 3 April 2025 89 | 90 | - feat: allow the ability to refreshTokens without also committing them to the session [`#67`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/67) 91 | - fix: add phone to UserType interface [`#68`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/68) 92 | - Update token-utils.ts [`#65`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/65) 93 | - chore: release v2.10.0 [`01d148b`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/01d148b84ba5ac20f77b9a9a7414232935c7f6be) 94 | - feat: update authcode impl with new refreshTokens parameter [`d9b0d2c`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/d9b0d2c30e3d3a5d40a630f5fa59d121b864f2df) 95 | - feat: update pkce client impl with new refreshTokens parameter [`f861a46`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/f861a46b7def84d8e8514c63de91c180f6553aab) 96 | 97 | #### [v2.9.1](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.9.0...v2.9.1) 98 | 99 | > 15 May 2024 100 | 101 | - chore: add release-it [`#62`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/62) 102 | - bug: prevent expiry exception when reading id token claims [`#61`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/61) 103 | - fix: logout redirect is not required [`#60`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/60) 104 | - bug: logout redirect is not required [`6e968b3`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/6e968b3fc9b2c258292865e2019d69108a6291a0) 105 | - chore: release v2.9.1 [`a9f596b`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/a9f596bd136ee0412d3eb3b4e3c8e1aca1473421) 106 | 107 | #### [v2.9.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.8.0...v2.9.0) 108 | 109 | > 17 April 2024 110 | 111 | - feat: verify JWTs before accessing and storing [`#46`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/46) 112 | - chore: bump version and update API spec [`a863827`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/a86382740ba42675709c08e13a5ed256627f9e5f) 113 | - feat: cache JWKS across requests [`fa5530f`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/fa5530f64a1dc35f45a4f2a0249840e43d8d7d77) 114 | - allow reading expired id tokens [`fbe22fb`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/fbe22fb8fce4b07504f5580cc1ce79ececdad493) 115 | 116 | #### [v2.8.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/2.7.2...v2.8.0) 117 | 118 | > 20 March 2024 119 | 120 | - feat: expose refreshTokens method [`#56`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/56) 121 | - feat: TS enhancements [`#53`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/53) 122 | - feat: update API spec [`c1dbc27`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/c1dbc27885b8d0d0be5d87fab78ecba8f3e9c869) 123 | - chore: prettier fix [`6cc94ab`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/6cc94ab21cd37baf9915dc14d72e8cf0b9cea3f1) 124 | - fix: build [`882696d`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/882696d5458b9e9c548d98bc5f3be8a215c57bfb) 125 | 126 | #### [2.7.2](https://github.com/kinde-oss/kinde-typescript-sdk/compare/2.7.1...2.7.2) 127 | 128 | > 11 March 2024 129 | 130 | - fix: ts-lint errors [`#51`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/51) 131 | - bug: exclude openid from client credentials flow [`#52`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/52) 132 | - chore: release v2.7.2 [`52e5bba`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/52e5bba6c14b072e41ffa6c71d4829719e8eb599) 133 | - update tests [`1f24555`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/1f245553fe9a33fdfa6de812b4b6ef66bd631987) 134 | - update tests [`22dc250`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/22dc25097877008df4f69d353a7e340c7328ae57) 135 | 136 | #### [2.7.1](https://github.com/kinde-oss/kinde-typescript-sdk/compare/2.7.0...2.7.1) 137 | 138 | > 7 March 2024 139 | 140 | - chore: add changelog [`ddbb6b6`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/ddbb6b6a7c2ebba9d7f442491e344470821fb485) 141 | - chore: release v2.7.1 [`b6bda8a`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/b6bda8a4ff4564a6ae8fd8e180f3b57d2eaa5eb6) 142 | 143 | #### [2.7.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.6.2...2.7.0) 144 | 145 | > 7 March 2024 146 | 147 | - feat: validate client secrets [`#49`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/49) 148 | - fix: generateRandomString returning double length [`#50`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/50) 149 | - fix: no openid scope crash [`#48`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/48) 150 | - test: update tests [`3c29168`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/3c29168d9ead4b8d20515197b1009ffc1d5c0d18) 151 | - fix: odd random string lengths [`ebc51a0`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/ebc51a08617403ada3f2ab5a391a959d8ff79ef2) 152 | - fix: ensure that openid is always passed to the API [`32abdf7`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/32abdf72168cc77be195d7993495f63db447e8e9) 153 | 154 | #### [v2.6.2](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.6.1...v2.6.2) 155 | 156 | > 11 March 2024 157 | 158 | - feat: more explicit flow state error [`#45`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/45) 159 | - test: update tests [`20d0ecd`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/20d0ecd35f813cd5160c1acf36d4de30fc01ad93) 160 | - chore: bump version [`a15b184`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/a15b184fa88ad7a839779044cbe24f437e3fa5b6) 161 | - fix: unit test [`7b94f95`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/7b94f95809dbcb3394b03ba56df8926bc9cb6bb8) 162 | 163 | #### [v2.6.1](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.6.0...v2.6.1) 164 | 165 | > 24 January 2024 166 | 167 | - feat: support multiple audiences [`#44`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/44) 168 | - chore: update audience type to be string array [`8712dcc`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/8712dcc24caf230f25593edb4f6f466a26c0ba0b) 169 | - fix: searchParams variable [`c8c034d`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/c8c034da24056aefec1a8973ee1362dce69b20f7) 170 | - chore: bump version [`44dee54`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/44dee54d59d3872cdd48af87b91242513a77ba33) 171 | 172 | #### [v2.6.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.5.2...v2.6.0) 173 | 174 | > 16 January 2024 175 | 176 | - Use state if existing in session [`#43`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/43) 177 | - chore: make it use the prettier config that is there [`943f759`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/943f759927a390edd3b0a7397119fca4d01feb54) 178 | - chore: increase version [`e71146d`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/e71146d59369c666f6908f5b0fe45e8c629ec3bf) 179 | 180 | #### [v2.5.2](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.5.1...v2.5.2) 181 | 182 | > 12 January 2024 183 | 184 | - Update to latest management API spec [`4a73c0d`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/4a73c0debd39a55c8abcb1c18fbbf15b1ecb6961) 185 | 186 | #### [v2.5.1](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.5.0...v2.5.1) 187 | 188 | > 12 January 2024 189 | 190 | - Add OpenAPI generator [`#35`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/35) 191 | - Handle case where both `refresh_token` and `access_token` have expired [`#42`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/42) 192 | - Introduce generator, remove and ignore generated files, add generator step [`d997a4d`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/d997a4d0316dcaf2d176e91356372daa7f9d1fa3) 193 | - Clean generated files, add files used by OpenAPI, ignore generated files [`3b41808`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/3b41808e1350afba915d1ba91525f17b372efbf2) 194 | - test: wrote tests to verify changes introduced to token-utils [`428e124`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/428e1241f6cacd435bbbf61db5d3359576a98035) 195 | 196 | #### [v2.5.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.4.0...v2.5.0) 197 | 198 | > 23 December 2023 199 | 200 | - exposing all management apis as a single object [`#38`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/38) 201 | - chore: exposing all management apis as a single object [`416684c`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/416684c3fe8e5f064b09eb764a354d1d29d2f76c) 202 | - chore: bump version [`0239214`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/0239214067fb170336a8b845e4a89f1b6bc9c7aa) 203 | 204 | #### [v2.4.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.3.2...v2.4.0) 205 | 206 | > 7 December 2023 207 | 208 | - Change tsconfig target to es5 [`#36`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/36) 209 | - chore: bump version [`1fa754d`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/1fa754d8436feaeb75ba2a0f98c7cc9c8938fc39) 210 | 211 | #### [v2.3.2](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.3.1...v2.3.2) 212 | 213 | > 30 November 2023 214 | 215 | - Allow `createKindeBrowserClient` with custom `SessionManager` [`#33`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/33) 216 | - chore: bump version [`12c9db7`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/12c9db7d166734a5a3ff2cc608d58170987638ca) 217 | 218 | #### [v2.3.1](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.3.0...v2.3.1) 219 | 220 | > 30 November 2023 221 | 222 | - fix: don't store the decoded payload and decode when needed [`#32`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/32) 223 | - chore: bump version [`75d395c`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/75d395c6eb65fa512edcd92fa35dfe774b9bda2c) 224 | 225 | #### [v2.3.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.2.0...v2.3.0) 226 | 227 | > 17 November 2023 228 | 229 | - fix: prevent trying to decode refresh token [`#31`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/31) 230 | - feat: include auth url params in payload [`#29`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/29) 231 | - Bugfix/id token expiry logout [`#26`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/26) 232 | - fix: ensured that `build-test-ci.yml` workflow runs pre/post scripts [`#28`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/28) 233 | - Bump @babel/traverse from 7.21.5 to 7.23.2 [`#24`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/24) 234 | - feat: created github action for building SDK and running tests [`#23`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/23) 235 | - feat: append post_login_redirect_url to search params [`#21`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/21) 236 | - Update README to Kinde OSS SDK template [`#19`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/19) 237 | - generated from latest API spec and bump version [`165743b`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/165743b630f3093c307671e56d4f88f52aafa563) 238 | - Update README to Kinde OSS Template [`7d70ba0`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/7d70ba07b640fbf9a2f8e2b36c0d843308355111) 239 | - fix: ensured that all server utilities preliminarily refresh tokens [`2f877bd`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/2f877bd0d4efaaa915b51e3462289bfaae3d7f5c) 240 | 241 | #### [v2.2.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.1.0...v2.2.0) 242 | 243 | > 21 September 2023 244 | 245 | - feat: handle being used as an ES module in Node.js [`61b4aca`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/61b4acab114ac94a5ad24ef8dc397e78c2d6e11d) 246 | 247 | #### [v2.1.0](https://github.com/kinde-oss/kinde-typescript-sdk/compare/v2.0.0...v2.1.0) 248 | 249 | > 14 September 2023 250 | 251 | - feat: add conditional types to createKindeServerClient to select the type of options and client [`28a3695`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/28a36954eb38e1a83691124bba3d598981f21746) 252 | 253 | #### v2.0.0 254 | 255 | > 12 September 2023 256 | 257 | - fix: avoid forcing login [`#16`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/16) 258 | - updated README post migration to async methods [`#15`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/15) 259 | - feat: migrate codebase to async functions [`#13`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/13) 260 | - update: API updates [`#11`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/11) 261 | - ensured that `getFlag` throws error when `defaultValue === undefined` [`#10`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/10) 262 | - bugfix/random string and pollyfills [`#9`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/9) 263 | - Feature/sdk header override [`#8`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/8) 264 | - Feature/feedback 6 [`#7`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/7) 265 | - Feature/feedback 5 [`#6`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/6) 266 | - Feature/feedback 4 refresh tokens update [`#5`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/5) 267 | - Feature/feedback 3 [`#4`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/4) 268 | - Feature/feedback 2 [`#3`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/3) 269 | - Feature/feedback 1 [`#2`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/2) 270 | - feature/initial implementation [`#1`](https://github.com/kinde-oss/kinde-typescript-sdk/pull/1) 271 | - update: regenerate API from updated spec [`695416a`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/695416a4aaaf2b55d971b92b6255197362d89820) 272 | - added files generated by openapi-generator for endpoints and models [`71a76e8`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/71a76e88d578022dd9d46282094351211a1baa7d) 273 | - completed jest setup for unit-test coverage [`05a2134`](https://github.com/kinde-oss/kinde-typescript-sdk/commit/05a21347e0dfd4659d14d9abd8f33f7f0c43f7cc) 274 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kinde 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kinde TypeScript SDK 2 | 3 | The Kinde SDK for TypeScript. 4 | 5 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) [![Kinde Docs](https://img.shields.io/badge/Kinde-Docs-eee?style=flat-square)](https://kinde.com/docs/developer-tools) [![Kinde Community](https://img.shields.io/badge/Kinde-Community-eee?style=flat-square)](https://thekindecommunity.slack.com) 6 | 7 | ## Documentation 8 | 9 | For details on integrating this SDK into your project, head over to the [Kinde docs](https://kinde.com/docs/) and see the [TypeScript SDK](https://kinde.com/docs/developer-tools/typescript-sdk/) doc 👍🏼. 10 | 11 | ## Publishing 12 | 13 | The Kinde core team handles publishing. 14 | 15 | ## Contributing 16 | 17 | Please refer to Kinde’s [contributing guidelines](https://github.com/kinde-oss/.github/blob/489e2ca9c3307c2b2e098a885e22f2239116394a/CONTRIBUTING.md). 18 | 19 | ## License 20 | 21 | By contributing to Kinde, you agree that your contributions will be licensed under its MIT License. 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tseslint from '@typescript-eslint/eslint-plugin'; 2 | import typescript from '@typescript-eslint/parser'; 3 | import n from 'eslint-plugin-n'; 4 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 5 | import promise from 'eslint-plugin-promise'; 6 | import { defineConfig, globalIgnores } from 'eslint/config'; 7 | 8 | export default defineConfig([ 9 | // Global ignores 10 | globalIgnores(['**/dist', '**/dist-cjs']), 11 | 12 | // Base configuration for all files 13 | { 14 | linterOptions: { 15 | reportUnusedDisableDirectives: true, 16 | }, 17 | languageOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | }, 21 | rules: { 22 | quotes: ['error', 'single'], 23 | }, 24 | }, 25 | 26 | // TypeScript files 27 | { 28 | files: ['**/*.ts'], 29 | plugins: { 30 | '@typescript-eslint': tseslint, 31 | }, 32 | languageOptions: { 33 | parser: typescript, 34 | parserOptions: { 35 | project: ['./tsconfig.json', './tsconfig.config.json'], 36 | }, 37 | }, 38 | rules: { 39 | '@typescript-eslint/explicit-function-return-type': 'off', 40 | '@typescript-eslint/strict-boolean-expressions': 'off', 41 | '@typescript-eslint/no-non-null-assertion': 'off', 42 | '@typescript-eslint/no-misused-promises': 'off', 43 | '@typescript-eslint/no-dynamic-delete': 'off', 44 | }, 45 | }, 46 | 47 | // JavaScript files 48 | { 49 | files: ['**/*.js', '**/*.mjs', '**/*.cjs'], 50 | ignores: ['**/*.ts', '**/*.tsx'], 51 | }, 52 | 53 | // Other plugins 54 | { 55 | plugins: { 56 | n, 57 | promise, 58 | }, 59 | rules: { 60 | 'n/no-missing-import': 'off', 61 | }, 62 | }, 63 | 64 | // Prettier at the end to override formatting rules 65 | eslintPluginPrettierRecommended, 66 | ]); 67 | -------------------------------------------------------------------------------- /generator-config.yaml: -------------------------------------------------------------------------------- 1 | templateDir: ./templates 2 | additionalProperties: 3 | artifactId: kinde-typescript-sdk 4 | -------------------------------------------------------------------------------- /lib/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /lib/__tests__/mocks.ts: -------------------------------------------------------------------------------- 1 | import { type JWK, SignJWT, exportJWK, generateKeyPair, importJWK } from 'jose'; 2 | import { type SessionManager } from '../sdk/session-managers'; 3 | import { vi } from 'vitest'; 4 | 5 | let mockPrivateKey: JWK | undefined; 6 | let mockPublicKey: JWK | undefined; 7 | 8 | export const mockJwtAlg = 'RS256'; 9 | 10 | export const getKeys = async (): Promise<{ privateKey: JWK; publicKey: JWK }> => { 11 | if (mockPrivateKey !== undefined && mockPublicKey !== undefined) { 12 | return { privateKey: mockPrivateKey, publicKey: mockPublicKey }; 13 | } 14 | const { publicKey: generatedPublicKey, privateKey: generatedPrivateKey } = 15 | await generateKeyPair(mockJwtAlg, { extractable: true }); 16 | 17 | const generatedPrivateJwk = await exportJWK(generatedPrivateKey); 18 | const generatedPublicJwk = await exportJWK(generatedPublicKey); 19 | 20 | mockPrivateKey = generatedPrivateJwk; 21 | mockPublicKey = generatedPublicJwk; 22 | 23 | return { privateKey: mockPrivateKey, publicKey: mockPublicKey }; 24 | }; 25 | 26 | export const fetchClient = vi.fn().mockImplementation( 27 | async () => 28 | await Promise.resolve({ 29 | json: async () => { 30 | await Promise.resolve(); 31 | }, 32 | }) 33 | ); 34 | 35 | export const getMockAccessToken = async ( 36 | domain: string = 'local-testing@kinde.com', 37 | isExpired: boolean = false, 38 | isExpClaimMissing: boolean = false 39 | ) => { 40 | const iat = Math.floor(Date.now() / 1000); 41 | const exp = isExpClaimMissing ? undefined : isExpired ? iat : iat + 1000000; 42 | const tokenPayload = { 43 | aud: [domain], 44 | azp: '', 45 | exp, 46 | gty: ['client_credentials'], 47 | iat, 48 | iss: domain, 49 | org_code: 'org_123456789', 50 | scp: ['openid', 'profile', 'email', 'offline'], 51 | permissions: ['perm1', 'perm2', 'perm3'], 52 | jti: '8a567995-ace9-4e82-8724-94651a5ca50c', 53 | sub: 'kp_0c3ff3d085flo6396as29d4ffee750be7', 54 | feature_flags: { 55 | is_dark_mode: { t: 'b', v: false }, 56 | competitions_limit: { t: 'i', v: 5 }, 57 | theme: { t: 's', v: 'pink' }, 58 | }, 59 | }; 60 | 61 | const { privateKey } = await getKeys(); 62 | const key = await importJWK(privateKey, mockJwtAlg); 63 | const jwt = await new SignJWT(tokenPayload) 64 | .setProtectedHeader({ alg: mockJwtAlg }) 65 | .sign(key); 66 | 67 | return { 68 | token: jwt, 69 | payload: tokenPayload, 70 | }; 71 | }; 72 | 73 | export const getMockIdToken = async ( 74 | domain: string = 'local-testing@kinde.com', 75 | isExpired: boolean = false 76 | ) => { 77 | const iat = Math.floor(Date.now() / 1000); 78 | const exp = isExpired ? iat : iat + 1000000; 79 | const tokenPayload = { 80 | at_hash: 'oQ2Pa8kOCGrCoOQocpFzTA', 81 | aud: [domain, '35d47ccb0b5040ki3f57a2d0631af559'], 82 | auth_time: 1684766671, 83 | azp: '35d47ccb0blo40faaf57a2d0631af559', 84 | email: 'test-first.test-last@test.com', 85 | exp, 86 | family_name: 'test-last', 87 | given_name: 'test-first', 88 | iat, 89 | iss: domain, 90 | jti: '687ddac5-bac4-48cf-b5ba-2db3ca5107c1', 91 | name: 'test-first', 92 | org_codes: ['org_12345678'], 93 | sub: 'kp_0c3ff3d085flo6396as29d4ffee750be7', 94 | updated_at: iat, 95 | }; 96 | 97 | const { privateKey } = await getKeys(); 98 | const key = await importJWK(privateKey, mockJwtAlg); 99 | const jwt = await new SignJWT(tokenPayload) 100 | .setProtectedHeader({ alg: mockJwtAlg }) 101 | .sign(key); 102 | 103 | return { 104 | token: jwt, 105 | payload: tokenPayload, 106 | }; 107 | }; 108 | 109 | class ServerSessionManager implements SessionManager { 110 | private memCache: Record = {}; 111 | 112 | async destroySession(): Promise { 113 | this.memCache = {}; 114 | } 115 | 116 | async getSessionItem(itemKey: string) { 117 | return this.memCache[itemKey] ?? null; 118 | } 119 | 120 | async setSessionItem(itemKey: string, itemValue: unknown): Promise { 121 | this.memCache[itemKey] = itemValue; 122 | } 123 | 124 | async removeSessionItem(itemKey: string): Promise { 125 | delete this.memCache[itemKey]; 126 | } 127 | } 128 | 129 | export const sessionManager = new ServerSessionManager(); 130 | 131 | global.fetch = fetchClient; 132 | 133 | // Mock @kinde/jwt-validator 134 | export const createJwtValidatorMock = () => ({ 135 | validateToken: vi.fn().mockImplementation(async ({ token, domain }) => { 136 | if (!token) { 137 | return { valid: false, message: 'Token is required' }; 138 | } 139 | 140 | if (!domain) { 141 | return { valid: false, message: 'Domain is required' }; 142 | } 143 | 144 | const jwtParts = token.split('.'); 145 | if (jwtParts.length !== 3) { 146 | return { valid: false, message: 'Invalid JWT format' }; 147 | } 148 | 149 | // If it passes basic validation, return true (simplified for testing) 150 | return { valid: true, message: 'Token is valid' }; 151 | }), 152 | }); 153 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/oauth2-flows/AuthCodeWithPKCE.spec.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AuthorizationCodeOptions, 3 | SDKHeaderOverrideOptions, 4 | } from '../../../sdk/oauth2-flows'; 5 | import { base64UrlEncode, sha256 } from '../../../sdk/utilities'; 6 | import { AuthCodeWithPKCE } from '../../../sdk/oauth2-flows'; 7 | import { getSDKHeader } from '../../../sdk/version'; 8 | import * as mocks from '../../mocks'; 9 | import { describe, it, expect, afterEach } from 'vitest'; 10 | 11 | describe('AuthCodeWitPKCE', () => { 12 | const { sessionManager } = mocks; 13 | const clientConfig: AuthorizationCodeOptions = { 14 | authDomain: 'https://local-testing@kinde.com', 15 | redirectURL: 'https://app-domain.com', 16 | logoutRedirectURL: 'http://app-domain.com', 17 | clientId: 'client-id', 18 | }; 19 | 20 | describe('new AuthCodeWithPKCE', () => { 21 | it('can construct AuthCodeWithPKCE instance', () => { 22 | expect(() => new AuthCodeWithPKCE(clientConfig)).not.toThrowError(); 23 | }); 24 | }); 25 | 26 | describe('createAuthorizationURL()', () => { 27 | afterEach(async () => { 28 | await sessionManager.destroySession(); 29 | }); 30 | 31 | it('saves generated code verifier to session storage again state', async () => { 32 | const client = new AuthCodeWithPKCE(clientConfig); 33 | const authURL = await client.createAuthorizationURL(sessionManager); 34 | const searchParams = new URLSearchParams(authURL.search); 35 | 36 | const state = searchParams.get('state'); 37 | const expectedChallenge = searchParams.get('code_challenge'); 38 | expect(state).toBeDefined(); 39 | expect(expectedChallenge).toBeDefined(); 40 | 41 | const codeVerifierKey = `${AuthCodeWithPKCE.STATE_KEY}-${state!}`; 42 | const codeVerifierState = JSON.parse( 43 | (await sessionManager.getSessionItem(codeVerifierKey)) as string 44 | ); 45 | expect(codeVerifierState).toBeDefined(); 46 | 47 | const { codeVerifier } = codeVerifierState; 48 | expect(codeVerifier).toBeDefined(); 49 | 50 | const foundChallenge = base64UrlEncode(await sha256(codeVerifier)); 51 | expect(foundChallenge).toBe(expectedChallenge); 52 | }); 53 | 54 | it('uses provided state to generate authorization URL if given', async () => { 55 | const expectedState = 'test-app-state'; 56 | const client = new AuthCodeWithPKCE(clientConfig); 57 | const authURL = await client.createAuthorizationURL(sessionManager, { 58 | state: expectedState, 59 | }); 60 | const searchParams = new URLSearchParams(authURL.search); 61 | 62 | const state = searchParams.get('state'); 63 | const expectedChallenge = searchParams.get('code_challenge'); 64 | expect(state).toBe(expectedState); 65 | expect(expectedChallenge).toBeDefined(); 66 | }); 67 | }); 68 | 69 | describe('handleRedirectFromAuthDomain()', () => { 70 | afterEach(async () => { 71 | await sessionManager.destroySession(); 72 | mocks.fetchClient.mockClear(); 73 | }); 74 | 75 | it('throws an error if callbackURL has an error query param', async () => { 76 | const callbackURL = new URL( 77 | `${clientConfig.redirectURL}?state=state&code=code&error=error` 78 | ); 79 | await expect(async () => { 80 | const client = new AuthCodeWithPKCE(clientConfig); 81 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 82 | }).rejects.toThrow('Authorization server reported an error: error'); 83 | expect(mocks.fetchClient).not.toHaveBeenCalled(); 84 | }); 85 | 86 | it('throws an error if auth flow state is not present in session store', async () => { 87 | const callbackURL = new URL( 88 | `${clientConfig.redirectURL}?state=state&code=code` 89 | ); 90 | 91 | await expect(async () => { 92 | const client = new AuthCodeWithPKCE(clientConfig); 93 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 94 | }).rejects.toThrow('Stored state not found'); 95 | expect(mocks.fetchClient).not.toHaveBeenCalled(); 96 | }); 97 | 98 | it('throws an exception when fetching tokens returns an error response', async () => { 99 | const callbackURL = new URL( 100 | `${clientConfig.redirectURL}?state=state&code=code` 101 | ); 102 | const errorDescription = 'error_description'; 103 | const codeVerifierKey = `${AuthCodeWithPKCE.STATE_KEY}-state`; 104 | await sessionManager.setSessionItem( 105 | codeVerifierKey, 106 | JSON.stringify({ codeVerifier: 'code-verifier' }) 107 | ); 108 | mocks.fetchClient.mockResolvedValue({ 109 | json: () => ({ 110 | error: 'error', 111 | [errorDescription]: errorDescription, 112 | }), 113 | }); 114 | 115 | await expect(async () => { 116 | const client = new AuthCodeWithPKCE(clientConfig); 117 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 118 | }).rejects.toThrow(errorDescription); 119 | expect(mocks.fetchClient).toHaveBeenCalled(); 120 | }); 121 | 122 | it('saves tokens to memory store after exchanging auth code for tokens', async () => { 123 | const mockAccessToken = await mocks.getMockAccessToken( 124 | clientConfig.authDomain 125 | ); 126 | const mockIdToken = await mocks.getMockIdToken(clientConfig.authDomain); 127 | mocks.fetchClient.mockResolvedValue({ 128 | json: () => ({ 129 | access_token: mockAccessToken.token, 130 | refresh_token: 'refresh_token', 131 | id_token: mockIdToken.token, 132 | }), 133 | }); 134 | 135 | const callbackURL = new URL( 136 | `${clientConfig.redirectURL}?state=state&code=code` 137 | ); 138 | const codeVerifierKey = `${AuthCodeWithPKCE.STATE_KEY}-state`; 139 | await sessionManager.setSessionItem( 140 | codeVerifierKey, 141 | JSON.stringify({ codeVerifier: 'code-verifier' }) 142 | ); 143 | 144 | const client = new AuthCodeWithPKCE(clientConfig); 145 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 146 | expect(mocks.fetchClient).toHaveBeenCalledTimes(1); 147 | 148 | const foundRefreshToken = await sessionManager.getSessionItem('refresh_token'); 149 | const foundAccessToken = await sessionManager.getSessionItem('access_token'); 150 | const foundIdToken = await sessionManager.getSessionItem('id_token'); 151 | 152 | expect(foundAccessToken).toBe(mockAccessToken.token); 153 | expect(foundRefreshToken).toBe('refresh_token'); 154 | expect(foundIdToken).toBe(mockIdToken.token); 155 | }); 156 | }); 157 | 158 | describe('getToken()', () => { 159 | afterEach(async () => { 160 | await sessionManager.destroySession(); 161 | mocks.fetchClient.mockClear(); 162 | }); 163 | 164 | it('return an existing token if an unexpired token is available', async () => { 165 | const mockAccessToken = await mocks.getMockAccessToken( 166 | clientConfig.authDomain 167 | ); 168 | await sessionManager.setSessionItem('access_token', mockAccessToken.token); 169 | const client = new AuthCodeWithPKCE(clientConfig); 170 | const token = await client.getToken(sessionManager); 171 | expect(token).toBe(mockAccessToken.token); 172 | expect(mocks.fetchClient).not.toHaveBeenCalled(); 173 | }); 174 | 175 | it('throws an error if no refresh token is found in memory', async () => { 176 | const mockAccessToken = await mocks.getMockAccessToken( 177 | clientConfig.authDomain, 178 | true 179 | ); 180 | await sessionManager.setSessionItem('access_token', mockAccessToken.token); 181 | await expect(async () => { 182 | const client = new AuthCodeWithPKCE(clientConfig); 183 | await client.getToken(sessionManager); 184 | }).rejects.toThrow('Cannot persist session no valid refresh token found'); 185 | }); 186 | 187 | it('fetches new tokens if access token is expired and refresh token is available', async () => { 188 | const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain); 189 | const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain); 190 | mocks.fetchClient.mockResolvedValue({ 191 | json: () => ({ 192 | access_token: newAccessToken.token, 193 | refresh_token: 'new_refresh_token', 194 | id_token: newIdToken.token, 195 | }), 196 | }); 197 | 198 | const expiredAccessToken = await mocks.getMockAccessToken( 199 | clientConfig.authDomain, 200 | true 201 | ); 202 | await sessionManager.setSessionItem('access_token', expiredAccessToken.token); 203 | await sessionManager.setSessionItem('refresh_token', 'refresh_token'); 204 | 205 | const body = new URLSearchParams({ 206 | grant_type: 'refresh_token', 207 | refresh_token: 'refresh_token', 208 | client_id: clientConfig.clientId, 209 | }); 210 | 211 | const headers = new Headers(); 212 | headers.append(...getSDKHeader()); 213 | headers.append( 214 | 'Content-Type', 215 | 'application/x-www-form-urlencoded; charset=UTF-8' 216 | ); 217 | 218 | const client = new AuthCodeWithPKCE(clientConfig); 219 | await client.getToken(sessionManager); 220 | expect(mocks.fetchClient).toHaveBeenCalledWith( 221 | `${clientConfig.authDomain}/oauth2/token`, 222 | { method: 'POST', headers, body, credentials: 'include' } 223 | ); 224 | }); 225 | 226 | it('overrides SDK version header if options are provided to client constructor', async () => { 227 | const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain); 228 | const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain); 229 | mocks.fetchClient.mockResolvedValue({ 230 | json: () => ({ 231 | access_token: newAccessToken.token, 232 | refresh_token: 'new_refresh_token', 233 | id_token: newIdToken.token, 234 | }), 235 | }); 236 | 237 | const expiredAccessToken = await mocks.getMockAccessToken( 238 | clientConfig.authDomain, 239 | true 240 | ); 241 | await sessionManager.setSessionItem('access_token', expiredAccessToken.token); 242 | await sessionManager.setSessionItem('refresh_token', 'refresh_token'); 243 | 244 | const headerOverrides: SDKHeaderOverrideOptions = { 245 | framework: 'TypeScript-Framework', 246 | frameworkVersion: '1.1.1', 247 | }; 248 | 249 | const headers = new Headers(); 250 | headers.append(...getSDKHeader(headerOverrides)); 251 | headers.append( 252 | 'Content-Type', 253 | 'application/x-www-form-urlencoded; charset=UTF-8' 254 | ); 255 | 256 | const client = new AuthCodeWithPKCE({ 257 | ...clientConfig, 258 | ...headerOverrides, 259 | }); 260 | await client.getToken(sessionManager); 261 | expect(mocks.fetchClient).toHaveBeenCalledWith( 262 | `${clientConfig.authDomain}/oauth2/token`, 263 | expect.objectContaining({ headers }) 264 | ); 265 | }); 266 | 267 | it('commits new tokens to memory if new tokens are fetched', async () => { 268 | const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain); 269 | const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain); 270 | const newRefreshToken = 'new_refresh_token'; 271 | 272 | mocks.fetchClient.mockResolvedValue({ 273 | json: () => ({ 274 | access_token: newAccessToken.token, 275 | refresh_token: newRefreshToken, 276 | id_token: newIdToken.token, 277 | }), 278 | }); 279 | 280 | const expiredAccessToken = await mocks.getMockAccessToken( 281 | clientConfig.authDomain, 282 | true 283 | ); 284 | await sessionManager.setSessionItem('access_token', expiredAccessToken.token); 285 | await sessionManager.setSessionItem('refresh_token', 'refresh_token'); 286 | 287 | const client = new AuthCodeWithPKCE(clientConfig); 288 | await client.getToken(sessionManager); 289 | expect(mocks.fetchClient).toHaveBeenCalledTimes(1); 290 | 291 | const foundRefreshToken = await sessionManager.getSessionItem('refresh_token'); 292 | const foundAccessToken = await sessionManager.getSessionItem('access_token'); 293 | const foundIdToken = await sessionManager.getSessionItem('id_token'); 294 | 295 | expect(foundAccessToken).toBe(newAccessToken.token); 296 | expect(foundRefreshToken).toBe(newRefreshToken); 297 | expect(foundIdToken).toBe(newIdToken.token); 298 | }); 299 | }); 300 | }); 301 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/oauth2-flows/AuthorizationCode.spec.ts: -------------------------------------------------------------------------------- 1 | import { getSDKHeader } from '../../../sdk/version'; 2 | import * as mocks from '../../mocks'; 3 | 4 | import { 5 | type AuthURLOptions, 6 | AuthorizationCode, 7 | type AuthorizationCodeOptions, 8 | type SDKHeaderOverrideOptions, 9 | } from '../../../sdk/oauth2-flows'; 10 | 11 | import { KindeSDKError, KindeSDKErrorCode } from '../../../sdk/exceptions'; 12 | import { generateRandomString } from '../../../sdk/utilities'; 13 | import { describe, it, expect, afterEach } from 'vitest'; 14 | 15 | describe('AuthorizationCode', () => { 16 | const { sessionManager } = mocks; 17 | const clientSecret = generateRandomString(50); 18 | const clientConfig: AuthorizationCodeOptions = { 19 | authDomain: 'https://local-testing@kinde.com', 20 | redirectURL: 'https://app-domain.com', 21 | logoutRedirectURL: 'http://app-domain.com', 22 | clientId: 'client-id', 23 | }; 24 | 25 | describe('new AuthorizationCode', () => { 26 | it('can construct AuthorizationCode instance', () => { 27 | expect( 28 | () => new AuthorizationCode(clientConfig, 'client-secret') 29 | ).not.toThrowError(); 30 | }); 31 | }); 32 | 33 | describe('createAuthorizationURL()', () => { 34 | afterEach(async () => { 35 | await sessionManager.destroySession(); 36 | }); 37 | 38 | it('uses default scopes if none is provided in the url options', async () => { 39 | const client = new AuthorizationCode(clientConfig, clientSecret); 40 | const authURL = await client.createAuthorizationURL(sessionManager); 41 | const searchParams = new URLSearchParams(authURL.search); 42 | expect(searchParams.get('scope')).toBe(AuthorizationCode.DEFAULT_TOKEN_SCOPES); 43 | }); 44 | 45 | it('uses provided scope and audience if given in url options', async () => { 46 | const expectedScope = 'test-scope'; 47 | const expectedAudience = 'test-audience'; 48 | const testClient = new AuthorizationCode( 49 | { 50 | ...clientConfig, 51 | audience: expectedAudience, 52 | scope: expectedScope, 53 | }, 54 | 'client-secret' 55 | ); 56 | const authURL = await testClient.createAuthorizationURL(sessionManager); 57 | const searchParams = new URLSearchParams(authURL.search); 58 | expect(searchParams.get('audience')).toBe(expectedAudience); 59 | expect(searchParams.get('scope')).toMatch('test-scope'); 60 | }); 61 | 62 | it('overrides optional url search params if they are provided', async () => { 63 | const expectedParams: AuthURLOptions = { 64 | is_create_org: true, 65 | start_page: 'registration', 66 | org_code: 'test-org-code', 67 | org_name: 'test-org-name', 68 | }; 69 | 70 | const client = new AuthorizationCode(clientConfig, clientSecret); 71 | const authURL = await client.createAuthorizationURL( 72 | sessionManager, 73 | expectedParams 74 | ); 75 | const searchParams = new URLSearchParams(authURL.search); 76 | Object.entries(expectedParams).forEach(([key, expectedValue]) => { 77 | expect(searchParams.get(key)).toBe(String(expectedValue)); 78 | }); 79 | }); 80 | 81 | it('saves state to session storage again state', async () => { 82 | const client = new AuthorizationCode(clientConfig, clientSecret); 83 | const authURL = await client.createAuthorizationURL(sessionManager); 84 | const searchParams = new URLSearchParams(authURL.search); 85 | const state = searchParams.get('state'); 86 | const stateKey = AuthorizationCode.STATE_KEY; 87 | const storedState = (await sessionManager.getSessionItem(stateKey)) as string; 88 | expect(storedState).toBe(state); 89 | }); 90 | 91 | it('uses provided state to generate authorization URL if given', async () => { 92 | const expectedState = 'test-app-state'; 93 | const client = new AuthorizationCode(clientConfig, clientSecret); 94 | const authURL = await client.createAuthorizationURL(sessionManager, { 95 | state: expectedState, 96 | }); 97 | const searchParams = new URLSearchParams(authURL.search); 98 | const state = searchParams.get('state'); 99 | expect(state).toBe(expectedState); 100 | }); 101 | 102 | it('uses same state to generate authorization URL if existing in session', async () => { 103 | const client = new AuthorizationCode(clientConfig, clientSecret); 104 | const authURL = await client.createAuthorizationURL(sessionManager); 105 | const searchParams = new URLSearchParams(authURL.search); 106 | const firstState = searchParams.get('state'); 107 | 108 | const authURL2 = await client.createAuthorizationURL(sessionManager); 109 | const searchParams2 = new URLSearchParams(authURL2.search); 110 | const secondState = searchParams2.get('state'); 111 | 112 | expect(firstState).toBe(secondState); 113 | }); 114 | }); 115 | 116 | describe('handleRedirectFromAuthDomain()', () => { 117 | afterEach(async () => { 118 | mocks.fetchClient.mockClear(); 119 | await sessionManager.destroySession(); 120 | }); 121 | 122 | it('throws an error if callbackURL has an error query param', async () => { 123 | const callbackURL = new URL( 124 | `${clientConfig.redirectURL}?state=state&code=code&error=error` 125 | ); 126 | await expect(async () => { 127 | const client = new AuthorizationCode(clientConfig, clientSecret); 128 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 129 | }).rejects.toThrow('Authorization server reported an error: error'); 130 | expect(mocks.fetchClient).not.toHaveBeenCalled(); 131 | }); 132 | 133 | it('throws an error if auth flow state is not present in session store', async () => { 134 | const callbackURL = new URL( 135 | `${clientConfig.redirectURL}?state=state&code=code` 136 | ); 137 | 138 | await expect(async () => { 139 | const client = new AuthorizationCode(clientConfig, clientSecret); 140 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 141 | }).rejects.toThrow( 142 | 'Authentication flow: Received: state | Expected: State not found' 143 | ); 144 | expect(mocks.fetchClient).not.toHaveBeenCalled(); 145 | }); 146 | 147 | it('throws an exception when fetching tokens returns an error response', async () => { 148 | const callbackURL = new URL( 149 | `${clientConfig.redirectURL}?state=state&code=code` 150 | ); 151 | const errorDescription = 'error_description'; 152 | await sessionManager.setSessionItem(AuthorizationCode.STATE_KEY, 'state'); 153 | mocks.fetchClient.mockResolvedValue({ 154 | json: () => ({ 155 | error: 'error', 156 | [errorDescription]: errorDescription, 157 | }), 158 | }); 159 | 160 | await expect(async () => { 161 | const client = new AuthorizationCode(clientConfig, clientSecret); 162 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 163 | }).rejects.toThrow(errorDescription); 164 | expect(mocks.fetchClient).toHaveBeenCalled(); 165 | }); 166 | 167 | it('saves tokens to memory store after exchanging auth code for tokens', async () => { 168 | const mockAccessToken = await mocks.getMockAccessToken( 169 | clientConfig.authDomain 170 | ); 171 | const mockIdToken = await mocks.getMockIdToken(clientConfig.authDomain); 172 | mocks.fetchClient.mockResolvedValue({ 173 | json: () => ({ 174 | access_token: mockAccessToken.token, 175 | refresh_token: 'refresh_token', 176 | id_token: mockIdToken.token, 177 | }), 178 | }); 179 | 180 | const callbackURL = new URL( 181 | `${clientConfig.redirectURL}?state=state&code=code` 182 | ); 183 | const stateKey = AuthorizationCode.STATE_KEY; 184 | await sessionManager.setSessionItem(stateKey, 'state'); 185 | const client = new AuthorizationCode(clientConfig, clientSecret); 186 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 187 | expect(mocks.fetchClient).toHaveBeenCalledTimes(1); 188 | 189 | const foundRefreshToken = await sessionManager.getSessionItem('refresh_token'); 190 | const foundAccessToken = await sessionManager.getSessionItem('access_token'); 191 | const foundIdToken = await sessionManager.getSessionItem('id_token'); 192 | 193 | expect(foundAccessToken).toBe(mockAccessToken.token); 194 | expect(foundRefreshToken).toBe('refresh_token'); 195 | expect(foundIdToken).toBe(mockIdToken.token); 196 | }); 197 | }); 198 | 199 | describe('getToken()', () => { 200 | afterEach(async () => { 201 | mocks.fetchClient.mockClear(); 202 | await sessionManager.destroySession(); 203 | }); 204 | 205 | it('return an existing token if an unexpired token is available', async () => { 206 | const mockAccessToken = await mocks.getMockAccessToken( 207 | clientConfig.authDomain 208 | ); 209 | await sessionManager.setSessionItem('access_token', mockAccessToken.token); 210 | const client = new AuthorizationCode(clientConfig, clientSecret); 211 | const token = await client.getToken(sessionManager); 212 | expect(token).toBe(mockAccessToken.token); 213 | expect(mocks.fetchClient).not.toHaveBeenCalled(); 214 | }); 215 | 216 | it('throws an error if no access token is found in memory', async () => { 217 | await expect(async () => { 218 | const client = new AuthorizationCode(clientConfig, clientSecret); 219 | await client.getToken(sessionManager); 220 | }).rejects.toThrow('No authentication credential found'); 221 | }); 222 | 223 | it('throws an error if no refresh token is found in memory', async () => { 224 | const mockAccessToken = await mocks.getMockAccessToken( 225 | clientConfig.authDomain, 226 | true 227 | ); 228 | await sessionManager.setSessionItem('access_token', mockAccessToken.token); 229 | await expect(async () => { 230 | const client = new AuthorizationCode(clientConfig, clientSecret); 231 | await client.getToken(sessionManager); 232 | }).rejects.toThrow('Cannot persist session no valid refresh token found'); 233 | }); 234 | 235 | it('throws an exception when refreshing tokens returns an error response', async () => { 236 | const errorDescription = 'error_description'; 237 | mocks.fetchClient.mockResolvedValue({ 238 | json: () => ({ 239 | error: 'error', 240 | [errorDescription]: errorDescription, 241 | }), 242 | }); 243 | 244 | const expiredAccessToken = await mocks.getMockAccessToken( 245 | clientConfig.authDomain, 246 | true 247 | ); 248 | await sessionManager.setSessionItem('access_token', expiredAccessToken.token); 249 | await sessionManager.setSessionItem('refresh_token', 'refresh_token'); 250 | 251 | await expect(async () => { 252 | const client = new AuthorizationCode(clientConfig, clientSecret); 253 | await client.getToken(sessionManager); 254 | }).rejects.toThrow(errorDescription); 255 | expect(mocks.fetchClient).toHaveBeenCalled(); 256 | }); 257 | 258 | it('fetches new tokens if access token is expired and refresh token is available', async () => { 259 | const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain); 260 | const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain); 261 | mocks.fetchClient.mockResolvedValue({ 262 | json: () => ({ 263 | access_token: newAccessToken.token, 264 | refresh_token: 'new_refresh_token', 265 | id_token: newIdToken.token, 266 | }), 267 | }); 268 | 269 | const expiredAccessToken = await mocks.getMockAccessToken( 270 | clientConfig.authDomain, 271 | true 272 | ); 273 | await sessionManager.setSessionItem('access_token', expiredAccessToken.token); 274 | await sessionManager.setSessionItem('refresh_token', 'refresh_token'); 275 | 276 | const body = new URLSearchParams({ 277 | grant_type: 'refresh_token', 278 | client_id: clientConfig.clientId, 279 | client_secret: clientSecret, 280 | refresh_token: 'refresh_token', 281 | }); 282 | 283 | const headers = new Headers(); 284 | headers.append(...getSDKHeader()); 285 | headers.append( 286 | 'Content-Type', 287 | 'application/x-www-form-urlencoded; charset=UTF-8' 288 | ); 289 | 290 | const client = new AuthorizationCode(clientConfig, clientSecret); 291 | await client.getToken(sessionManager); 292 | expect(mocks.fetchClient).toHaveBeenCalledWith( 293 | `${clientConfig.authDomain}/oauth2/token`, 294 | { method: 'POST', headers, body, credentials: undefined } 295 | ); 296 | }); 297 | 298 | it('overrides SDK version header if options are provided to client constructor', async () => { 299 | const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain); 300 | const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain); 301 | mocks.fetchClient.mockResolvedValue({ 302 | json: () => ({ 303 | access_token: newAccessToken.token, 304 | refresh_token: 'new_refresh_token', 305 | id_token: newIdToken.token, 306 | }), 307 | }); 308 | 309 | const expiredAccessToken = await mocks.getMockAccessToken( 310 | clientConfig.authDomain, 311 | true 312 | ); 313 | await sessionManager.setSessionItem('access_token', expiredAccessToken.token); 314 | await sessionManager.setSessionItem('refresh_token', 'refresh_token'); 315 | 316 | const headerOverrides: SDKHeaderOverrideOptions = { 317 | framework: 'TypeScript-Framework', 318 | frameworkVersion: '1.1.1', 319 | }; 320 | 321 | const headers = new Headers(); 322 | headers.append(...getSDKHeader(headerOverrides)); 323 | headers.append( 324 | 'Content-Type', 325 | 'application/x-www-form-urlencoded; charset=UTF-8' 326 | ); 327 | 328 | const client = new AuthorizationCode( 329 | { 330 | ...clientConfig, 331 | ...headerOverrides, 332 | }, 333 | clientSecret 334 | ); 335 | await client.getToken(sessionManager); 336 | expect(mocks.fetchClient).toHaveBeenCalledWith( 337 | `${clientConfig.authDomain}/oauth2/token`, 338 | expect.objectContaining({ headers }) 339 | ); 340 | }); 341 | 342 | it('commits new tokens to memory if new tokens are fetched', async () => { 343 | const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain); 344 | const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain); 345 | const newRefreshToken = 'new_refresh_token'; 346 | 347 | mocks.fetchClient.mockResolvedValue({ 348 | json: () => ({ 349 | access_token: newAccessToken.token, 350 | refresh_token: newRefreshToken, 351 | id_token: newIdToken.token, 352 | }), 353 | }); 354 | 355 | const expiredAccessToken = await mocks.getMockAccessToken( 356 | clientConfig.authDomain, 357 | true 358 | ); 359 | await sessionManager.setSessionItem('access_token', expiredAccessToken.token); 360 | await sessionManager.setSessionItem('refresh_token', 'refresh_token'); 361 | 362 | const client = new AuthorizationCode(clientConfig, clientSecret); 363 | await client.getToken(sessionManager); 364 | expect(mocks.fetchClient).toHaveBeenCalledTimes(1); 365 | 366 | const foundRefreshToken = await sessionManager.getSessionItem('refresh_token'); 367 | const foundAccessToken = await sessionManager.getSessionItem('access_token'); 368 | const foundIdToken = await sessionManager.getSessionItem('id_token'); 369 | 370 | expect(foundAccessToken).toBe(newAccessToken.token); 371 | expect(foundRefreshToken).toBe(newRefreshToken); 372 | expect(foundIdToken).toBe(newIdToken.token); 373 | }); 374 | 375 | it('throws error if refreshTokens abstract method fails to refresh existing tokens', async () => { 376 | mocks.fetchClient.mockRejectedValue(Error('failed-refresh-attempt')); 377 | await sessionManager.setSessionItem('refresh_token', 'mines are here'); 378 | await sessionManager.setSessionItem( 379 | 'access_token', 380 | (await mocks.getMockAccessToken(clientConfig.authDomain, true)).token 381 | ); 382 | 383 | const client = new AuthorizationCode(clientConfig, clientSecret); 384 | const getTokenFn = async () => await client.getToken(sessionManager); 385 | await expect(getTokenFn).rejects.toBeInstanceOf(KindeSDKError); 386 | await expect(getTokenFn).rejects.toHaveProperty( 387 | 'errorCode', 388 | KindeSDKErrorCode.FAILED_TOKENS_REFRESH_ATTEMPT 389 | ); 390 | }); 391 | }); 392 | 393 | describe('getUserProfile()', () => { 394 | afterEach(async () => { 395 | mocks.fetchClient.mockClear(); 396 | await sessionManager.destroySession(); 397 | }); 398 | 399 | it('fetches user profile using the available access token', async () => { 400 | const mockAccessToken = await mocks.getMockAccessToken( 401 | clientConfig.authDomain 402 | ); 403 | await sessionManager.setSessionItem('access_token', mockAccessToken.token); 404 | 405 | const headers = new Headers(); 406 | headers.append('Authorization', `Bearer ${mockAccessToken.token}`); 407 | headers.append('Accept', 'application/json'); 408 | 409 | mocks.fetchClient.mockResolvedValue({ 410 | json: () => ({ 411 | family_name: 'family_name', 412 | given_name: 'give_name', 413 | email: 'test@test.com', 414 | picture: null, 415 | id: 'id', 416 | }), 417 | }); 418 | 419 | const client = new AuthorizationCode(clientConfig, clientSecret); 420 | await client.getUserProfile(sessionManager); 421 | expect(mocks.fetchClient).toHaveBeenCalledWith( 422 | `${clientConfig.authDomain}/oauth2/v2/user_profile`, 423 | { method: 'GET', headers } 424 | ); 425 | }); 426 | }); 427 | }); 428 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/oauth2-flows/ClientCredentials.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClientCredentials } from '../../../sdk/oauth2-flows/ClientCredentials'; 2 | import { type ClientCredentialsOptions } from '../../../sdk/oauth2-flows/types'; 3 | import { 4 | type TokenValidationDetailsType, 5 | commitTokenToSession, 6 | generateRandomString, 7 | } from '../../../sdk/utilities'; 8 | import { getSDKHeader } from '../../../sdk/version'; 9 | import * as mocks from '../../mocks'; 10 | import { describe, it, expect, beforeAll, afterEach } from 'vitest'; 11 | 12 | describe('ClientCredentials', () => { 13 | const clientConfig: ClientCredentialsOptions = { 14 | authDomain: 'https://local-testing@kinde.com', 15 | logoutRedirectURL: 'http://app-domain.com', 16 | clientSecret: generateRandomString(50), 17 | clientId: 'client-id', 18 | }; 19 | 20 | const { sessionManager } = mocks; 21 | 22 | describe('new ClientCredentials()', () => { 23 | it('can construct ClientCredentials instance', () => { 24 | expect(() => new ClientCredentials(clientConfig)).not.toThrowError(); 25 | }); 26 | }); 27 | 28 | describe('getToken()', () => { 29 | const tokenEndpoint = `${clientConfig.authDomain}/oauth2/token`; 30 | 31 | let validationDetails: TokenValidationDetailsType; 32 | 33 | beforeAll(async () => { 34 | validationDetails = { 35 | issuer: clientConfig.authDomain, 36 | }; 37 | }); 38 | 39 | const body = new URLSearchParams({ 40 | grant_type: 'client_credentials', 41 | client_id: clientConfig.clientId, 42 | client_secret: clientConfig.clientSecret, 43 | }); 44 | 45 | const headers = new Headers(); 46 | headers.append(...getSDKHeader()); 47 | headers.append( 48 | 'Content-Type', 49 | 'application/x-www-form-urlencoded; charset=UTF-8' 50 | ); 51 | 52 | afterEach(async () => { 53 | await sessionManager.destroySession(); 54 | mocks.fetchClient.mockClear(); 55 | }); 56 | 57 | it('throws an exception when fetching access token returns an error response', async () => { 58 | const errorDescription = 'error_description'; 59 | mocks.fetchClient.mockResolvedValue({ 60 | json: () => ({ 61 | error: 'error', 62 | [errorDescription]: errorDescription, 63 | }), 64 | }); 65 | 66 | const client = new ClientCredentials(clientConfig); 67 | await expect(async () => { 68 | await client.getToken(sessionManager); 69 | }).rejects.toThrow(errorDescription); 70 | expect(mocks.fetchClient).toHaveBeenCalled(); 71 | }); 72 | 73 | it('return access token if an unexpired token is available in memory', async () => { 74 | const { authDomain } = clientConfig; 75 | const { token: mockAccessToken } = await mocks.getMockAccessToken(authDomain); 76 | await commitTokenToSession( 77 | sessionManager, 78 | mockAccessToken, 79 | 'access_token', 80 | validationDetails 81 | ); 82 | 83 | const client = new ClientCredentials(clientConfig); 84 | const accessToken = await client.getToken(sessionManager); 85 | expect(mocks.fetchClient).not.toHaveBeenCalled(); 86 | expect(accessToken).toBe(mockAccessToken); 87 | }); 88 | 89 | it('fetches an access token if no access token is available in memory', async () => { 90 | const { token: mockAccessToken } = await mocks.getMockAccessToken( 91 | clientConfig.authDomain 92 | ); 93 | mocks.fetchClient.mockResolvedValue({ 94 | json: () => ({ access_token: mockAccessToken }), 95 | }); 96 | 97 | const client = new ClientCredentials(clientConfig); 98 | const accessToken = await client.getToken(sessionManager); 99 | expect(accessToken).toBe(mockAccessToken); 100 | expect(mocks.fetchClient).toHaveBeenCalledTimes(1); 101 | }); 102 | 103 | it('fetches an access token if available access token is expired', async () => { 104 | const { token: expiredMockAccessToken } = await mocks.getMockAccessToken( 105 | clientConfig.authDomain, 106 | true 107 | ); 108 | await sessionManager.setSessionItem('access_token', expiredMockAccessToken); 109 | const { token: mockAccessToken } = await mocks.getMockAccessToken( 110 | clientConfig.authDomain 111 | ); 112 | mocks.fetchClient.mockResolvedValue({ 113 | json: () => ({ access_token: mockAccessToken }), 114 | }); 115 | 116 | const client = new ClientCredentials(clientConfig); 117 | const accessToken = await client.getToken(sessionManager); 118 | expect(accessToken).toBe(mockAccessToken); 119 | expect(mocks.fetchClient).toHaveBeenCalledTimes(1); 120 | expect(mocks.fetchClient).toHaveBeenCalledWith(tokenEndpoint, { 121 | method: 'POST', 122 | headers, 123 | body, 124 | }); 125 | }); 126 | 127 | it('overrides scope and audience in token request body is provided', async () => { 128 | const { token: mockAccessToken } = await mocks.getMockAccessToken( 129 | clientConfig.authDomain 130 | ); 131 | mocks.fetchClient.mockResolvedValue({ 132 | json: () => ({ access_token: mockAccessToken }), 133 | }); 134 | 135 | const expectedScope = 'test-scope'; 136 | const expectedAudience = 'test-audience'; 137 | const client = new ClientCredentials({ 138 | ...clientConfig, 139 | audience: expectedAudience, 140 | scope: expectedScope, 141 | }); 142 | 143 | const expectedBody = new URLSearchParams({ 144 | grant_type: 'client_credentials', 145 | client_id: clientConfig.clientId, 146 | client_secret: clientConfig.clientSecret, 147 | scope: expectedScope, 148 | audience: expectedAudience, 149 | }); 150 | 151 | await client.getToken(sessionManager); 152 | expect(mocks.fetchClient).toHaveBeenCalledWith(tokenEndpoint, { 153 | method: 'POST', 154 | headers, 155 | body: expectedBody, 156 | }); 157 | }); 158 | 159 | it('commits access token to memory, when a new one is fetched', async () => { 160 | const mockAccessToken = await mocks.getMockAccessToken( 161 | clientConfig.authDomain 162 | ); 163 | mocks.fetchClient.mockResolvedValue({ 164 | json: () => ({ access_token: mockAccessToken.token }), 165 | }); 166 | 167 | const client = new ClientCredentials(clientConfig); 168 | await client.getToken(sessionManager); 169 | expect(mocks.fetchClient).toHaveBeenCalledTimes(1); 170 | expect(await sessionManager.getSessionItem('access_token')).toBe( 171 | mockAccessToken.token 172 | ); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/session-managers/BrowserSessionManager.browser.spec.ts: -------------------------------------------------------------------------------- 1 | import { BrowserSessionManager } from '../../../sdk/session-managers'; 2 | import { describe, it, expect, afterEach } from 'vitest'; 3 | 4 | describe('BrowserSessionManager', () => { 5 | const sessionManager = new BrowserSessionManager(); 6 | 7 | afterEach(async () => { 8 | await sessionManager.destroySession(); 9 | }); 10 | 11 | describe('new BrowserSessionManager()', () => { 12 | it('can construct BrowserSessionManager instance', () => { 13 | expect(() => new BrowserSessionManager()).not.toThrowError(); 14 | }); 15 | }); 16 | 17 | describe('destroySession()', () => { 18 | it('clears all items in session after being called', async () => { 19 | const sessionItemKey = 'session-item-key'; 20 | await sessionManager.setSessionItem(sessionItemKey, 'session-key-value'); 21 | await sessionManager.destroySession(); 22 | expect(await sessionManager.getSessionItem(sessionItemKey)).toBe(null); 23 | }); 24 | }); 25 | 26 | describe('setSessionItem()', () => { 27 | it('stores a value against the provided key in memory', async () => { 28 | const sessionItemKey = 'session-item-key'; 29 | const sessionItemValue = 'session-item-value'; 30 | await sessionManager.setSessionItem(sessionItemKey, sessionItemValue); 31 | expect(await sessionManager.getSessionItem(sessionItemKey)).toBe( 32 | sessionItemValue 33 | ); 34 | }); 35 | }); 36 | 37 | describe('removeSessionItem()', () => { 38 | it('removes a session item from memory', async () => { 39 | const sessionItemKey = 'session-item-key'; 40 | const sessionItemValue = 'session-item-value'; 41 | await sessionManager.setSessionItem(sessionItemKey, sessionItemValue); 42 | expect(await sessionManager.getSessionItem(sessionItemKey)).toBe( 43 | sessionItemValue 44 | ); 45 | await sessionManager.removeSessionItem(sessionItemKey); 46 | expect(await sessionManager.getSessionItem(sessionItemKey)).toBe(null); 47 | }); 48 | }); 49 | 50 | describe('setBrowserSessionItem()', () => { 51 | it("stores a value against the provided key in the browser's session storage", async () => { 52 | const sessionItemKey = 'session-item-key'; 53 | const sessionItemValue = 'session-item-value'; 54 | await sessionManager.setSessionItemBrowser(sessionItemKey, sessionItemValue); 55 | expect(await sessionManager.getSessionItemBrowser(sessionItemKey)).toBe( 56 | sessionItemValue 57 | ); 58 | }); 59 | }); 60 | 61 | describe('removeBrowserSessionItem()', () => { 62 | it("removes a session item from the browser's session storage", async () => { 63 | const sessionItemKey = 'session-item-key'; 64 | const sessionItemValue = 'session-item-value'; 65 | await sessionManager.setSessionItemBrowser(sessionItemKey, sessionItemValue); 66 | expect(await sessionManager.getSessionItemBrowser(sessionItemKey)).toBe( 67 | sessionItemValue 68 | ); 69 | await sessionManager.removeSessionItemBrowser(sessionItemKey); 70 | expect(await sessionManager.getSessionItemBrowser(sessionItemKey)).toBe(null); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/utilities/feature-flags.spec.ts: -------------------------------------------------------------------------------- 1 | import * as mocks from '../../mocks'; 2 | import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; 3 | 4 | import { 5 | type FeatureFlags, 6 | FlagDataType, 7 | getFlag, 8 | type TokenValidationDetailsType, 9 | } from '../../../sdk/utilities'; 10 | 11 | describe('feature-flags', () => { 12 | let mockAccessToken: Awaited>; 13 | const { sessionManager } = mocks; 14 | const authDomain = 'local-testing@kinde.com'; 15 | 16 | let validationDetails: TokenValidationDetailsType; 17 | 18 | beforeAll(async () => { 19 | validationDetails = { 20 | issuer: authDomain, 21 | }; 22 | }); 23 | 24 | beforeEach(async () => { 25 | mockAccessToken = await mocks.getMockAccessToken(); 26 | await sessionManager.setSessionItem('access_token', mockAccessToken.token); 27 | }); 28 | 29 | afterEach(async () => { 30 | await sessionManager.destroySession(); 31 | }); 32 | 33 | describe('getFlag', () => { 34 | it('throws error if no flag is found no defaultValue is given', async () => { 35 | const code = 'non-existant-code'; 36 | await expect( 37 | async () => await getFlag(sessionManager, code, validationDetails) 38 | ).rejects.toThrowError( 39 | new Error( 40 | `Flag ${code} was not found, and no default value has been provided` 41 | ) 42 | ); 43 | }); 44 | 45 | it('throw error if provided type is different from typeof of found flag', async () => { 46 | const featureFlags = mockAccessToken.payload.feature_flags as FeatureFlags; 47 | const code = 'is_dark_mode'; 48 | const flag = featureFlags[code]; 49 | await expect( 50 | async () => await getFlag(sessionManager, code, validationDetails, true, 's') 51 | ).rejects.toThrowError( 52 | new Error( 53 | `Flag ${code} is of type ${FlagDataType[flag!.t]}, expected type is ${ 54 | FlagDataType.s 55 | }` 56 | ) 57 | ); 58 | }); 59 | 60 | it('should not throw error for falsy default value which is not `undefined`', () => { 61 | const code = 'non-existant-code'; 62 | const getFlagFnArray = [ 63 | async () => 64 | await getFlag(sessionManager, code, validationDetails, false, 'b'), 65 | async () => await getFlag(sessionManager, code, validationDetails, '', 's'), 66 | async () => await getFlag(sessionManager, code, validationDetails, 0, 'i'), 67 | ]; 68 | 69 | getFlagFnArray.forEach((getFlagFn) => { 70 | expect(getFlagFn).not.toThrow(); 71 | }); 72 | }); 73 | 74 | it('provide result contains no type if default-value is used', async () => { 75 | const defaultValue = 'default-value'; 76 | const code = 'non-existant-code'; 77 | expect( 78 | await getFlag(sessionManager, code, validationDetails, defaultValue) 79 | ).toStrictEqual({ 80 | value: defaultValue, 81 | is_default: true, 82 | code, 83 | }); 84 | }); 85 | 86 | it('retrieves flag data for a defined feature flag', async () => { 87 | const featureFlags = mockAccessToken.payload.feature_flags as FeatureFlags; 88 | for (const code in featureFlags) { 89 | const flag = featureFlags[code]; 90 | expect(await getFlag(sessionManager, code, validationDetails)).toStrictEqual( 91 | { 92 | is_default: false, 93 | value: flag!.v, 94 | type: FlagDataType[flag!.t], 95 | code, 96 | } 97 | ); 98 | } 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/utilities/generateRandomString.spec.ts: -------------------------------------------------------------------------------- 1 | import { generateRandomString } from '../../../sdk/utilities'; 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | describe('validateClientSecret', () => { 5 | it('should return true for valid secrets', () => { 6 | const result = generateRandomString(25); 7 | expect(result.length).toBe(25); 8 | }); 9 | 10 | it('should return false for invalid secrets - odd length', () => { 11 | const result = generateRandomString(47); 12 | expect(result.length).toBe(47); 13 | }); 14 | 15 | it('should return false for invalid secrets', () => { 16 | const result = generateRandomString(50); 17 | expect(result.length).toBe(50); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/utilities/token-claims.spec.ts: -------------------------------------------------------------------------------- 1 | import * as mocks from '../../mocks'; 2 | 3 | import { 4 | getUserOrganizations, 5 | getOrganization, 6 | getClaimValue, 7 | getPermission, 8 | getClaim, 9 | type TokenValidationDetailsType, 10 | } from '../../../sdk/utilities'; 11 | import { describe, it, expect, beforeAll, afterAll } from 'vitest'; 12 | 13 | describe('token-claims', () => { 14 | let mockAccessToken: Awaited>; 15 | let mockIdToken: Awaited>; 16 | const authDomain = 'local-testing@kinde.com'; 17 | const { sessionManager } = mocks; 18 | 19 | let validationDetails: TokenValidationDetailsType; 20 | 21 | beforeAll(async () => { 22 | validationDetails = { 23 | issuer: authDomain, 24 | }; 25 | mockAccessToken = await mocks.getMockAccessToken(); 26 | mockIdToken = await mocks.getMockIdToken(); 27 | await sessionManager.setSessionItem('access_token', mockAccessToken.token); 28 | await sessionManager.setSessionItem('id_token', mockIdToken.token); 29 | }); 30 | 31 | afterAll(async () => { 32 | await sessionManager.destroySession(); 33 | }); 34 | 35 | describe('getClaimValue', () => { 36 | it('returns value for a token claim if claim exists', () => { 37 | Object.keys(mockAccessToken.payload).forEach(async (name: string) => { 38 | const claimValue = await getClaimValue( 39 | sessionManager, 40 | name, 41 | 'access_token', 42 | validationDetails 43 | ); 44 | const tokenPayload = mockAccessToken.payload as Record; 45 | expect(claimValue).toStrictEqual(tokenPayload[name]); 46 | }); 47 | }); 48 | 49 | it('return null if claim does not exist', async () => { 50 | const claimName = 'non-existant-claim'; 51 | const claimValue = await getClaimValue( 52 | sessionManager, 53 | claimName, 54 | 'access_token', 55 | validationDetails 56 | ); 57 | expect(claimValue).toBe(null); 58 | }); 59 | }); 60 | 61 | describe('getClaim', () => { 62 | it('returns value for a token claim if claim exists', () => { 63 | Object.keys(mockAccessToken.payload).forEach(async (name: string) => { 64 | const claim = await getClaim( 65 | sessionManager, 66 | name, 67 | 'access_token', 68 | validationDetails 69 | ); 70 | const tokenPayload = mockAccessToken.payload as Record; 71 | expect(claim).toStrictEqual({ name, value: tokenPayload[name] }); 72 | }); 73 | }); 74 | 75 | it('return null if claim does not exist', async () => { 76 | const claimName = 'non-existant-claim'; 77 | const claim = await getClaim( 78 | sessionManager, 79 | claimName, 80 | 'access_token', 81 | validationDetails 82 | ); 83 | expect(claim).toStrictEqual({ name: claimName, value: null }); 84 | }); 85 | }); 86 | 87 | describe('getPermission', () => { 88 | it('return orgCode and isGranted = true if permission is given', () => { 89 | const { permissions } = mockAccessToken.payload; 90 | permissions.forEach(async (permission) => { 91 | expect( 92 | await getPermission(sessionManager, permission, validationDetails) 93 | ).toStrictEqual({ 94 | orgCode: mockAccessToken.payload.org_code, 95 | isGranted: true, 96 | }); 97 | }); 98 | }); 99 | 100 | it('return isGranted = false is permission is not given', async () => { 101 | const orgCode = mockAccessToken.payload.org_code; 102 | const permissionName = 'non-existant-permission'; 103 | expect( 104 | await getPermission(sessionManager, permissionName, validationDetails) 105 | ).toStrictEqual({ 106 | orgCode, 107 | isGranted: false, 108 | }); 109 | }); 110 | }); 111 | describe('getUserOrganizations', () => { 112 | it('lists all user organizations using id token', async () => { 113 | const orgCodes = mockIdToken.payload.org_codes; 114 | expect( 115 | await getUserOrganizations(sessionManager, validationDetails) 116 | ).toStrictEqual({ 117 | orgCodes, 118 | }); 119 | }); 120 | }); 121 | 122 | describe('getOrganization', () => { 123 | it('returns organization code using accesss token', async () => { 124 | const orgCode = mockAccessToken.payload.org_code; 125 | expect(await getOrganization(sessionManager, validationDetails)).toStrictEqual( 126 | { orgCode } 127 | ); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/utilities/token-utils.spec.ts: -------------------------------------------------------------------------------- 1 | import * as mocks from '../../mocks'; 2 | import { describe, it, expect, beforeAll, afterEach } from 'vitest'; 3 | import { 4 | type TokenCollection, 5 | commitTokensToSession, 6 | commitTokenToSession, 7 | isTokenExpired, 8 | type TokenValidationDetailsType, 9 | getUserFromSession, 10 | } from '../../../sdk/utilities'; 11 | 12 | import { KindeSDKError, KindeSDKErrorCode } from '../../../sdk/exceptions'; 13 | 14 | describe('token-utils', () => { 15 | const domain = 'local-testing@kinde.com'; 16 | const { sessionManager } = mocks; 17 | let validationDetails: TokenValidationDetailsType; 18 | 19 | beforeAll(async () => { 20 | validationDetails = { 21 | issuer: domain, 22 | }; 23 | }); 24 | 25 | describe('commitTokensToMemory', () => { 26 | it('stores all provided tokens to memory', async () => { 27 | const { token: mockAccessToken } = await mocks.getMockAccessToken(domain); 28 | const { token: mockIdToken } = await mocks.getMockAccessToken(domain); 29 | const tokenCollection: TokenCollection = { 30 | refresh_token: 'refresh_token', 31 | access_token: mockAccessToken, 32 | id_token: mockIdToken, 33 | }; 34 | await commitTokensToSession( 35 | sessionManager, 36 | tokenCollection, 37 | validationDetails 38 | ); 39 | 40 | expect(await sessionManager.getSessionItem('refresh_token')).toBe( 41 | tokenCollection.refresh_token 42 | ); 43 | expect(await sessionManager.getSessionItem('access_token')).toBe( 44 | mockAccessToken 45 | ); 46 | expect(await sessionManager.getSessionItem('id_token')).toBe(mockIdToken); 47 | }); 48 | }); 49 | 50 | describe('commitTokenToMemory()', () => { 51 | afterEach(async () => { 52 | await sessionManager.destroySession(); 53 | }); 54 | 55 | it('stores provided token to memory', async () => { 56 | const { token: mockAccessToken } = await mocks.getMockAccessToken(domain); 57 | await commitTokenToSession( 58 | sessionManager, 59 | mockAccessToken, 60 | 'access_token', 61 | validationDetails 62 | ); 63 | expect(await sessionManager.getSessionItem('access_token')).toBe( 64 | mockAccessToken 65 | ); 66 | }); 67 | 68 | it('throws exception if attempting to store invalid token', async () => { 69 | const { token: mockAccessToken } = await mocks.getMockAccessToken( 70 | domain, 71 | true 72 | ); 73 | const commitTokenFn = async () => { 74 | await commitTokenToSession( 75 | sessionManager, 76 | mockAccessToken, 77 | 'access_token', 78 | validationDetails 79 | ); 80 | }; 81 | await expect(commitTokenFn).rejects.toBeInstanceOf(KindeSDKError); 82 | await expect(commitTokenFn).rejects.toHaveProperty( 83 | 'errorCode', 84 | KindeSDKErrorCode.INVALID_TOKEN_MEMORY_COMMIT 85 | ); 86 | }); 87 | 88 | it('stores user information if provide token is an id token', async () => { 89 | const { token: mockIdToken, payload: idTokenPayload } = 90 | await mocks.getMockIdToken(domain); 91 | await commitTokenToSession( 92 | sessionManager, 93 | mockIdToken, 94 | 'id_token', 95 | validationDetails 96 | ); 97 | 98 | const storedUser = await getUserFromSession(sessionManager, validationDetails); 99 | const expectedUser = { 100 | family_name: idTokenPayload.family_name, 101 | given_name: idTokenPayload.given_name, 102 | email: idTokenPayload.email, 103 | id: idTokenPayload.sub, 104 | picture: null, 105 | phone: undefined, 106 | }; 107 | 108 | expect(await sessionManager.getSessionItem('id_token')).toBe(mockIdToken); 109 | expect(storedUser).toStrictEqual(expectedUser); 110 | }); 111 | }); 112 | 113 | describe('isTokenExpired()', () => { 114 | it('returns true if null is provided as argument', async () => { 115 | expect(await isTokenExpired(null, validationDetails)).toBe(true); 116 | }); 117 | 118 | it('returns true if provided token is expired', async () => { 119 | const { token: mockAccessToken } = await mocks.getMockAccessToken( 120 | domain, 121 | true 122 | ); 123 | expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(true); 124 | }); 125 | 126 | it('returns true if provided token is missing "exp" claim', async () => { 127 | const { token: mockAccessToken } = await mocks.getMockAccessToken( 128 | domain, 129 | true 130 | ); 131 | expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(true); 132 | }); 133 | 134 | it('returns false if provided token is not expired', async () => { 135 | const { token: mockAccessToken } = await mocks.getMockAccessToken(domain); 136 | expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(false); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /lib/__tests__/sdk/utilities/validate-client-secret.spec.ts: -------------------------------------------------------------------------------- 1 | import { validateClientSecret } from '../../../sdk/utilities'; 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | describe('validateClientSecret', () => { 5 | it('should return true for valid secrets', () => { 6 | const validSecret = 'HlibujiUbwbMXofgh12F7Abur5JM5FZCDZHJQenpwEO7UCsNnqzm'; 7 | const result = validateClientSecret(validSecret); 8 | expect(result).toBe(true); 9 | }); 10 | 11 | it('should return false for invalid secrets', () => { 12 | const invalidSecret = '123'; 13 | const result = validateClientSecret(invalidSecret); 14 | expect(result).toBe(false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/sdk/clients/browser/authcode-with-pkce.ts: -------------------------------------------------------------------------------- 1 | import { BrowserSessionManager } from '../../session-managers/index.js'; 2 | import { AuthCodeWithPKCE } from '../../oauth2-flows/index.js'; 3 | import * as utilities from '../../utilities/index.js'; 4 | import type { GeneratePortalUrlParams } from '@kinde/js-utils'; 5 | 6 | import type { 7 | UserType, 8 | ClaimTokenType, 9 | GetFlagType, 10 | FlagType, 11 | } from '../../utilities/index.js'; 12 | 13 | import type { 14 | CreateOrgURLOptions, 15 | RegisterURLOptions, 16 | LoginURLOptions, 17 | BrowserPKCEClientOptions, 18 | } from '../types.js'; 19 | 20 | import type { OAuth2CodeExchangeResponse } from '../../oauth2-flows/types.js'; 21 | 22 | const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { 23 | const { featureFlags, tokenClaims } = utilities; 24 | const sessionManager = options.sessionManager ?? new BrowserSessionManager(); 25 | const client = new AuthCodeWithPKCE(options); 26 | 27 | /** 28 | * Method makes use of the `createAuthorizationURL` method of the AuthCodeWithPKCE 29 | * client above to return login url. 30 | * @param {LoginURLOptions} options 31 | * @returns {Promise} required authorization URL 32 | */ 33 | const login = async (options?: LoginURLOptions): Promise => { 34 | return await client.createAuthorizationURL(sessionManager, { 35 | ...options, 36 | }); 37 | }; 38 | 39 | /** 40 | * Method makes use of the `createAuthorizationURL` method of the AuthCodeWithPKCE 41 | * client above to return registration url. 42 | * @param {RegisterURLOptions} options 43 | * @returns {Promise} required authorization URL 44 | */ 45 | const register = async (options?: RegisterURLOptions): Promise => { 46 | return await client.createAuthorizationURL(sessionManager, { 47 | ...options, 48 | start_page: 'registration', 49 | }); 50 | }; 51 | 52 | /** 53 | * Method makes use of the `createAuthorizationURL` method of the AuthCodeWithPKCE 54 | * client above to return registration url with the `is_create_org` query param 55 | * set to true. 56 | * @param {CreateOrgURLOptions} options 57 | * @returns {Promise} required authorization URL 58 | */ 59 | const createOrg = async (options?: CreateOrgURLOptions): Promise => { 60 | return await client.createAuthorizationURL(sessionManager, { 61 | ...options, 62 | start_page: 'registration', 63 | is_create_org: true, 64 | }); 65 | }; 66 | 67 | /** 68 | * Method makes use of the `createPortalUrl` method of the AuthCodeWithPKCE 69 | * client above to return portal url. 70 | * @param {GeneratePortalUrlParams} options 71 | * @returns {Promise<{url: URL}>} portal URL 72 | */ 73 | const portal = async (options: GeneratePortalUrlParams): Promise<{ url: URL }> => { 74 | return await client.createPortalUrl(sessionManager, { 75 | ...options, 76 | }); 77 | }; 78 | 79 | /** 80 | * Method makes use of the `handleRedirectFromAuthDomain` method of the 81 | * `AuthCodeWithPKCE` client above to handle the redirection back to the app. 82 | * @param {URL} callbackURL 83 | * @returns {Promise} 84 | */ 85 | const handleRedirectToApp = async (callbackURL: URL): Promise => { 86 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 87 | }; 88 | 89 | /** 90 | * Method acts as a wrapper around the `isAuthenticated` method provided by the 91 | * `AuthCodeWithPKCE` client created above. 92 | * @returns {Promise} 93 | */ 94 | const isAuthenticated = async (): Promise => { 95 | return await client.isAuthenticated(sessionManager); 96 | }; 97 | 98 | /** 99 | * Method makes use of the `getUserProfile` method of the `AuthCodeWithPKCE` client 100 | * above to fetch the current user's information, raises exception if current user 101 | * is not authenticated. 102 | * @returns {Promise} 103 | */ 104 | const getUserProfile = async (): Promise => { 105 | return await client.getUserProfile(sessionManager); 106 | }; 107 | 108 | /** 109 | * Method extracts the current user's details from the current session, raises 110 | * exception if current user is not authenticated. 111 | * @returns {Promise} 112 | */ 113 | const getUser = async (): Promise => { 114 | if (!(await isAuthenticated())) { 115 | throw new Error('Cannot get user details, no authentication credential found'); 116 | } 117 | return (await utilities.getUserFromSession( 118 | sessionManager, 119 | client.tokenValidationDetails 120 | ))!; 121 | }; 122 | 123 | /** 124 | * Method extracts the provided number feature flag from the access token in 125 | * the current session. 126 | * @param {string} code 127 | * @param {number} defaultValue 128 | * @returns {number} integer flag value 129 | */ 130 | const getIntegerFlag = async ( 131 | code: string, 132 | defaultValue?: number 133 | ): Promise => { 134 | if (!(await isAuthenticated())) { 135 | throw new Error( 136 | `Cannot return integer flag "${code}", no authentication credential found` 137 | ); 138 | } 139 | return await featureFlags.getIntegerFlag( 140 | sessionManager, 141 | code, 142 | client.tokenValidationDetails, 143 | defaultValue 144 | ); 145 | }; 146 | 147 | /** 148 | * Method extracts the provided string feature flag from the access token in 149 | * the current session. 150 | * @param {string} code 151 | * @param {string} defaultValue 152 | * @returns {string} string flag value 153 | */ 154 | const getStringFlag = async ( 155 | code: string, 156 | defaultValue?: string 157 | ): Promise => { 158 | if (!(await isAuthenticated())) { 159 | throw new Error( 160 | `Cannot return string flag "${code}", no authentication credential found` 161 | ); 162 | } 163 | return await featureFlags.getStringFlag( 164 | sessionManager, 165 | code, 166 | client.tokenValidationDetails, 167 | defaultValue 168 | ); 169 | }; 170 | 171 | /** 172 | * Method extracts the provided boolean feature flag from the access token in 173 | * the current session. 174 | * @param {string} code 175 | * @param {boolean} defaultValue 176 | * @returns {boolean} boolean flag value 177 | */ 178 | const getBooleanFlag = async ( 179 | code: string, 180 | defaultValue?: boolean 181 | ): Promise => { 182 | if (!(await isAuthenticated())) { 183 | throw new Error( 184 | `Cannot return boolean flag "${code}", no authentication credential found` 185 | ); 186 | } 187 | return await featureFlags.getBooleanFlag( 188 | sessionManager, 189 | code, 190 | client.tokenValidationDetails, 191 | defaultValue 192 | ); 193 | }; 194 | 195 | /** 196 | * Method extracts the provided claim from the provided token type in the 197 | * current session. 198 | * @param {string} claim 199 | * @param {ClaimTokenType} type 200 | * @returns {unknown | null} 201 | */ 202 | const getClaimValue = async ( 203 | claim: string, 204 | type: ClaimTokenType = 'access_token' 205 | ): Promise => { 206 | if (!(await isAuthenticated())) { 207 | throw new Error( 208 | `Cannot return claim "${claim}", no authentication credential found` 209 | ); 210 | } 211 | 212 | return await tokenClaims.getClaimValue( 213 | sessionManager, 214 | claim, 215 | type, 216 | client.tokenValidationDetails 217 | ); 218 | }; 219 | 220 | /** 221 | * Method extracts the provided claim from the provided token type in the 222 | * current session, the returned object includes the provided claim. 223 | * @param {string} claim 224 | * @param {ClaimTokenType} type 225 | * @returns {{ name: string, value: unknown | null }} 226 | */ 227 | const getClaim = async ( 228 | claim: string, 229 | type: ClaimTokenType = 'access_token' 230 | ): Promise<{ name: string; value: unknown | null }> => { 231 | if (!(await isAuthenticated())) { 232 | throw new Error( 233 | `Cannot return claim "${claim}", no authentication credential found` 234 | ); 235 | } 236 | return await tokenClaims.getClaim( 237 | sessionManager, 238 | claim, 239 | type, 240 | client.tokenValidationDetails 241 | ); 242 | }; 243 | 244 | /** 245 | * Method returns the organization code from the current session and returns 246 | * a boolean in the returned object indicating if the provided permission is 247 | * present in the session. 248 | * @param {string} name 249 | * @returns {{ orgCode: string | null, isGranted: boolean }} 250 | */ 251 | const getPermission = async ( 252 | name: string 253 | ): Promise<{ orgCode: string | null; isGranted: boolean }> => { 254 | if (!(await isAuthenticated())) { 255 | throw new Error( 256 | `Cannot return permission "${name}", no authentication credential found` 257 | ); 258 | } 259 | return await tokenClaims.getPermission( 260 | sessionManager, 261 | name, 262 | client.tokenValidationDetails 263 | ); 264 | }; 265 | 266 | /** 267 | * Method extracts the organization code from the current session. 268 | * @returns {{ orgCode: string | null }} 269 | */ 270 | const getOrganization = async (): Promise<{ orgCode: string | null }> => { 271 | if (!(await isAuthenticated())) { 272 | throw new Error( 273 | 'Cannot return user organization, no authentication credential found' 274 | ); 275 | } 276 | return await tokenClaims.getOrganization( 277 | sessionManager, 278 | client.tokenValidationDetails 279 | ); 280 | }; 281 | 282 | /** 283 | * Method extracts all organization codes from the id token in the current 284 | * session. 285 | * @returns {{ orgCodes: string[] }} 286 | */ 287 | const getUserOrganizations = async (): Promise<{ orgCodes: string[] }> => { 288 | if (!(await isAuthenticated())) { 289 | throw new Error( 290 | 'Cannot return user organizations, no authentication credential found' 291 | ); 292 | } 293 | return await tokenClaims.getUserOrganizations( 294 | sessionManager, 295 | client.tokenValidationDetails 296 | ); 297 | }; 298 | 299 | /** 300 | * Method extracts all the permission and the organization code in the access 301 | * token in the current session. 302 | * @returns {{ permissions: string[], orgCode: string | null }} 303 | */ 304 | const getPermissions = async (): Promise<{ 305 | permissions: string[]; 306 | orgCode: string | null; 307 | }> => { 308 | if (!(await isAuthenticated())) { 309 | throw new Error( 310 | 'Cannot return user permissions, no authentication credential found' 311 | ); 312 | } 313 | return await tokenClaims.getPermissions( 314 | sessionManager, 315 | client.tokenValidationDetails 316 | ); 317 | }; 318 | 319 | /** 320 | * Method makes use of the `getToken` of the `AuthCodeWithPKCE` client above 321 | * to return the access token from the current session. 322 | * @returns {Promise} 323 | */ 324 | const getToken = async (): Promise => { 325 | return await client.getToken(sessionManager); 326 | }; 327 | 328 | /** 329 | * Method makes user of the `refreshTokens` method of the `AuthCodeWithPKCE` client 330 | * to use the refresh token to get new tokens 331 | * @returns {Promise} 332 | */ 333 | const refreshTokens = async (): Promise => { 334 | return await client.refreshTokens(sessionManager); 335 | }; 336 | 337 | /** 338 | * Method extracts the provided feature flag from the access token in the 339 | * current session. 340 | * @param {string} code 341 | * @param {FlagType[keyof FlagType]} defaultValue 342 | * @param {keyof FlagType} type 343 | * @returns {GetFlagType} 344 | */ 345 | const getFlag = async ( 346 | code: string, 347 | defaultValue?: FlagType[keyof FlagType], 348 | type?: keyof FlagType 349 | ): Promise => { 350 | if (!(await isAuthenticated())) { 351 | throw new Error( 352 | `Cannot return flag "${code}", no authentication credential found` 353 | ); 354 | } 355 | return await featureFlags.getFlag( 356 | sessionManager, 357 | code, 358 | client.tokenValidationDetails, 359 | defaultValue, 360 | type 361 | ); 362 | }; 363 | 364 | /** 365 | * Method clears the current session and returns the logout URL, redirecting 366 | * to which will clear the user's session on the authorization server. 367 | * @returns {URL} 368 | */ 369 | const logout = async (): Promise => { 370 | await sessionManager.destroySession(); 371 | return new URL(client.logoutEndpoint); 372 | }; 373 | 374 | return { 375 | getUserOrganizations, 376 | handleRedirectToApp, 377 | isAuthenticated, 378 | getOrganization, 379 | getBooleanFlag, 380 | getIntegerFlag, 381 | getUserProfile, 382 | getPermissions, 383 | getPermission, 384 | getClaimValue, 385 | getStringFlag, 386 | createOrg, 387 | getClaim, 388 | getToken, 389 | refreshTokens, 390 | register, 391 | getUser, 392 | getFlag, 393 | logout, 394 | login, 395 | portal, 396 | }; 397 | }; 398 | 399 | export default createAuthCodeWithPKCEClient; 400 | -------------------------------------------------------------------------------- /lib/sdk/clients/browser/index.ts: -------------------------------------------------------------------------------- 1 | import createAuthCodeWithPKCEClient from './authcode-with-pkce.js'; 2 | import { isBrowserEnvironment } from '../../environment.js'; 3 | import type { BrowserPKCEClientOptions } from '../types.js'; 4 | 5 | export const createKindeBrowserClient = (options: BrowserPKCEClientOptions) => { 6 | if (!isBrowserEnvironment()) { 7 | throw new Error('this method must be invoked in a browser environment'); 8 | } 9 | 10 | return createAuthCodeWithPKCEClient(options); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/sdk/clients/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browser/index.js'; 2 | export * from './server/index.js'; 3 | export * from './types.js'; 4 | -------------------------------------------------------------------------------- /lib/sdk/clients/server/authorization-code.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizationCode, AuthCodeWithPKCE } from '../../oauth2-flows/index.js'; 2 | import withAuthUtilities from './with-auth-utilities.js'; 3 | import { type SessionManager } from '../../session-managers/index.js'; 4 | import type { UserType } from '../../utilities/index.js'; 5 | import * as utilities from '../../utilities/index.js'; 6 | 7 | import type { 8 | CreateOrgURLOptions, 9 | RegisterURLOptions, 10 | LoginURLOptions, 11 | ACClientOptions, 12 | } from '../types.js'; 13 | 14 | import type { OAuth2CodeExchangeResponse } from '../../oauth2-flows/types.js'; 15 | import type { GeneratePortalUrlParams } from '@kinde/js-utils'; 16 | 17 | const createAuthorizationCodeClient = ( 18 | options: ACClientOptions, 19 | isPKCE: boolean 20 | ) => { 21 | const client = !isPKCE 22 | ? new AuthorizationCode(options, options.clientSecret!) 23 | : new AuthCodeWithPKCE(options); 24 | 25 | /** 26 | * Method makes use of the `createAuthorizationURL` method of the AuthCodeAbstract 27 | * client above to return login url. 28 | * @param {SessionManager} sessionManager 29 | * @param {LoginURLOptions} options 30 | * @returns {Promise} required authorization URL 31 | */ 32 | const login = async ( 33 | sessionManager: SessionManager, 34 | options?: LoginURLOptions 35 | ): Promise => { 36 | return await client.createAuthorizationURL(sessionManager, { 37 | ...options, 38 | }); 39 | }; 40 | 41 | /** 42 | * Method makes use of the `createAuthorizationURL` method of the AuthCodeAbstract 43 | * client above to return registration url. 44 | * @param {SessionManager} sessionManager 45 | * @param {RegisterURLOptions} options 46 | * @returns {Promise} required authorization URL 47 | */ 48 | const register = async ( 49 | sessionManager: SessionManager, 50 | options?: RegisterURLOptions 51 | ): Promise => { 52 | return await client.createAuthorizationURL(sessionManager, { 53 | ...options, 54 | start_page: 'registration', 55 | }); 56 | }; 57 | 58 | /** 59 | * Method makes use of the `createAuthorizationURL` method of the AuthCodeAbstract 60 | * client above to return registration url with the `is_create_org` query param 61 | * set to true. 62 | * @param {SessionManager} sessionManager 63 | * @param {CreateOrgURLOptions} options 64 | * @returns {Promise} required authorization URL 65 | */ 66 | const createOrg = async ( 67 | sessionManager: SessionManager, 68 | options?: CreateOrgURLOptions 69 | ): Promise => { 70 | return await client.createAuthorizationURL(sessionManager, { 71 | ...options, 72 | start_page: 'registration', 73 | is_create_org: true, 74 | }); 75 | }; 76 | 77 | /** 78 | * Method makes use of the `createPortalUrl` method of the AuthCodeAbstract 79 | * client above to return login url. 80 | * @param {SessionManager} sessionManager 81 | * @param {GeneratePortalUrlParams} options 82 | * @returns {Promise<{ url: URL }>} required authorization URL 83 | */ 84 | const portal = async ( 85 | sessionManager: SessionManager, 86 | options: GeneratePortalUrlParams 87 | ): Promise<{ url: URL }> => { 88 | return await client.createPortalUrl(sessionManager, { 89 | ...options, 90 | }); 91 | }; 92 | 93 | /** 94 | * Method makes use of the `handleRedirectFromAuthDomain` method of the 95 | * `AuthCodeAbstract` client above to handle the redirection back to the app. 96 | * @param {SessionManager} sessionManager 97 | * @param {URL} callbackURL 98 | * @returns {Promise} 99 | */ 100 | const handleRedirectToApp = async ( 101 | sessionManager: SessionManager, 102 | callbackURL: URL 103 | ): Promise => { 104 | await client.handleRedirectFromAuthDomain(sessionManager, callbackURL); 105 | }; 106 | 107 | /** 108 | * Method acts as a wrapper around the `isAuthenticated` method provided by the 109 | * `AuthCodeAbstract` client created above. 110 | * @param {SessionManager} sessionManager 111 | * @returns {Promise} 112 | */ 113 | const isAuthenticated = async ( 114 | sessionManager: SessionManager 115 | ): Promise => { 116 | return await client.isAuthenticated(sessionManager); 117 | }; 118 | 119 | /** 120 | * Method makes use of the `getUserProfile` method of the `AuthCodeAbstract` client 121 | * above to fetch the current user's information, raises exception if current user 122 | * is not authenticated. 123 | * @param {SessionManager} sessionManager 124 | * @returns {Promise} 125 | */ 126 | const getUserProfile = async ( 127 | sessionManager: SessionManager 128 | ): Promise => { 129 | return await client.getUserProfile(sessionManager); 130 | }; 131 | 132 | /** 133 | * Method extracts the current user's details from the current session, raises 134 | * exception if current user is not authenticated. 135 | * @param {SessionManager} sessionManager 136 | * @returns {Promise} 137 | */ 138 | const getUser = async (sessionManager: SessionManager): Promise => { 139 | if (!(await isAuthenticated(sessionManager))) { 140 | throw new Error('Cannot get user details, no authentication credential found'); 141 | } 142 | return (await utilities.getUserFromSession( 143 | sessionManager, 144 | client.tokenValidationDetails 145 | ))!; 146 | }; 147 | 148 | /** 149 | * Method makes use of the `getToken` method of the `AuthCodeAbstract` client 150 | * to retrieve an access token. 151 | * @param sessionManager 152 | * @returns {Promise} 153 | */ 154 | const getToken = async (sessionManager: SessionManager): Promise => { 155 | return await client.getToken(sessionManager); 156 | }; 157 | 158 | /** 159 | * Method makes user of the `refreshTokens` method of the `AuthCodeAbstract` client 160 | * to use the refresh token to get new tokens 161 | * @param {SessionManager} sessionManager 162 | * @param {boolean} [commitToSession=true] - Optional parameter, determines whether to commit the refreshed tokens to the session. Defaults to true. 163 | * @returns {Promise} 164 | */ 165 | const refreshTokens = async ( 166 | sessionManager: SessionManager, 167 | commitToSession: boolean = true 168 | ): Promise => { 169 | return await client.refreshTokens(sessionManager, commitToSession); 170 | }; 171 | 172 | /** 173 | * Method clears the current session and returns the logout URL, redirecting 174 | * to which will clear the user's session on the authorization server. 175 | * @param {SessionManager} sessionManager 176 | * @returns {URL} 177 | */ 178 | const logout = async (sessionManager: SessionManager): Promise => { 179 | await sessionManager.destroySession(); 180 | return new URL(client.logoutEndpoint); 181 | }; 182 | 183 | return { 184 | ...withAuthUtilities(isAuthenticated, client.tokenValidationDetails), 185 | handleRedirectToApp, 186 | isAuthenticated, 187 | getUserProfile, 188 | createOrg, 189 | getToken, 190 | refreshTokens, 191 | register, 192 | getUser, 193 | logout, 194 | login, 195 | portal, 196 | }; 197 | }; 198 | 199 | export default createAuthorizationCodeClient; 200 | -------------------------------------------------------------------------------- /lib/sdk/clients/server/client-credentials.ts: -------------------------------------------------------------------------------- 1 | import withAuthUtilities from './with-auth-utilities.js'; 2 | import { type SessionManager } from '../../session-managers/index.js'; 3 | import { ClientCredentials } from '../../oauth2-flows/index.js'; 4 | import type { CCClientOptions } from '../types.js'; 5 | 6 | const createCCClient = (options: CCClientOptions) => { 7 | const client = new ClientCredentials(options); 8 | 9 | /** 10 | * Method clears the current session and returns the logout URL, redirecting 11 | * to which will clear the user's session on the authorization server. 12 | * @param {SessionManager} sessionManager 13 | * @returns {URL} 14 | */ 15 | const logout = async (sessionManager: SessionManager): Promise => { 16 | await sessionManager.destroySession(); 17 | return new URL(client.logoutEndpoint); 18 | }; 19 | 20 | /** 21 | * Method makes use of the `getToken` method of the `ClientCredentials` client 22 | * to retrieve an access token. 23 | * @param sessionManager 24 | * @returns {Promise} 25 | */ 26 | const getToken = async (sessionManager: SessionManager): Promise => { 27 | return await client.getToken(sessionManager); 28 | }; 29 | 30 | /** 31 | * Method acts as a wrapper around the `isAuthenticated` method provided by the 32 | * `ClientCredentials` client created above. 33 | * @param {SessionManager} sessionManager 34 | * @returns {Promise} 35 | */ 36 | const isAuthenticated = async ( 37 | sessionManager: SessionManager 38 | ): Promise => { 39 | return await client.isAuthenticated(sessionManager); 40 | }; 41 | 42 | return { 43 | ...withAuthUtilities(isAuthenticated, client.tokenValidationDetails), 44 | isAuthenticated, 45 | getToken, 46 | logout, 47 | }; 48 | }; 49 | 50 | export default createCCClient; 51 | -------------------------------------------------------------------------------- /lib/sdk/clients/server/index.ts: -------------------------------------------------------------------------------- 1 | import createAuthCodeClient from './authorization-code.js'; 2 | import createCCClient from './client-credentials.js'; 3 | import { GrantType } from '../../oauth2-flows/index.js'; 4 | 5 | import type { 6 | CCClient, 7 | ACClient, 8 | PKCEClientOptions, 9 | ACClientOptions, 10 | CCClientOptions, 11 | } from '../types.js'; 12 | 13 | type Options = T extends GrantType.PKCE 14 | ? PKCEClientOptions 15 | : T extends GrantType.AUTHORIZATION_CODE 16 | ? ACClientOptions 17 | : T extends GrantType.CLIENT_CREDENTIALS 18 | ? CCClientOptions 19 | : never; 20 | type Client = T extends PKCEClientOptions 21 | ? ACClient 22 | : T extends ACClientOptions 23 | ? ACClient 24 | : T extends CCClientOptions 25 | ? CCClient 26 | : never; 27 | 28 | export const createKindeServerClient = ( 29 | grantType: G, 30 | options: Options 31 | ) => { 32 | switch (grantType) { 33 | case GrantType.AUTHORIZATION_CODE: { 34 | const clientOptions = options as ACClientOptions; 35 | return createAuthCodeClient(clientOptions, false) as Client>; 36 | } 37 | case GrantType.PKCE: { 38 | const clientOptions = options as PKCEClientOptions; 39 | return createAuthCodeClient(clientOptions, true) as Client>; 40 | } 41 | case GrantType.CLIENT_CREDENTIALS: { 42 | const clientOptions = options as CCClientOptions; 43 | return createCCClient(clientOptions) as Client>; 44 | } 45 | default: { 46 | throw new Error('Unrecognized grant type provided'); 47 | } 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /lib/sdk/clients/server/with-auth-utilities.ts: -------------------------------------------------------------------------------- 1 | import { type SessionManager } from '../../session-managers'; 2 | import * as utilities from '../../utilities/index.js'; 3 | 4 | import type { 5 | ClaimTokenType, 6 | FlagType, 7 | GetFlagType, 8 | } from '../../utilities/index.js'; 9 | 10 | const withAuthUtilities = ( 11 | isAuthenticated: (session: SessionManager) => Promise, 12 | validationDetails: utilities.TokenValidationDetailsType 13 | ) => { 14 | const { featureFlags, tokenClaims } = utilities; 15 | 16 | /** 17 | * Method extracts the provided number feature flag from the access token in 18 | * the current session. 19 | * @param {SessionManager} sessionManager 20 | * @param {string} code 21 | * @param {number} defaultValue 22 | * @returns {number} integer flag value 23 | */ 24 | const getIntegerFlag = async ( 25 | sessionManager: SessionManager, 26 | code: string, 27 | defaultValue?: number 28 | ): Promise => { 29 | if (!(await isAuthenticated(sessionManager))) { 30 | throw new Error( 31 | `Cannot return integer flag "${code}", no authentication credential found` 32 | ); 33 | } 34 | return await featureFlags.getIntegerFlag( 35 | sessionManager, 36 | code, 37 | validationDetails, 38 | defaultValue 39 | ); 40 | }; 41 | 42 | /** 43 | * Method extracts the provided string feature flag from the access token in 44 | * the current session. 45 | * @param {SessionManager} sessionManager 46 | * @param {string} code 47 | * @param {string} defaultValue 48 | * @returns {string} string flag value 49 | */ 50 | const getStringFlag = async ( 51 | sessionManager: SessionManager, 52 | code: string, 53 | defaultValue?: string 54 | ): Promise => { 55 | if (!(await isAuthenticated(sessionManager))) { 56 | throw new Error( 57 | `Cannot return string flag "${code}", no authentication credential found` 58 | ); 59 | } 60 | return await featureFlags.getStringFlag( 61 | sessionManager, 62 | code, 63 | validationDetails, 64 | defaultValue 65 | ); 66 | }; 67 | 68 | /** 69 | * Method extracts the provided boolean feature flag from the access token in 70 | * the current session. 71 | * @param {SessionManager} sessionManager 72 | * @param {string} code 73 | * @param {boolean} defaultValue 74 | * @returns {boolean} boolean flag value 75 | */ 76 | const getBooleanFlag = async ( 77 | sessionManager: SessionManager, 78 | code: string, 79 | defaultValue?: boolean 80 | ): Promise => { 81 | if (!(await isAuthenticated(sessionManager))) { 82 | throw new Error( 83 | `Cannot return boolean flag "${code}", no authentication credential found` 84 | ); 85 | } 86 | return await featureFlags.getBooleanFlag( 87 | sessionManager, 88 | code, 89 | validationDetails, 90 | defaultValue 91 | ); 92 | }; 93 | 94 | /** 95 | * Method extracts the provided claim from the provided token type in the 96 | * current session. 97 | * @param {SessionManager} sessionManager 98 | * @param {string} claim 99 | * @param {ClaimTokenType} type 100 | * @returns {unknown | null} 101 | */ 102 | const getClaimValue = async ( 103 | sessionManager: SessionManager, 104 | claim: string, 105 | type: ClaimTokenType = 'access_token' 106 | ): Promise => { 107 | if (!(await isAuthenticated(sessionManager))) { 108 | throw new Error( 109 | `Cannot return claim "${claim}", no authentication credential found` 110 | ); 111 | } 112 | 113 | return await tokenClaims.getClaimValue( 114 | sessionManager, 115 | claim, 116 | type, 117 | validationDetails 118 | ); 119 | }; 120 | 121 | /** 122 | * Method extracts the provided claim from the provided token type in the 123 | * current session, the returned object includes the provided claim. 124 | * @param {SessionManager} sessionManager 125 | * @param {string} claim 126 | * @param {ClaimTokenType} type 127 | * @returns {{ name: string, value: unknown | null }} 128 | */ 129 | const getClaim = async ( 130 | sessionManager: SessionManager, 131 | claim: string, 132 | type: ClaimTokenType = 'access_token' 133 | ): Promise<{ name: string; value: unknown | null }> => { 134 | if (!(await isAuthenticated(sessionManager))) { 135 | throw new Error( 136 | `Cannot return claim "${claim}", no authentication credential found` 137 | ); 138 | } 139 | return await tokenClaims.getClaim( 140 | sessionManager, 141 | claim, 142 | type, 143 | validationDetails 144 | ); 145 | }; 146 | 147 | /** 148 | * Method returns the organization code from the current session and returns 149 | * a boolean in the returned object indicating if the provided permission is 150 | * present in the session. 151 | * @param {SessionManager} sessionManager 152 | * @param {string} name 153 | * @returns {{ orgCode: string | null, isGranted: boolean }} 154 | */ 155 | const getPermission = async ( 156 | sessionManager: SessionManager, 157 | name: string 158 | ): Promise<{ orgCode: string | null; isGranted: boolean }> => { 159 | if (!(await isAuthenticated(sessionManager))) { 160 | throw new Error( 161 | `Cannot return permission "${name}", no authentication credential found` 162 | ); 163 | } 164 | return await tokenClaims.getPermission(sessionManager, name, validationDetails); 165 | }; 166 | 167 | /** 168 | * Method extracts the organization code from the current session. 169 | * @param {SessionManager} sessionManager 170 | * @returns {{ orgCode: string | null }} 171 | */ 172 | const getOrganization = async ( 173 | sessionManager: SessionManager 174 | ): Promise<{ orgCode: string | null }> => { 175 | if (!(await isAuthenticated(sessionManager))) { 176 | throw new Error( 177 | 'Cannot return user organization, no authentication credential found' 178 | ); 179 | } 180 | return await tokenClaims.getOrganization(sessionManager, validationDetails); 181 | }; 182 | 183 | /** 184 | * Method extracts all organization codes from the id token in the current 185 | * session. 186 | * @param {SessionManager} sessionManager 187 | * @returns {{ orgCodes: string[] }} 188 | */ 189 | const getUserOrganizations = async ( 190 | sessionManager: SessionManager 191 | ): Promise<{ orgCodes: string[] }> => { 192 | if (!(await isAuthenticated(sessionManager))) { 193 | throw new Error( 194 | 'Cannot return user organizations, no authentication credential found' 195 | ); 196 | } 197 | return await tokenClaims.getUserOrganizations(sessionManager, validationDetails); 198 | }; 199 | 200 | /** 201 | * Method extracts all the permission and the organization code in the access 202 | * token in the current session. 203 | * @param {SessionManager} sessionManager 204 | * @returns {{ permissions: string[], orgCode: string | null }} 205 | */ 206 | const getPermissions = async ( 207 | sessionManager: SessionManager 208 | ): Promise<{ 209 | permissions: string[]; 210 | orgCode: string | null; 211 | }> => { 212 | if (!(await isAuthenticated(sessionManager))) { 213 | throw new Error( 214 | 'Cannot return user permissions, no authentication credential found' 215 | ); 216 | } 217 | return await tokenClaims.getPermissions(sessionManager, validationDetails); 218 | }; 219 | 220 | /** 221 | * Method extracts the provided feature flag from the access token in the 222 | * current session. 223 | * @param {SessionManager} sessionManager 224 | * @param {string} code 225 | * @param {FlagType[keyof FlagType]} defaultValue 226 | * @param {keyof FlagType} type 227 | * @returns {GetFlagType} 228 | */ 229 | const getFlag = async ( 230 | sessionManager: SessionManager, 231 | code: string, 232 | defaultValue?: FlagType[keyof FlagType], 233 | type?: keyof FlagType 234 | ): Promise => { 235 | if (!(await isAuthenticated(sessionManager))) { 236 | throw new Error( 237 | `Cannot return flag "${code}", no authentication credential found` 238 | ); 239 | } 240 | return await featureFlags.getFlag( 241 | sessionManager, 242 | code, 243 | validationDetails, 244 | defaultValue, 245 | type 246 | ); 247 | }; 248 | 249 | return { 250 | getUserOrganizations, 251 | getOrganization, 252 | getBooleanFlag, 253 | getIntegerFlag, 254 | getPermissions, 255 | getPermission, 256 | getClaimValue, 257 | getStringFlag, 258 | getClaim, 259 | getFlag, 260 | }; 261 | }; 262 | 263 | export default withAuthUtilities; 264 | -------------------------------------------------------------------------------- /lib/sdk/clients/types.ts: -------------------------------------------------------------------------------- 1 | import { type default as createAuthCodeClient } from './server/authorization-code.js'; 2 | import { type default as createCCClient } from './server/client-credentials.js'; 3 | 4 | import type { 5 | ClientCredentialsOptions, 6 | AuthorizationCodeOptions, 7 | AuthURLOptions, 8 | } from '../oauth2-flows/index.js'; 9 | import { type SessionManager } from '../session-managers'; 10 | 11 | export interface BrowserPKCEClientOptions extends AuthorizationCodeOptions { 12 | sessionManager?: SessionManager; 13 | } 14 | 15 | export { PortalPage } from '@kinde/js-utils'; 16 | export type { GeneratePortalUrlParams } from '@kinde/js-utils'; 17 | 18 | export interface PKCEClientOptions extends AuthorizationCodeOptions {} 19 | export interface CCClientOptions extends ClientCredentialsOptions {} 20 | export interface ACClientOptions extends AuthorizationCodeOptions { 21 | clientSecret?: string; 22 | } 23 | 24 | export type ACClient = ReturnType; 25 | export type CCClient = ReturnType; 26 | 27 | export type RegisterURLOptions = Omit< 28 | AuthURLOptions, 29 | 'start_page' | 'is_create_org' 30 | >; 31 | 32 | export type CreateOrgURLOptions = RegisterURLOptions; 33 | export type LoginURLOptions = RegisterURLOptions; 34 | -------------------------------------------------------------------------------- /lib/sdk/environment.ts: -------------------------------------------------------------------------------- 1 | enum JSEnvironment { 2 | BROWSER = 'BROWSER', 3 | NODEJS = 'NODEJS', 4 | } 5 | 6 | const currentEnvironment = 7 | typeof window === 'undefined' ? JSEnvironment.NODEJS : JSEnvironment.BROWSER; 8 | 9 | /** 10 | * Method returns if current environment is node.js 11 | * @returns {boolean} 12 | */ 13 | export const isNodeEnvironment = (): boolean => { 14 | return currentEnvironment === JSEnvironment.NODEJS; 15 | }; 16 | 17 | /** 18 | * Method returns if current environment is browser. 19 | * @returns {boolean} 20 | */ 21 | export const isBrowserEnvironment = (): boolean => { 22 | return currentEnvironment === JSEnvironment.BROWSER; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/sdk/exceptions.ts: -------------------------------------------------------------------------------- 1 | export enum KindeSDKErrorCode { 2 | INVALID_TOKEN_MEMORY_COMMIT = 'INVALID_TOKEN_MEMORY_COMMIT', 3 | FAILED_TOKENS_REFRESH_ATTEMPT = 'FAILED_TOKENS_REFRESH_ATTEMPT', 4 | } 5 | 6 | export class KindeSDKError extends Error { 7 | constructor( 8 | public errorCode: KindeSDKErrorCode, 9 | message: string 10 | ) { 11 | super(message); 12 | this.name = 'KindeSDKError'; 13 | Object.setPrototypeOf(this, KindeSDKError.prototype); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export * from './session-managers/types.js'; 2 | export * from './oauth2-flows/types.js'; 3 | export * from './utilities/types.js'; 4 | export * from './clients/index.js'; 5 | export * from './exceptions.js'; 6 | export * from './utilities/validate-client-secret.js'; 7 | -------------------------------------------------------------------------------- /lib/sdk/oauth2-flows/AuthCodeAbstract.ts: -------------------------------------------------------------------------------- 1 | import { KindeSDKError, KindeSDKErrorCode } from '../exceptions.js'; 2 | import { type SessionManager } from '../session-managers/index.js'; 3 | import { isNodeEnvironment } from '../environment.js'; 4 | import type { UserType } from '../utilities/index.js'; 5 | import * as utilities from '../utilities/index.js'; 6 | import { getSDKHeader } from '../version.js'; 7 | 8 | import type { 9 | OAuth2CodeExchangeErrorResponse, 10 | OAuth2CodeExchangeResponse, 11 | AuthorizationCodeOptions, 12 | AuthURLOptions, 13 | } from './types.js'; 14 | import type { GeneratePortalUrlParams } from '@kinde/js-utils'; 15 | 16 | /** 17 | * Abstract class provides contract (methods) for classes implementing OAuth2.0 flows 18 | * for authorization_code grant type, this includes the basic Authorization Code flow 19 | * and the PKCE extention code flow. 20 | * @class AuthCodeAbstract 21 | * @param {AuthorizationCodeOptions} config 22 | */ 23 | export abstract class AuthCodeAbstract { 24 | public static DEFAULT_TOKEN_SCOPES: string = 'openid profile email offline'; 25 | public readonly authorizationEndpoint: string; 26 | public readonly userProfileEndpoint: string; 27 | public readonly logoutEndpoint: string; 28 | public readonly tokenEndpoint: string; 29 | protected state?: string; 30 | public readonly tokenValidationDetails: utilities.TokenValidationDetailsType; 31 | 32 | constructor(protected readonly config: AuthorizationCodeOptions) { 33 | const { authDomain, logoutRedirectURL } = config; 34 | this.logoutEndpoint = `${authDomain}/logout?redirect=${logoutRedirectURL ?? ''}`; 35 | this.userProfileEndpoint = `${authDomain}/oauth2/v2/user_profile`; 36 | this.authorizationEndpoint = `${authDomain}/oauth2/auth`; 37 | this.tokenEndpoint = `${authDomain}/oauth2/token`; 38 | this.tokenValidationDetails = { 39 | issuer: config.authDomain, 40 | audience: config.audience, 41 | }; 42 | } 43 | 44 | /** 45 | * Abstract method will return the initial set of query parameters required for 46 | * creating the authorization URL in child class for the kinde client's register 47 | * and login methods. 48 | * @returns {URLSearchParams} Required query parameters 49 | */ 50 | protected abstract getBaseAuthURLParams(): URLSearchParams; 51 | 52 | /** 53 | * Abstract method mandates implementation of logic required for creating auth URL 54 | * in kinde client's login and register methods, as well saving state parameter to 55 | * the session using the provided sessionManager. 56 | * @param {SessionManager} sessionManager 57 | * @param {AuthURLOptions} options 58 | * @returns {Promise} required authorization URL 59 | */ 60 | public abstract createAuthorizationURL( 61 | sessionManager: SessionManager, 62 | options: AuthURLOptions 63 | ): Promise; 64 | 65 | /** 66 | * Abstract method mandates implementation of logic required for creating portal URL 67 | * for accessing Kinde's portal interface, utilizing session data for authentication. 68 | * @param {GeneratePortalUrlParams} options 69 | * @returns {Promise<{url: URL}>} object containing the portal URL 70 | */ 71 | public abstract createPortalUrl( 72 | sessionManager: SessionManager, 73 | options: GeneratePortalUrlParams 74 | ): Promise<{ url: URL }>; 75 | 76 | /** 77 | * Abstract method will implement logic required for exchanging received auth code 78 | * post user-authentication with authorization server to receive access, refresh 79 | * and id tokens from this exchange. 80 | * @param {SessionManager} sessionManager 81 | * @param {URL} callbackURL 82 | * @returns {Promise} 83 | */ 84 | protected abstract exchangeAuthCodeForTokens( 85 | sessionManager: SessionManager, 86 | callbackURL: URL 87 | ): Promise; 88 | 89 | /** 90 | * Abstract method will implement logic in child classes for refreshing access token 91 | * using refresh token available in current session. 92 | * @param {SessionManager} sessionManager 93 | * @param {boolean} [commitToSession=true] - Optional parameter, determines whether to commit the refreshed tokens to the session. Defaults to true. 94 | * @returns {Promise} 95 | */ 96 | public abstract refreshTokens( 97 | sessionManager: SessionManager, 98 | commitToSession?: boolean 99 | ): Promise; 100 | 101 | /** 102 | * Method handles redirection logic to after authorization server redirects back 103 | * to application, this method makes use of the @see {exchangeAuthCodeForTokens} 104 | * method above and saves the received tokens to the current session. 105 | * @param {SessionManager} sessionManager 106 | * @param {URL} callbackURL 107 | * @returns {Promise} 108 | */ 109 | async handleRedirectFromAuthDomain( 110 | sessionManager: SessionManager, 111 | callbackURL: URL 112 | ): Promise { 113 | const tokens = await this.exchangeAuthCodeForTokens(sessionManager, callbackURL); 114 | await utilities.commitTokensToSession( 115 | sessionManager, 116 | tokens, 117 | this.tokenValidationDetails 118 | ); 119 | } 120 | 121 | /** 122 | * Method retrieves the access token, if the token present in the current session 123 | * is unexpired it will be returned otherwise, a new one will be obtained using 124 | * the refresh token if the refresh token is not available either an error will 125 | * be thrown. 126 | * @param {SessionManager} sessionManager 127 | * @returns {Promise} 128 | */ 129 | public async getToken(sessionManager: SessionManager): Promise { 130 | const accessToken = await utilities.getAccessToken(sessionManager); 131 | if (!accessToken) { 132 | throw new Error('No authentication credential found'); 133 | } 134 | 135 | const isAccessTokenExpired = await utilities.isTokenExpired( 136 | accessToken, 137 | this.tokenValidationDetails 138 | ); 139 | if (!isAccessTokenExpired) { 140 | return accessToken; 141 | } 142 | 143 | const refreshToken = await utilities.getRefreshToken(sessionManager); 144 | if (!refreshToken && isNodeEnvironment()) { 145 | throw Error('Cannot persist session no valid refresh token found'); 146 | } 147 | 148 | try { 149 | const tokens = await this.refreshTokens(sessionManager); 150 | return tokens.access_token; 151 | } catch (error) { 152 | throw new KindeSDKError( 153 | KindeSDKErrorCode.FAILED_TOKENS_REFRESH_ATTEMPT, 154 | `Failed to refresh tokens owing to: ${(error as Error).message}` 155 | ); 156 | } 157 | } 158 | 159 | /** 160 | * Method returns a boolean indicating if the access token in session is expired 161 | * or not, in the event the token is expired it makes use of the `getToken` method 162 | * above to first refresh it, in the event refresh fails false is returned. 163 | * @param sessionManager 164 | * @returns {Promise} 165 | */ 166 | public async isAuthenticated(sessionManager: SessionManager): Promise { 167 | try { 168 | await this.getToken(sessionManager); 169 | return true; 170 | } catch (error) { 171 | return false; 172 | } 173 | } 174 | 175 | /** 176 | * Method makes use of the user profile V2 endpoint to fetch the authenticated 177 | * user's profile information. 178 | * @param {SessionManager} sessionManager 179 | * @returns {Promise} 180 | */ 181 | async getUserProfile(sessionManager: SessionManager): Promise { 182 | const accessToken = await this.getToken(sessionManager); 183 | const headers = new Headers(); 184 | headers.append('Authorization', `Bearer ${accessToken}`); 185 | headers.append('Accept', 'application/json'); 186 | 187 | const targetURL = this.userProfileEndpoint; 188 | const config: RequestInit = { method: 'GET', headers }; 189 | const response = await fetch(targetURL, config); 190 | const payload = (await response.json()) as UserType; 191 | 192 | return payload; 193 | } 194 | 195 | /** 196 | * A helper method employed by @see {exchangeAuthCodeForTokens} method in child 197 | * classes to extract code and state parameters from the received callback URL 198 | * an exception is raised in the event the callback URL contains an error query 199 | * parameter. 200 | * @param {URL} callbackURL 201 | * @returns {[string, string]} c 202 | */ 203 | protected getCallbackURLParams(callbackURL: URL): [string, string] { 204 | const searchParams = new URLSearchParams(callbackURL.search); 205 | const state = searchParams.get('state')!; 206 | const error = searchParams.get('error'); 207 | const code = searchParams.get('code')!; 208 | 209 | if (error) { 210 | throw new Error(`Authorization server reported an error: ${error}`); 211 | } 212 | 213 | return [code, state]; 214 | } 215 | 216 | /** 217 | * Method implements logic for fetching tokens from the authorization server using 218 | * the provided body, the `useCookies` is used exclusively on the browser. 219 | * @param {SessionManager} sessionManager 220 | * @param {URLSearchParams} body 221 | * @param {boolean} useCookies 222 | * @returns {Promise} 223 | */ 224 | protected async fetchTokensFor( 225 | sessionManager: SessionManager, 226 | body: URLSearchParams, 227 | useCookies: boolean = false 228 | ): Promise { 229 | const headers = new Headers(); 230 | headers.append( 231 | 'Content-Type', 232 | 'application/x-www-form-urlencoded; charset=UTF-8' 233 | ); 234 | headers.append( 235 | ...getSDKHeader({ 236 | frameworkVersion: this.config.frameworkVersion, 237 | framework: this.config.framework, 238 | }) 239 | ); 240 | 241 | const config: RequestInit = { 242 | method: 'POST', 243 | headers, 244 | body, 245 | credentials: useCookies ? 'include' : undefined, 246 | }; 247 | const response = await fetch(this.tokenEndpoint, config); 248 | const payload = (await response.json()) as 249 | | OAuth2CodeExchangeErrorResponse 250 | | OAuth2CodeExchangeResponse; 251 | 252 | const errorPayload = payload as OAuth2CodeExchangeErrorResponse; 253 | if (errorPayload.error) { 254 | await sessionManager.destroySession(); 255 | const errorDescription = errorPayload.error_description; 256 | const message = errorDescription ?? errorPayload.error; 257 | throw new Error(message); 258 | } 259 | 260 | return payload as OAuth2CodeExchangeResponse; 261 | } 262 | 263 | /** 264 | * Helper method employed by @see {createAuthorizationURL} method above for 265 | * generating the aforementioned authorization URL. 266 | * @param {AuthURLOptions} 267 | * @returns {URLSearchParams} 268 | */ 269 | protected generateAuthURLParams(options: AuthURLOptions = {}): URLSearchParams { 270 | const searchParams = this.getBaseAuthURLParams(); 271 | 272 | let scope = this.config.scope ?? AuthCodeAbstract.DEFAULT_TOKEN_SCOPES; 273 | scope = scope.split(' ').includes('openid') ? scope : `${scope} openid`; 274 | 275 | let searchParamsObject: Record = { 276 | scope, 277 | }; 278 | 279 | if (options.start_page) { 280 | searchParamsObject.start_page = options.start_page; 281 | } 282 | 283 | if (options.org_code) { 284 | searchParamsObject.org_code = options.org_code; 285 | } 286 | 287 | if (options.is_create_org) { 288 | searchParamsObject.org_name = options.org_name ?? ''; 289 | searchParamsObject.is_create_org = 'true'; 290 | } 291 | 292 | if (options.authUrlParams) { 293 | const { 294 | lang, 295 | login_hint: loginHint, 296 | connection_id: connectionId, 297 | state, 298 | ...rest 299 | } = options.authUrlParams; 300 | 301 | searchParamsObject = { ...rest, ...searchParamsObject }; 302 | 303 | if (lang) { 304 | searchParamsObject.lang = lang; 305 | } 306 | 307 | if (loginHint) { 308 | searchParamsObject.login_hint = loginHint; 309 | } 310 | 311 | if (connectionId) { 312 | searchParamsObject.connection_id = connectionId; 313 | } 314 | } 315 | 316 | for (const key in searchParamsObject) { 317 | const value = searchParamsObject[key]; 318 | if (typeof value === 'object' && value !== null) { 319 | searchParams.append(key, JSON.stringify(value)); 320 | } else { 321 | searchParams.append(key, String(value)); 322 | } 323 | } 324 | 325 | if (this.config.audience) { 326 | const audienceArray = Array.isArray(this.config.audience) 327 | ? this.config.audience 328 | : [this.config.audience]; 329 | 330 | audienceArray.forEach((aud) => { 331 | searchParams.append('audience', aud); 332 | }); 333 | } 334 | 335 | return searchParams; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /lib/sdk/oauth2-flows/AuthCodeWithPKCE.ts: -------------------------------------------------------------------------------- 1 | import { isBrowserEnvironment } from '../environment.js'; 2 | import { AuthCodeAbstract } from './AuthCodeAbstract.js'; 3 | import * as utilities from '../utilities/index.js'; 4 | 5 | import { 6 | type BrowserSessionManager, 7 | type SessionManager, 8 | } from '../session-managers/index.js'; 9 | 10 | import type { 11 | OAuth2CodeExchangeResponse, 12 | AuthURLOptions, 13 | AuthorizationCodeOptions, 14 | } from './types.js'; 15 | import type { GeneratePortalUrlParams } from '@kinde/js-utils'; 16 | import { createPortalUrl } from '../utilities/createPortalUrl.js'; 17 | 18 | /** 19 | * Class provides implementation for the authorization code with PKCE extension 20 | * OAuth2.0 flow, please note the use of the `isBrowserEnvironment()` method 21 | * in certain methods of this class, this is because this class is intended to 22 | * be used on both the browser and server. 23 | * @class AuthCodeWithPKCE 24 | * @param {AuthorizationCodeOptions} config 25 | */ 26 | export class AuthCodeWithPKCE extends AuthCodeAbstract { 27 | public static STATE_KEY: string = 'acwpf-state-key'; 28 | private codeChallenge?: string; 29 | private codeVerifier?: string; 30 | 31 | constructor(protected readonly config: AuthorizationCodeOptions) { 32 | super(config); 33 | } 34 | 35 | /** 36 | * Method provides implementation for `createAuthorizationURL` method mandated by 37 | * `AuthCodeAbstract` parent class, see corresponding comment in parent class for 38 | * further explanation. 39 | * @param {SessionManager} sessionManager 40 | * @param {AuthURLOptions} options 41 | * @returns {Promise} required authorization URL 42 | */ 43 | async createAuthorizationURL( 44 | sessionManager: SessionManager, 45 | options: AuthURLOptions = {} 46 | ): Promise { 47 | const challengeSetup = await utilities.setupCodeChallenge(); 48 | const { challenge, verifier } = challengeSetup; 49 | this.codeChallenge = challenge; 50 | this.codeVerifier = verifier; 51 | 52 | const providedState = options.state ?? options.authUrlParams?.state; 53 | 54 | this.state = providedState ?? utilities.generateRandomString(); 55 | 56 | const setItem = isBrowserEnvironment() 57 | ? (sessionManager as unknown as BrowserSessionManager).setSessionItemBrowser 58 | : sessionManager.setSessionItem; 59 | 60 | await setItem.call( 61 | sessionManager, 62 | this.getCodeVerifierKey(this.state), 63 | JSON.stringify({ codeVerifier: this.codeVerifier }) 64 | ); 65 | 66 | const authURL = new URL(this.authorizationEndpoint); 67 | const authParams = this.generateAuthURLParams(options); 68 | authURL.search = authParams.toString(); 69 | return authURL; 70 | } 71 | 72 | /** 73 | * Method provides implementation for `createPortalUrl` method mandated by 74 | * `AuthCodeAbstract` parent class, see corresponding comment in parent class for 75 | * further explanation. 76 | * @param {SessionManager} sessionManager 77 | * @param {Omit} options 78 | * @returns {Promise<{url: URL}>} required authorization URL 79 | */ 80 | async createPortalUrl( 81 | sessionManager: SessionManager, 82 | options: Omit 83 | ): Promise<{ url: URL }> { 84 | return await createPortalUrl(sessionManager, { 85 | domain: this.config.authDomain, 86 | ...options, 87 | }); 88 | } 89 | 90 | /** 91 | * Method provides implementation for `refreshTokens` method mandated by 92 | * `AuthCodeAbstract` parent class, see corresponding comment in parent class for 93 | * further explanation. 94 | * @param {SessionManager} sessionManager 95 | * @param {boolean} [commitToSession=true] - Optional parameter, determines whether to commit the refreshed tokens to the session. Defaults to true. 96 | * @returns {Promise} 97 | */ 98 | public async refreshTokens( 99 | sessionManager: SessionManager, 100 | commitToSession: boolean = true 101 | ): Promise { 102 | const refreshToken = await utilities.getRefreshToken(sessionManager); 103 | const body = new URLSearchParams({ 104 | grant_type: 'refresh_token', 105 | refresh_token: refreshToken!, 106 | client_id: this.config.clientId, 107 | }); 108 | 109 | const tokens = await this.fetchTokensFor(sessionManager, body, true); 110 | if (commitToSession) { 111 | await utilities.commitTokensToSession( 112 | sessionManager, 113 | tokens, 114 | this.tokenValidationDetails 115 | ); 116 | } 117 | return tokens; 118 | } 119 | 120 | /** 121 | * Method provides implementation for `exchangeAuthCodeForTokens` method mandated 122 | * by `AuthCodeAbstract` parent class, see corresponding comment in parent class 123 | * for further explanation. 124 | * @param {SessionManager} sessionManager 125 | * @param {URL} callbackURL 126 | * @returns {Promise} 127 | */ 128 | protected async exchangeAuthCodeForTokens( 129 | sessionManager: SessionManager, 130 | callbackURL: URL 131 | ): Promise { 132 | const [code, state] = super.getCallbackURLParams(callbackURL); 133 | const storedStateKey = this.getCodeVerifierKey(state); 134 | if (!storedStateKey?.endsWith(state)) { 135 | throw new Error('Received state does not match stored state'); 136 | } 137 | 138 | const getItem = isBrowserEnvironment() 139 | ? (sessionManager as unknown as BrowserSessionManager).getSessionItemBrowser 140 | : sessionManager.getSessionItem; 141 | 142 | const storedState = (await getItem.call(sessionManager, storedStateKey)) as 143 | | string 144 | | null; 145 | if (!storedState) { 146 | throw new Error('Stored state not found'); 147 | } 148 | 149 | const authFlowState = JSON.parse(storedState); 150 | this.codeVerifier = authFlowState.codeVerifier; 151 | 152 | const body = new URLSearchParams({ 153 | redirect_uri: this.config.redirectURL, 154 | client_id: this.config.clientId, 155 | code_verifier: this.codeVerifier!, 156 | grant_type: 'authorization_code', 157 | code, 158 | }); 159 | 160 | const removeItem = isBrowserEnvironment() 161 | ? (sessionManager as unknown as BrowserSessionManager).removeSessionItemBrowser 162 | : sessionManager.removeSessionItem; 163 | 164 | try { 165 | return await this.fetchTokensFor(sessionManager, body); 166 | } finally { 167 | await removeItem.call(sessionManager, this.getCodeVerifierKey(state)); 168 | } 169 | } 170 | 171 | /** 172 | * Method generates the key against which the code verifier is stored in session 173 | * storage. 174 | * @param {string} state 175 | * @returns {string} - required code verifer key 176 | */ 177 | private getCodeVerifierKey(state: string): string { 178 | return `${AuthCodeWithPKCE.STATE_KEY}-${state}`; 179 | } 180 | 181 | /** 182 | * Method provides implementation for `getBaseAuthURLParams` method mandated by 183 | * `AuthCodeAbstract` parent class, see corresponding comment in parent class 184 | * for further explanation. 185 | * @returns {URLSearchParams} Required query parameters 186 | */ 187 | protected getBaseAuthURLParams(): URLSearchParams { 188 | return new URLSearchParams({ 189 | state: this.state!, 190 | client_id: this.config.clientId, 191 | redirect_uri: this.config.redirectURL, 192 | response_type: 'code', 193 | code_challenge: this.codeChallenge!, 194 | code_challenge_method: 'S256', 195 | }); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/sdk/oauth2-flows/AuthorizationCode.ts: -------------------------------------------------------------------------------- 1 | import { type SessionManager } from '../session-managers/index.js'; 2 | import { AuthCodeAbstract } from './AuthCodeAbstract.js'; 3 | import * as utilities from '../utilities/index.js'; 4 | 5 | import type { 6 | OAuth2CodeExchangeResponse, 7 | AuthorizationCodeOptions, 8 | AuthURLOptions, 9 | } from './types.js'; 10 | import type { GeneratePortalUrlParams } from '@kinde/js-utils'; 11 | import { createPortalUrl } from '../utilities/createPortalUrl.js'; 12 | 13 | /** 14 | * Class provides implementation for the authorization code OAuth2.0 flow. 15 | * @class AuthorizationCode 16 | * @param {AuthorizationCodeOptions} config 17 | * @param {string} clientSecret 18 | */ 19 | export class AuthorizationCode extends AuthCodeAbstract { 20 | public static STATE_KEY: string = 'ac-state-key'; 21 | 22 | constructor( 23 | protected readonly config: AuthorizationCodeOptions, 24 | private readonly clientSecret: string 25 | ) { 26 | super(config); 27 | } 28 | 29 | /** 30 | * Method provides implementation for `createAuthorizationURL` method mandated by 31 | * `AuthCodeAbstract` parent class, see corresponding comment in parent class for 32 | * further explanation. 33 | * @param {SessionManager} sessionManager 34 | * @param {AuthURLOptions} options 35 | * @returns {Promise} required authorization URL 36 | */ 37 | async createAuthorizationURL( 38 | sessionManager: SessionManager, 39 | options: AuthURLOptions = {} 40 | ): Promise { 41 | const providedState = options.state ?? options.authUrlParams?.state; 42 | 43 | this.state = 44 | providedState ?? 45 | ((await sessionManager.getSessionItem( 46 | AuthorizationCode.STATE_KEY 47 | )) as string) ?? 48 | utilities.generateRandomString(); 49 | 50 | await sessionManager.setSessionItem(AuthorizationCode.STATE_KEY, this.state); 51 | const authURL = new URL(this.authorizationEndpoint); 52 | const authParams = this.generateAuthURLParams(options); 53 | authURL.search = authParams.toString(); 54 | return authURL; 55 | } 56 | 57 | /** 58 | * Method provides implementation for `createPortalUrl` method mandated by 59 | * `AuthCodeAbstract` parent class, see corresponding comment in parent class for 60 | * further explanation. 61 | * @param {SessionManager} sessionManager 62 | * @param {Omit} options 63 | * @returns {Promise<{url: URL}>} required authorization URL 64 | */ 65 | async createPortalUrl( 66 | sessionManager: SessionManager, 67 | options: Omit 68 | ): Promise<{ url: URL }> { 69 | return await createPortalUrl(sessionManager, { 70 | domain: this.config.authDomain, 71 | ...options, 72 | }); 73 | } 74 | 75 | /** 76 | * Method provides implementation for `refreshTokens` method mandated by 77 | * `AuthCodeAbstract` parent class, see corresponding comment in parent class for 78 | * further explanation. 79 | * @param {SessionManager} sessionManager 80 | * @param {boolean} [commitToSession=true] - Optional parameter, determines whether to commit the refreshed tokens to the session. Defaults to true. 81 | * @returns {Promise} 82 | */ 83 | public async refreshTokens( 84 | sessionManager: SessionManager, 85 | commitToSession: boolean = true 86 | ): Promise { 87 | const refreshToken = await utilities.getRefreshToken(sessionManager); 88 | 89 | if (!utilities.validateClientSecret(this.clientSecret)) { 90 | throw new Error(`Invalid client secret ${this.clientSecret}`); 91 | } 92 | 93 | const body = new URLSearchParams({ 94 | grant_type: 'refresh_token', 95 | client_id: this.config.clientId, 96 | client_secret: this.clientSecret, 97 | refresh_token: refreshToken!, 98 | }); 99 | 100 | const tokens = await this.fetchTokensFor(sessionManager, body); 101 | if (commitToSession) { 102 | await utilities.commitTokensToSession( 103 | sessionManager, 104 | tokens, 105 | this.tokenValidationDetails 106 | ); 107 | } 108 | return tokens; 109 | } 110 | 111 | /** 112 | * Method provides implementation for `exchangeAuthCodeForTokens` method mandated 113 | * by `AuthCodeAbstract` parent class, see corresponding comment in parent class 114 | * for further explanation. 115 | * @param {SessionManager} sessionManager 116 | * @param {URL} callbackURL 117 | * @returns {Promise} 118 | */ 119 | protected async exchangeAuthCodeForTokens( 120 | sessionManager: SessionManager, 121 | callbackURL: URL 122 | ): Promise { 123 | const [code, state] = this.getCallbackURLParams(callbackURL); 124 | const stateKey = AuthorizationCode.STATE_KEY; 125 | const storedState = (await sessionManager.getSessionItem(stateKey)) as 126 | | string 127 | | null; 128 | if (!storedState) { 129 | throw new Error( 130 | `Authentication flow: Received: ${state} | Expected: State not found` 131 | ); 132 | } 133 | 134 | if (storedState !== state) { 135 | throw new Error( 136 | `Authentication flow: State mismatch. Received: ${state} | Expected: ${storedState}` 137 | ); 138 | } 139 | 140 | if (!utilities.validateClientSecret(this.clientSecret)) { 141 | throw new Error(`Invalid client secret ${this.clientSecret}`); 142 | } 143 | 144 | const body = new URLSearchParams({ 145 | grant_type: 'authorization_code', 146 | client_id: this.config.clientId, 147 | client_secret: this.clientSecret, 148 | redirect_uri: this.config.redirectURL, 149 | code, 150 | }); 151 | 152 | try { 153 | return await this.fetchTokensFor(sessionManager, body); 154 | } finally { 155 | await sessionManager.removeSessionItem(stateKey); 156 | } 157 | } 158 | 159 | /** 160 | * Method provides implementation for `getBaseAuthURLParams` method mandated by 161 | * `AuthCodeAbstract` parent class, see corresponding comment in parent class 162 | * for further explanation. 163 | * @returns {URLSearchParams} Required query parameters 164 | */ 165 | protected getBaseAuthURLParams(): URLSearchParams { 166 | return new URLSearchParams({ 167 | state: this.state!, 168 | client_id: this.config.clientId, 169 | redirect_uri: this.config.redirectURL, 170 | response_type: 'code', 171 | }); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/sdk/oauth2-flows/ClientCredentials.ts: -------------------------------------------------------------------------------- 1 | import { type SessionManager } from '../session-managers/index.js'; 2 | import * as utilities from '../utilities/index.js'; 3 | import { getSDKHeader } from '../version.js'; 4 | 5 | import type { 6 | OAuth2CCTokenErrorResponse, 7 | ClientCredentialsOptions, 8 | OAuth2CCTokenResponse, 9 | } from './types.js'; 10 | 11 | /** 12 | * Class provides implementation for the client credentials OAuth2.0 flow. 13 | * @class ClientCredentials 14 | */ 15 | export class ClientCredentials { 16 | public readonly logoutEndpoint: string; 17 | public readonly tokenEndpoint: string; 18 | public readonly tokenValidationDetails: utilities.TokenValidationDetailsType; 19 | 20 | constructor(private readonly config: ClientCredentialsOptions) { 21 | const { authDomain, logoutRedirectURL } = config; 22 | this.logoutEndpoint = `${authDomain}/logout?redirect=${logoutRedirectURL ?? ''}`; 23 | this.tokenEndpoint = `${authDomain}/oauth2/token`; 24 | this.config = config; 25 | this.tokenValidationDetails = { 26 | issuer: config.authDomain, 27 | audience: config.audience, 28 | }; 29 | } 30 | 31 | /** 32 | * Method retrieves the access token, if the token present in the current session 33 | * is unexpired it will be returned otherwise, a new one will be be obtained by 34 | * performing a network call. 35 | * @param {SessionManager} sessionManager 36 | * @returns {Promise} 37 | */ 38 | async getToken(sessionManager: SessionManager): Promise { 39 | const accessToken = await utilities.getAccessToken(sessionManager); 40 | const isTokenExpired = await utilities.isTokenExpired( 41 | accessToken, 42 | this.tokenValidationDetails 43 | ); 44 | if (accessToken && !isTokenExpired) { 45 | return accessToken; 46 | } 47 | 48 | const payload = await this.fetchAccessTokenFor(sessionManager); 49 | await utilities.commitTokenToSession( 50 | sessionManager, 51 | payload.access_token, 52 | 'access_token', 53 | this.tokenValidationDetails 54 | ); 55 | return payload.access_token; 56 | } 57 | 58 | /** 59 | * Method implements logic for requesting access token using token endpoint. 60 | * @param {SessionManager} sessionManager 61 | * @returns {Promise} 62 | */ 63 | private async fetchAccessTokenFor( 64 | sessionManager: SessionManager 65 | ): Promise { 66 | const body = this.generateTokenURLParams(); 67 | const headers = new Headers(); 68 | headers.append( 69 | 'Content-Type', 70 | 'application/x-www-form-urlencoded; charset=UTF-8' 71 | ); 72 | headers.append( 73 | ...getSDKHeader({ 74 | frameworkVersion: this.config.frameworkVersion, 75 | framework: this.config.framework, 76 | }) 77 | ); 78 | 79 | const config: RequestInit = { method: 'POST', headers, body }; 80 | const response = await fetch(this.tokenEndpoint, config); 81 | const payload = (await response.json()) as 82 | | OAuth2CCTokenErrorResponse 83 | | OAuth2CCTokenResponse; 84 | 85 | const errorPayload = payload as OAuth2CCTokenErrorResponse; 86 | if (errorPayload.error) { 87 | await sessionManager.destroySession(); 88 | const errorDescription = errorPayload.error_description; 89 | const message = errorDescription ?? errorPayload.error; 90 | throw new Error(message); 91 | } 92 | 93 | return payload as OAuth2CCTokenResponse; 94 | } 95 | 96 | /** 97 | * Method returns a boolean indicating if the access token in session is expired 98 | * or not, in the event the token is expired it makes use of the `getToken` method 99 | * above to first refresh it, in the event refresh fails false is returned. 100 | * @param sessionManager 101 | * @returns {Promise} 102 | */ 103 | public async isAuthenticated(sessionManager: SessionManager): Promise { 104 | try { 105 | await this.getToken(sessionManager); 106 | return true; 107 | } catch (error) { 108 | return false; 109 | } 110 | } 111 | 112 | /** 113 | * Method provides the query params required for generating the token URL for 114 | * obtaining the required access token. 115 | * @returns {URLSearchParams} 116 | */ 117 | private generateTokenURLParams(): URLSearchParams { 118 | if (!utilities.validateClientSecret(this.config.clientSecret)) { 119 | throw new Error(`Invalid client secret ${this.config.clientSecret}`); 120 | } 121 | 122 | const searchParams = new URLSearchParams({ 123 | grant_type: 'client_credentials', 124 | client_id: this.config.clientId, 125 | client_secret: this.config.clientSecret, 126 | }); 127 | 128 | if (this.config.scope !== undefined) { 129 | searchParams.append('scope', this.config.scope); 130 | } 131 | 132 | if (this.config.audience) { 133 | const audienceArray = Array.isArray(this.config.audience) 134 | ? this.config.audience 135 | : [this.config.audience]; 136 | 137 | audienceArray.forEach((aud) => { 138 | searchParams.append('audience', aud); 139 | }); 140 | } 141 | 142 | return new URLSearchParams(searchParams); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/sdk/oauth2-flows/index.ts: -------------------------------------------------------------------------------- 1 | export { ClientCredentials } from './ClientCredentials.js'; 2 | export { AuthorizationCode } from './AuthorizationCode.js'; 3 | export { AuthCodeWithPKCE } from './AuthCodeWithPKCE.js'; 4 | export * from './types.js'; 5 | -------------------------------------------------------------------------------- /lib/sdk/oauth2-flows/types.ts: -------------------------------------------------------------------------------- 1 | export enum GrantType { 2 | AUTHORIZATION_CODE = 'AUTHORIZATION_CODE', 3 | CLIENT_CREDENTIALS = 'CLIENT_CREDENTIALS', 4 | PKCE = 'PKCE', 5 | } 6 | 7 | export interface OAuth2CodeExchangeErrorResponse { 8 | error?: string; 9 | error_description?: string; 10 | } 11 | 12 | export interface OAuth2CodeExchangeResponse { 13 | access_token: string; 14 | expires_in: string; 15 | token_type: string; 16 | refresh_token: string; 17 | id_token: string; 18 | scope: string; 19 | } 20 | 21 | export interface OAuth2CCTokenErrorResponse { 22 | error?: string; 23 | error_description?: string; 24 | } 25 | 26 | export interface OAuth2CCTokenResponse { 27 | access_token: string; 28 | expires_in: string; 29 | } 30 | 31 | export interface OAuth2FlowOptions { 32 | clientId: string; 33 | logoutRedirectURL?: string; 34 | authDomain: string; 35 | audience?: string | string[]; 36 | scope?: string; 37 | } 38 | 39 | export interface SDKHeaderOverrideOptions { 40 | framework?: string; 41 | frameworkVersion?: string; 42 | } 43 | 44 | export interface AuthorizationCodeOptions 45 | extends OAuth2FlowOptions, 46 | SDKHeaderOverrideOptions { 47 | redirectURL: string; 48 | } 49 | 50 | export interface ClientCredentialsOptions 51 | extends OAuth2FlowOptions, 52 | SDKHeaderOverrideOptions { 53 | clientSecret: string; 54 | } 55 | 56 | export interface AuthURLParams { 57 | lang?: string; 58 | login_hint?: string; 59 | connection_id?: string; 60 | [key: string]: string | undefined; 61 | } 62 | 63 | export interface AuthURLOptions { 64 | start_page?: 'registration' | 'login'; 65 | is_create_org?: boolean; 66 | org_name?: string; 67 | org_code?: string; 68 | state?: string; 69 | post_login_redirect_url?: string; 70 | authUrlParams?: AuthURLParams; 71 | } 72 | -------------------------------------------------------------------------------- /lib/sdk/session-managers/BrowserSessionManager.ts: -------------------------------------------------------------------------------- 1 | import { isBrowserEnvironment } from '../environment.js'; 2 | import { type SessionManager } from './types.js'; 3 | 4 | /** 5 | * Provides a session manager implementation for the browser. 6 | * @class BrowserSessionManager 7 | */ 8 | export class BrowserSessionManager implements SessionManager { 9 | public static ITEM_NAME_PREFIX = 'browser-session-store@'; 10 | private memCache: Record = {}; 11 | 12 | constructor() { 13 | if (!isBrowserEnvironment()) { 14 | throw new Error('BrowserSessionStore must be instantiated on the browser'); 15 | } 16 | } 17 | 18 | /** 19 | * Prefixes provided item key with class static prefix. 20 | * @param {string} itemKey 21 | * @returns {string} 22 | */ 23 | private generateItemKey(itemKey: string): string { 24 | return `${BrowserSessionManager.ITEM_NAME_PREFIX}${itemKey}`; 25 | } 26 | 27 | /** 28 | * Clears all items from session store. 29 | * @returns {void} 30 | */ 31 | async destroySession(): Promise { 32 | sessionStorage.clear(); 33 | this.memCache = {}; 34 | } 35 | 36 | /** 37 | * Sets the provided key-value store to the memory cache. 38 | * @param {string} itemKey 39 | * @param {unknown} itemValue 40 | * @returns {void} 41 | */ 42 | async setSessionItem(itemKey: string, itemValue: unknown): Promise { 43 | const key = this.generateItemKey(itemKey); 44 | this.memCache[key] = itemValue; 45 | } 46 | 47 | /** 48 | * Sets the provided key-value store to the browser session storage. 49 | * @param {string} itemKey 50 | * @param {unknown} itemValue 51 | */ 52 | async setSessionItemBrowser(itemKey: string, itemValue: unknown): Promise { 53 | const key = this.generateItemKey(itemKey); 54 | const isString = typeof itemValue === 'string'; 55 | const value = !isString ? JSON.stringify(itemValue) : itemValue; 56 | sessionStorage.setItem(key, value); 57 | } 58 | 59 | /** 60 | * Gets the item for the provided key from the memory cache. 61 | * @param {string} itemKey 62 | * @returns {unknown | null} 63 | */ 64 | async getSessionItem(itemKey: string): Promise { 65 | const key = this.generateItemKey(itemKey); 66 | return this.memCache[key] ?? null; 67 | } 68 | 69 | /** 70 | * Gets the item for the provided key from the browser session storage. 71 | * @param {string} itemKey 72 | * @returns {unknown | null} 73 | */ 74 | async getSessionItemBrowser(itemKey: string): Promise { 75 | const key = this.generateItemKey(itemKey); 76 | return sessionStorage.getItem(key); 77 | } 78 | 79 | /** 80 | * Removes the item for the provided key from the memory cache. 81 | * @param {string} itemKey 82 | * @returns {void} 83 | */ 84 | async removeSessionItem(itemKey: string): Promise { 85 | const key = this.generateItemKey(itemKey); 86 | delete this.memCache[key]; 87 | } 88 | 89 | /** 90 | * Removes the item for the provided key from the browser session storage. 91 | * @param {string} itemKey 92 | * @returns {void} 93 | */ 94 | async removeSessionItemBrowser(itemKey: string): Promise { 95 | const key = this.generateItemKey(itemKey); 96 | sessionStorage.removeItem(key); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/sdk/session-managers/index.ts: -------------------------------------------------------------------------------- 1 | export { BrowserSessionManager } from './BrowserSessionManager.js'; 2 | export * from './types.js'; 3 | -------------------------------------------------------------------------------- /lib/sdk/session-managers/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This interfaces provides the contract that an session management utility must 3 | * satisfiy in order to work with this SDK, please vist the example provided in the 4 | * README, to understand how this works. 5 | */ 6 | type Awaitable = Promise; 7 | 8 | export interface SessionManager { 9 | persistent?: boolean; 10 | getSessionItem: (itemKey: string) => Awaitable; 11 | setSessionItem: (itemKey: string, itemValue: T) => Awaitable; 12 | removeSessionItem: (itemKey: string) => Awaitable; 13 | destroySession: () => Awaitable; 14 | } 15 | -------------------------------------------------------------------------------- /lib/sdk/utilities/code-challenge.ts: -------------------------------------------------------------------------------- 1 | import { generateRandomString } from './random-string.js'; 2 | import { subtle } from 'uncrypto'; 3 | 4 | /** 5 | * Encodes the provided ArrayBuffer string to base-64 format. 6 | * @param {ArrayBuffer} str 7 | * @returns {string} 8 | */ 9 | export const base64UrlEncode = (str: ArrayBuffer): string => { 10 | return btoa(String.fromCharCode(...new Uint8Array(str))) 11 | .replace(/\+/g, '-') 12 | .replace(/\//g, '_') 13 | .replace(/=+$/, ''); 14 | }; 15 | 16 | /** 17 | * Creates a one-way hash for the provided string using SHA-256 18 | * algorithm, the result is provided as an ArrayBuffer instance. 19 | * @param {string} plain 20 | * @returns {Promise} 21 | */ 22 | export const sha256 = async (plain: string): Promise => { 23 | const encoder = new TextEncoder(); 24 | const data = encoder.encode(plain); 25 | return await subtle.digest('SHA-256', data); 26 | }; 27 | 28 | /** 29 | * Sets up the code challenge required for PKCE OAuth2.0 flow 30 | * returning the verifier (secret) and its corresponding one-way 31 | * hash (challenge). 32 | * @returns {Promise<{ challenge: string, verifier: string }>} 33 | */ 34 | export const setupCodeChallenge = async (): Promise<{ 35 | challenge: string; 36 | verifier: string; 37 | }> => { 38 | const secret = generateRandomString(50); 39 | const challenge = base64UrlEncode(await sha256(secret)); 40 | return { challenge, verifier: secret }; 41 | }; 42 | -------------------------------------------------------------------------------- /lib/sdk/utilities/createPortalUrl.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generatePortalUrl, 3 | MemoryStorage, 4 | setActiveStorage, 5 | StorageKeys, 6 | } from '@kinde/js-utils'; 7 | import type { GeneratePortalUrlParams } from '@kinde/js-utils'; 8 | import { type SessionManager } from '../session-managers/index.js'; 9 | 10 | export const createPortalUrl = async ( 11 | sessionManager: SessionManager, 12 | options: GeneratePortalUrlParams 13 | ): Promise<{ url: URL }> => { 14 | const token = await sessionManager.getSessionItem('access_token'); // Ensure session is initialized 15 | 16 | if (!token) { 17 | throw new Error('No active session found.'); 18 | } 19 | 20 | const storage = new MemoryStorage(); 21 | await storage.setSessionItem(StorageKeys.accessToken, token); 22 | setActiveStorage(storage); 23 | 24 | return await generatePortalUrl({ 25 | domain: options.domain, 26 | returnUrl: options.returnUrl, 27 | subNav: options.subNav, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/sdk/utilities/feature-flags.ts: -------------------------------------------------------------------------------- 1 | import { type SessionManager } from '../session-managers/index.js'; 2 | import { getClaimValue } from './token-claims.js'; 3 | 4 | import { 5 | type FeatureFlags, 6 | type GetFlagType, 7 | type FlagType, 8 | FlagDataType, 9 | type TokenValidationDetailsType, 10 | } from './types.js'; 11 | 12 | /** 13 | * Method extracts the provided feature flag from the access token in the 14 | * current session. 15 | * @param {SessionManager} sessionManager 16 | * @param {string} code 17 | * @param {FlagType[keyof FlagType]} defaultValue 18 | * @param {keyof FlagType} type 19 | * @returns {GetFlagType} 20 | */ 21 | export const getFlag = async ( 22 | sessionManager: SessionManager, 23 | code: string, 24 | validationDetails: TokenValidationDetailsType, 25 | defaultValue?: FlagType[keyof FlagType], 26 | type?: keyof FlagType 27 | ): Promise => { 28 | const featureFlags = 29 | ((await getClaimValue( 30 | sessionManager, 31 | 'feature_flags', 32 | 'access_token', 33 | validationDetails 34 | )) as FeatureFlags) ?? {}; 35 | const flag = featureFlags[code]; 36 | 37 | if (!flag && defaultValue === undefined) { 38 | throw new Error( 39 | `Flag ${code} was not found, and no default value has been provided` 40 | ); 41 | } 42 | 43 | if (flag?.t && type && type !== flag?.t) { 44 | throw new Error( 45 | `Flag ${code} is of type ${FlagDataType[flag.t]}, expected type is ${ 46 | FlagDataType[type] 47 | }` 48 | ); 49 | } 50 | 51 | const response: GetFlagType = { 52 | is_default: flag?.v === undefined, 53 | value: flag?.v ?? defaultValue!, 54 | code, 55 | }; 56 | 57 | if (!response.is_default) { 58 | response.type = FlagDataType[flag?.t ?? type!]; 59 | } 60 | 61 | return response; 62 | }; 63 | 64 | /** 65 | * Method extracts the provided number feature flag from the access token in 66 | * the current session. 67 | * @param {SessionManager} sessionManager 68 | * @param {string} code 69 | * @param {number} defaultValue 70 | * @returns {number} integer flag value 71 | */ 72 | export const getIntegerFlag = async ( 73 | sessionManager: SessionManager, 74 | code: string, 75 | validationDetails: TokenValidationDetailsType, 76 | defaultValue?: number 77 | ): Promise => { 78 | return (await getFlag(sessionManager, code, validationDetails, defaultValue, 'i')) 79 | .value as number; 80 | }; 81 | 82 | /** 83 | * Method extracts the provided string feature flag from the access token in 84 | * the current session. 85 | * @param {SessionManager} sessionManager 86 | * @param {string} code 87 | * @param {string} defaultValue 88 | * @returns {string} string flag value 89 | */ 90 | export const getStringFlag = async ( 91 | sessionManager: SessionManager, 92 | code: string, 93 | validationDetails: TokenValidationDetailsType, 94 | defaultValue?: string 95 | ): Promise => { 96 | return (await getFlag(sessionManager, code, validationDetails, defaultValue, 's')) 97 | .value as string; 98 | }; 99 | 100 | /** 101 | * Method extracts the provided boolean feature flag from the access token in 102 | * the current session. 103 | * @param {SessionManager} sessionManager 104 | * @param {string} code 105 | * @param {boolean} defaultValue 106 | * @returns {boolean} boolean flag value 107 | */ 108 | export const getBooleanFlag = async ( 109 | sessionManager: SessionManager, 110 | code: string, 111 | validationDetails: TokenValidationDetailsType, 112 | defaultValue?: boolean 113 | ): Promise => { 114 | return (await getFlag(sessionManager, code, validationDetails, defaultValue, 'b')) 115 | .value as boolean; 116 | }; 117 | -------------------------------------------------------------------------------- /lib/sdk/utilities/index.ts: -------------------------------------------------------------------------------- 1 | import * as featureFlags from './feature-flags.js'; 2 | import * as tokenClaims from './token-claims.js'; 3 | 4 | export { featureFlags, tokenClaims }; 5 | export * from './feature-flags.js'; 6 | export * from './code-challenge.js'; 7 | export * from './random-string.js'; 8 | export * from './token-claims.js'; 9 | export * from './token-utils.js'; 10 | export * from './types.js'; 11 | export * from './validate-client-secret.js'; 12 | -------------------------------------------------------------------------------- /lib/sdk/utilities/random-string.ts: -------------------------------------------------------------------------------- 1 | import { getRandomValues } from 'uncrypto'; 2 | 3 | /** 4 | * Creates a random string of provided length. 5 | * @param {number} length 6 | * @returns {string} required secret 7 | */ 8 | export const generateRandomString = (length: number = 28): string => { 9 | const bytesNeeded = Math.ceil(length / 2); 10 | const array = new Uint32Array(bytesNeeded); 11 | getRandomValues(array); 12 | let result = Array.from(array, (dec) => ('0' + dec.toString(16)).slice(-2)).join( 13 | '' 14 | ); 15 | if (length % 2 !== 0) { 16 | // If the requested length is odd, remove the last character to adjust the length 17 | result = result.slice(0, -1); 18 | } 19 | return result; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/sdk/utilities/token-claims.ts: -------------------------------------------------------------------------------- 1 | import { type SessionManager } from '../session-managers/index.js'; 2 | import { type ClaimTokenType, type TokenValidationDetailsType } from './types.js'; 3 | import { jwtDecoder } from '@kinde/jwt-decoder'; 4 | import { validateToken } from '@kinde/jwt-validator'; 5 | import { isTokenExpired } from './token-utils.js'; 6 | 7 | /** 8 | * Method extracts the provided claim from the provided token type in the 9 | * current session. 10 | * @param {SessionManager} sessionManager 11 | * @param {string} claim 12 | * @param {ClaimTokenType} type 13 | * @param {TokenValidationDetailsType} validationDetails 14 | * @returns {unknown | null} 15 | */ 16 | export const getClaimValue = async ( 17 | sessionManager: SessionManager, 18 | claim: string, 19 | type: ClaimTokenType = 'access_token', 20 | validationDetails: TokenValidationDetailsType 21 | ): Promise => { 22 | const token = (await sessionManager.getSessionItem(`${type}`)) as string; 23 | if (type === 'access_token') { 24 | const expired = await isTokenExpired(token, validationDetails); 25 | if (expired) { 26 | throw new Error('Access token expired'); 27 | } 28 | } else { 29 | // ID token - signature validation only 30 | const validation = await validateToken({ 31 | token, 32 | domain: validationDetails.issuer, 33 | }); 34 | if (!validation.valid) { 35 | throw new Error(validation.message); 36 | } 37 | } 38 | const tokenPayload = jwtDecoder(token) as Record; 39 | return tokenPayload[claim] ?? null; 40 | }; 41 | 42 | /** 43 | * Method extracts the provided claim from the provided token type in the 44 | * current session, the returned object includes the provided claim. 45 | * @param {SessionManager} sessionManager 46 | * @param {string} claim 47 | * @param {ClaimTokenType} type 48 | * @param {TokenValidationDetailsType} validationDetails 49 | * @returns {{ name: string, value: unknown | null }} 50 | */ 51 | export const getClaim = async ( 52 | sessionManager: SessionManager, 53 | claim: string, 54 | type: ClaimTokenType, 55 | validationDetails: TokenValidationDetailsType 56 | ): Promise<{ name: string; value: unknown | null }> => { 57 | return { 58 | name: claim, 59 | value: await getClaimValue(sessionManager, claim, type, validationDetails), 60 | }; 61 | }; 62 | 63 | /** 64 | * Method returns the organization code from the current session and returns 65 | * a boolean in the returned object indicating if the provided permission is 66 | * present in the session. 67 | * @param {SessionManager} sessionManager 68 | * @param {string} name 69 | * @param {TokenValidationDetailsType} validationDetails 70 | * @returns {{ orgCode: string | null, isGranted: boolean }} 71 | */ 72 | export const getPermission = async ( 73 | sessionManager: SessionManager, 74 | name: string, 75 | validationDetails: TokenValidationDetailsType 76 | ): Promise<{ orgCode: string | null; isGranted: boolean }> => { 77 | const permissions = ((await getClaimValue( 78 | sessionManager, 79 | 'permissions', 80 | 'access_token', 81 | validationDetails 82 | )) ?? []) as string[]; 83 | const isGranted = permissions.some((p) => p === name); 84 | const orgCode = (await getClaimValue( 85 | sessionManager, 86 | 'org_code', 87 | 'access_token', 88 | validationDetails 89 | )) as string | null; 90 | return { orgCode, isGranted }; 91 | }; 92 | 93 | /** 94 | * Method extracts the organization code from the current session. 95 | * @param {SessionManager} sessionManager 96 | * @param {TokenValidationDetailsType} validationDetails 97 | * @returns {{ orgCode: string | null }} 98 | */ 99 | export const getOrganization = async ( 100 | sessionManager: SessionManager, 101 | validationDetails: TokenValidationDetailsType 102 | ): Promise<{ orgCode: string | null }> => ({ 103 | orgCode: (await getClaimValue( 104 | sessionManager, 105 | 'org_code', 106 | 'access_token', 107 | validationDetails 108 | )) as string | null, 109 | }); 110 | 111 | /** 112 | * Method extracts all the permission and the organization code in the access 113 | * token in the current session. 114 | * @param {SessionManager} sessionManager 115 | * @param {TokenValidationDetailsType} validationDetails 116 | * @returns {{ permissions: string[], orgCode: string | null }} 117 | */ 118 | export const getPermissions = async ( 119 | sessionManager: SessionManager, 120 | validationDetails: TokenValidationDetailsType 121 | ): Promise<{ permissions: string[]; orgCode: string | null }> => { 122 | const [permissions, orgCode] = await Promise.all([ 123 | (await getClaimValue( 124 | sessionManager, 125 | 'permissions', 126 | 'access_token', 127 | validationDetails 128 | )) as Promise, 129 | (await getClaimValue( 130 | sessionManager, 131 | 'org_code', 132 | 'access_token', 133 | validationDetails 134 | )) as Promise, 135 | ]); 136 | return { 137 | permissions, 138 | orgCode, 139 | }; 140 | }; 141 | 142 | /** 143 | * Method extracts all organization codes from the id token in the current 144 | * session. 145 | * @param {SessionManager} sessionManager 146 | * @param {TokenValidationDetailsType} validationDetails 147 | * @returns {{ orgCodes: string[] }} 148 | */ 149 | export const getUserOrganizations = async ( 150 | sessionManager: SessionManager, 151 | validationDetails: TokenValidationDetailsType 152 | ): Promise<{ orgCodes: string[] }> => ({ 153 | orgCodes: ((await getClaimValue( 154 | sessionManager, 155 | 'org_codes', 156 | 'id_token', 157 | validationDetails 158 | )) ?? []) as string[], 159 | }); 160 | -------------------------------------------------------------------------------- /lib/sdk/utilities/token-utils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | TokenCollection, 3 | UserType, 4 | TokenType, 5 | TokenValidationDetailsType, 6 | } from './types.js'; 7 | import { type SessionManager } from '../session-managers/index.js'; 8 | import { KindeSDKError, KindeSDKErrorCode } from '../exceptions.js'; 9 | import { validateToken } from '@kinde/jwt-validator'; 10 | import { jwtDecoder } from '@kinde/jwt-decoder'; 11 | 12 | /** 13 | * Saves the provided token to the current session. 14 | * @param {SessionManager} sessionManager 15 | * @param {string} token 16 | * @param {TokenValidationDetailsType} validationDetails 17 | * @param {TokenType} type 18 | */ 19 | export const commitTokenToSession = async ( 20 | sessionManager: SessionManager, 21 | token: string, 22 | type: TokenType, 23 | validationDetails: TokenValidationDetailsType 24 | ): Promise => { 25 | if (!token) { 26 | await sessionManager.removeSessionItem(type); 27 | return; 28 | } 29 | 30 | if (type === 'access_token' || type === 'id_token') { 31 | try { 32 | const validation = await validateToken({ 33 | token, 34 | domain: validationDetails.issuer, 35 | }); 36 | if (!validation.valid) { 37 | throw new Error(validation.message); 38 | } 39 | const isExpired = await isTokenExpired(token, validationDetails); 40 | if (isExpired) { 41 | throw new Error('Token is expired'); 42 | } 43 | } catch (e) { 44 | throw new KindeSDKError( 45 | KindeSDKErrorCode.INVALID_TOKEN_MEMORY_COMMIT, 46 | `Attempting to commit invalid ${type} token "${token}" to memory` 47 | ); 48 | } 49 | } 50 | 51 | await sessionManager.setSessionItem(type, token); 52 | }; 53 | 54 | /** 55 | * Saves the access, refresh and id tokens provided in the `TokenCollection` 56 | * object to the current session. 57 | * @param {SessionManager} sessionManager 58 | * @param tokens 59 | */ 60 | export const commitTokensToSession = async ( 61 | sessionManager: SessionManager, 62 | tokens: TokenCollection, 63 | validationDetails: TokenValidationDetailsType 64 | ): Promise => { 65 | const payload: { ksp?: { persistent: boolean } } | null = jwtDecoder<{ 66 | ksp: { persistent: boolean }; 67 | }>(tokens.access_token); 68 | if (payload) { 69 | sessionManager.persistent = payload.ksp?.persistent ?? true; 70 | } 71 | await Promise.all([ 72 | commitTokenToSession( 73 | sessionManager, 74 | tokens.refresh_token, 75 | 'refresh_token', 76 | validationDetails 77 | ), 78 | commitTokenToSession( 79 | sessionManager, 80 | tokens.access_token, 81 | 'access_token', 82 | validationDetails 83 | ), 84 | commitTokenToSession( 85 | sessionManager, 86 | tokens.id_token, 87 | 'id_token', 88 | validationDetails 89 | ), 90 | ]); 91 | }; 92 | 93 | /** 94 | * Extracts the refresh token from current session returns null if the 95 | * token is not found. 96 | * @param {SessionManager} sessionManager 97 | * @returns {string | null} 98 | */ 99 | export const getRefreshToken = async ( 100 | sessionManager: SessionManager 101 | ): Promise => { 102 | return await (sessionManager.getSessionItem('refresh_token') as Promise< 103 | string | null 104 | >); 105 | }; 106 | 107 | /** 108 | * Extracts the access token from current session returns null if the 109 | * token is not found. 110 | * @param {SessionManager} sessionManager 111 | * @returns {string | null} 112 | */ 113 | export const getAccessToken = async ( 114 | sessionManager: SessionManager 115 | ): Promise => { 116 | return await (sessionManager.getSessionItem('access_token') as Promise< 117 | string | null 118 | >); 119 | }; 120 | 121 | /** 122 | * Extracts the user information from the current session returns null if 123 | * the token is not found. 124 | * @param {SessionManager} sessionManager 125 | * @param {TokenValidationDetailsType} validationDetails 126 | * @returns {UserType | null} 127 | */ 128 | export const getUserFromSession = async ( 129 | sessionManager: SessionManager, 130 | validationDetails: TokenValidationDetailsType 131 | ): Promise => { 132 | const idTokenString = (await sessionManager.getSessionItem('id_token')) as string; 133 | 134 | // Validate signature to prevent tampering 135 | const validation = await validateToken({ 136 | token: idTokenString, 137 | domain: validationDetails.issuer, 138 | }); 139 | if (!validation.valid) { 140 | throw new Error(validation.message); 141 | } 142 | 143 | // Decode the ID token for user information 144 | const payload: Record = jwtDecoder(idTokenString) ?? {}; 145 | if (Object.keys(payload).length === 0) { 146 | throw new Error('Invalid ID token'); 147 | } 148 | 149 | const user: UserType = { 150 | family_name: payload.family_name as string, 151 | given_name: payload.given_name as string, 152 | picture: (payload.picture as string) ?? null, 153 | email: payload.email as string, 154 | phone: payload.phone as string, 155 | id: payload.sub as string, 156 | }; 157 | 158 | return user; 159 | }; 160 | 161 | /** 162 | * Checks if the provided JWT token is valid (expired or not). 163 | * @param {string | null} token 164 | * @param {TokenValidationDetailsType} validationDetails 165 | * @returns {boolean} is expired or not 166 | */ 167 | export const isTokenExpired = async ( 168 | token: string | null, 169 | validationDetails: TokenValidationDetailsType 170 | ): Promise => { 171 | if (!token) return true; 172 | try { 173 | // Validate signature to prevent tampering 174 | const validation = await validateToken({ 175 | token, 176 | domain: validationDetails.issuer, 177 | }); 178 | if (!validation.valid) { 179 | return true; 180 | } 181 | 182 | const currentUnixTime = Math.floor(Date.now() / 1000); 183 | const payload = jwtDecoder(token); 184 | if (!payload || payload.exp === undefined) return true; 185 | return currentUnixTime >= payload.exp; 186 | } catch (e) { 187 | return true; 188 | } 189 | }; 190 | -------------------------------------------------------------------------------- /lib/sdk/utilities/types.ts: -------------------------------------------------------------------------------- 1 | export interface TokenCollection { 2 | refresh_token: string; 3 | access_token: string; 4 | id_token: string; 5 | } 6 | 7 | export interface UserType { 8 | picture: null | string; 9 | family_name: string; 10 | given_name: string; 11 | email: string; 12 | id: string; 13 | phone: string; 14 | } 15 | 16 | export type TokenType = 'refresh_token' | 'access_token' | 'id_token'; 17 | export type ClaimTokenType = 'access_token' | 'id_token'; 18 | 19 | export enum FlagDataType { 20 | s = 'string', 21 | b = 'boolean', 22 | i = 'number', 23 | } 24 | 25 | export interface FlagType { 26 | s: string; 27 | b: boolean; 28 | i: number; 29 | } 30 | 31 | export interface FeatureFlag { 32 | t: T; 33 | v: FlagType[T]; 34 | } 35 | 36 | export type FeatureFlags = Record | undefined>; 37 | 38 | export interface GetFlagType { 39 | type?: 'string' | 'boolean' | 'number'; 40 | value: FlagType[keyof FlagType]; 41 | is_default: boolean; 42 | code: string; 43 | } 44 | 45 | export interface TokenValidationDetailsType { 46 | issuer: string; 47 | audience?: string | string[]; 48 | } 49 | -------------------------------------------------------------------------------- /lib/sdk/utilities/validate-client-secret.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Validates that the supplied client secret is in the correct format. 3 | * @param {string} secret 4 | * @returns {boolean} 5 | */ 6 | export const validateClientSecret = (secret: string): boolean => { 7 | return !!secret.match('^[a-zA-Z0-9]{40,60}$')?.length; 8 | }; 9 | -------------------------------------------------------------------------------- /lib/sdk/version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is read-only, the const SDK_VERSION will be set to the package version 3 | * from package.json at build time, this action may or may not effect any changes 4 | * you make to this file. We therefore recommend that you refrain from editing this 5 | * file. 6 | */ 7 | 8 | import { type SDKHeaderOverrideOptions } from './oauth2-flows/index.js'; 9 | 10 | export const SDK_VERSION = 'SDK_VERSION_PLACEHOLDER' as const; 11 | 12 | export const getSDKHeader = ( 13 | options: SDKHeaderOverrideOptions = {} 14 | ): [string, string] => { 15 | const version = options.frameworkVersion ?? SDK_VERSION; 16 | const framework = options.framework ?? 'TypeScript'; 17 | const headerValue = `${framework}/${version}`; 18 | return ['Kinde-SDK', headerValue]; 19 | }; 20 | -------------------------------------------------------------------------------- /openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "7.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /package-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kinde-oss/kinde-typescript-sdk", 3 | "version": "2.13.0", 4 | "description": "Kinde Typescript SDK", 5 | "main": "dist-cjs/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/types/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "dist-cjs" 11 | ], 12 | "exports": { 13 | ".": { 14 | "require": { 15 | "types": "./dist-cjs/types/index.d.ts", 16 | "default": "./dist-cjs/index.js" 17 | }, 18 | "import": { 19 | "types": "./dist/types/index.d.ts", 20 | "default": "./dist/index.js" 21 | } 22 | } 23 | }, 24 | "scripts": { 25 | "build": "npm run generate && tsc && tsc -p tsconfig.cjs.json", 26 | "postbuild": "node sdk-version.js clean && ncp ./package-cjs.json ./dist-cjs/package.json && ncp ./package-esm.json ./dist/package.json", 27 | "prebuild": "node sdk-version.js && rimraf dist dist-cjs lib/models lib/apis", 28 | "lint": "eslint . && prettier . --check", 29 | "lint:fix": "eslint --fix . && prettier . --write", 30 | "test": "vitest", 31 | "test:coverage": "vitest --coverage", 32 | "lint-staged": "lint-staged", 33 | "husky": "husky install", 34 | "generate": "npx @openapitools/openapi-generator-cli generate -i ./kinde-mgmt-api-specs.yaml -c ./generator-config.yaml -g typescript-fetch -o ./lib --additional-properties=importFileExtension=.js" 35 | }, 36 | "author": { 37 | "name": "Kinde", 38 | "email": "engineering@kinde.com", 39 | "url": "https://kinde.com" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/kinde-oss/kinde-typescript-sdk" 44 | }, 45 | "bugs": "https://github.com/kinde-oss/kinde-typescript-sdk", 46 | "homepage": "https://kinde.com", 47 | "license": "MIT", 48 | "devDependencies": { 49 | "@openapitools/openapi-generator-cli": "^2.7.0", 50 | "@tsconfig/node18": "^18.0.0", 51 | "@types/jsdom": "^21.1.1", 52 | "@types/node": "^22.18.1", 53 | "@typescript-eslint/eslint-plugin": "^8.42.0", 54 | "@vitest/coverage-v8": "^3.2.4", 55 | "eslint": "^9.24.0", 56 | "eslint-config-prettier": "^10.1.2", 57 | "eslint-plugin-import": "^2.31.0", 58 | "eslint-plugin-n": "^17.17.0", 59 | "eslint-plugin-prettier": "^5.2.6", 60 | "eslint-plugin-promise": "^7.2.1", 61 | "husky": "^9.0.0", 62 | "jose": "^6.0.10", 63 | "jsdom": "^26.0.0", 64 | "lint-staged": "^16.1.6", 65 | "ncp": "^2.0.0", 66 | "prettier": "^3.5.3", 67 | "rimraf": "^6.0.1", 68 | "typescript": "^5.8.3", 69 | "vitest": "^3.2.4" 70 | }, 71 | "dependencies": { 72 | "@kinde/js-utils": "0.23.0", 73 | "@kinde/jwt-decoder": "^0.2.0", 74 | "@kinde/jwt-validator": "^0.4.0", 75 | "@typescript-eslint/parser": "^8.42.0", 76 | "eslint-config-love": "125.0.0", 77 | "uncrypto": "^0.1.3" 78 | }, 79 | "packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a" 80 | } 81 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"] 4 | } 5 | -------------------------------------------------------------------------------- /sdk-version.js: -------------------------------------------------------------------------------- 1 | const { version: packageVersion } = require('./package.json'); 2 | const { promises: fsPromises } = require('fs'); 3 | const { join } = require('path'); 4 | 5 | const VERSION_PLACEHOLDER = 'SDK_VERSION_PLACEHOLDER'; 6 | 7 | const run = async () => { 8 | const pathToFile = join(__dirname, 'lib/sdk/version.ts'); 9 | const fileContent = await fsPromises.readFile(pathToFile, 'utf8'); 10 | 11 | const isCleanArg = process.argv[2] === 'clean'; 12 | const [searchValue, replaceValue] = !isCleanArg 13 | ? [VERSION_PLACEHOLDER, packageVersion] 14 | : [packageVersion, VERSION_PLACEHOLDER]; 15 | 16 | const updatedContent = fileContent.replace(searchValue, replaceValue); 17 | await fsPromises.writeFile(pathToFile, updatedContent); 18 | }; 19 | 20 | run().catch((error) => console.error(error)); 21 | -------------------------------------------------------------------------------- /templates/index.mustache: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | import * as apis from './apis/index.js'; 4 | import * as models from './models/index.js'; 5 | 6 | export * from './runtime.js'; 7 | {{#useSagaAndRecords}} 8 | export * from './runtimeSagasAndRecords'; 9 | export * from './ApiEntitiesRecord'; 10 | export * from './ApiEntitiesReducer'; 11 | export * from './ApiEntitiesSelectors'; 12 | {{/useSagaAndRecords}} 13 | {{#apiInfo}} 14 | {{#apis.0}} 15 | export * from './apis/index.js'; 16 | {{/apis.0}} 17 | {{/apiInfo}} 18 | {{#models.0}} 19 | export * from './models/index.js'; 20 | {{/models.0}} 21 | export * from './sdk/index.js'; 22 | 23 | export const managementApi = { 24 | apis, models 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "./dist-cjs", 6 | "declarationDir": "dist-cjs/types", 7 | }, 8 | "include": ["lib"], 9 | "exclude": ["node_modules", "tsconfig.json"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["*.ts", "*.js"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "downlevelIteration": true, 6 | "lib": ["esnext", "dom"], 7 | "declaration": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "declarationDir": "dist/types", 11 | "outDir": "./dist", 12 | "strict": true, 13 | "esModuleInterop": true 14 | }, 15 | "include": ["lib"], 16 | "exclude": ["node_modules", "tsconfig.cjs.json"] 17 | } 18 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test: { 3 | environment: 'node', 4 | include: ['lib/**/*.{test,spec}.ts'], 5 | exclude: ['lib/__tests__/mocks.ts', 'lib/**/*.browser.{test,spec}.ts'], 6 | globals: true, 7 | setupFiles: ['./vitest.setup.js'], 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /vitest.setup.js: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'node:crypto'; 2 | import { vi } from 'vitest'; 3 | import { createJwtValidatorMock } from './lib/__tests__/mocks.js'; 4 | 5 | // Polyfill crypto for Node.js environment 6 | if (typeof global.crypto === 'undefined') { 7 | global.crypto = webcrypto; 8 | } 9 | 10 | // Mock @kinde/jwt-validator globally 11 | vi.mock('@kinde/jwt-validator', () => createJwtValidatorMock()); 12 | -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from 'node:crypto'; 2 | import { vi } from 'vitest'; 3 | import { createJwtValidatorMock } from './lib/__tests__/mocks'; 4 | 5 | // Polyfill crypto for Node.js environment 6 | if (typeof global.crypto === 'undefined') { 7 | global.crypto = webcrypto as any; 8 | } 9 | 10 | // Mock @kinde/jwt-validator globally 11 | vi.mock('@kinde/jwt-validator', () => createJwtValidatorMock()); 12 | --------------------------------------------------------------------------------