├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── .template-lintrc.js ├── .watchmanconfig ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── addon ├── .gitkeep ├── adapters │ ├── oidc-json-api-adapter.js │ └── oidc-rest-adapter.js ├── apollo.js ├── authenticators │ └── oidc.js ├── config.js ├── index.js ├── routes │ └── oidc-authentication.js ├── services │ └── session.js ├── unauthorized.js └── utils │ ├── absolute-url.js │ └── pkce.js ├── app ├── .gitkeep ├── adapters │ ├── oidc-json-api-adapter.js │ └── oidc-rest-adapter.js ├── authenticators │ └── oidc.js ├── routes │ └── oidc-authentication.js └── services │ └── session.js ├── docs └── migration-v4.md ├── ember-cli-build.js ├── eslint.config.mjs ├── index.js ├── package.json ├── pnpm-lock.yaml ├── testem.js └── tests ├── dummy ├── app │ ├── adapters │ │ └── application.js │ ├── app.js │ ├── components │ │ └── .gitkeep │ ├── controllers │ │ ├── .gitkeep │ │ ├── application.js │ │ └── protected │ │ │ ├── apollo.js │ │ │ └── users.js │ ├── helpers │ │ └── .gitkeep │ ├── index.html │ ├── models │ │ ├── .gitkeep │ │ └── user.js │ ├── router.js │ ├── routes │ │ ├── .gitkeep │ │ ├── application.js │ │ ├── index.js │ │ ├── login.js │ │ ├── oidc.js │ │ ├── oidcend.js │ │ ├── protected.js │ │ └── protected │ │ │ ├── apollo.js │ │ │ ├── profile.js │ │ │ ├── secret.js │ │ │ └── users.js │ ├── serializers │ │ └── application.js │ ├── services │ │ ├── apollo.js │ │ └── store.js │ ├── styles │ │ └── app.css │ └── templates │ │ ├── application.hbs │ │ ├── components │ │ └── .gitkeep │ │ ├── index.hbs │ │ ├── protected.hbs │ │ └── protected │ │ ├── apollo.hbs │ │ ├── profile.hbs │ │ ├── secret.hbs │ │ └── users.hbs ├── config │ ├── ember-cli-update.json │ ├── ember-try.js │ ├── environment.js │ ├── optional-features.json │ └── targets.js ├── mirage │ ├── config.js │ ├── scenarios │ │ └── default.js │ └── serializers │ │ └── application.js └── public │ └── robots.txt ├── helpers └── index.js ├── index.html ├── integration └── .gitkeep ├── test-helper.js └── unit ├── .gitkeep ├── adapters ├── oidc-json-api-adapter-test.js └── oidc-rest-adapter-test.js ├── authenticators └── oidc-test.js ├── routes └── oidc-authentication-test.js ├── services └── session-test.js └── utils ├── absolute-url-test.js ├── apollo-test.js └── pkce-test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript 4 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available. 5 | */ 6 | "isTypeScriptProject": false 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | extends: ["@adfinis/eslint-config/ember-addon"], 4 | settings: { 5 | "import/internal-regex": "^ember-simple-auth-oidc/", 6 | }, 7 | rules: { 8 | "ember/no-runloop": "warn", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "friday" 8 | time: "12:00" 9 | timezone: "Europe/Zurich" 10 | - package-ecosystem: npm 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | day: "friday" 15 | time: "12:00" 16 | timezone: "Europe/Zurich" 17 | open-pull-requests-limit: 10 18 | versioning-strategy: increase 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: {} 9 | 10 | concurrency: 11 | group: ci-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: "Tests" 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: pnpm/action-setup@v4 23 | - name: Install Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: pnpm 28 | - name: Install Dependencies 29 | run: pnpm install --frozen-lockfile 30 | - name: Lint 31 | run: pnpm lint 32 | - name: Run Tests 33 | run: pnpm test:ember 34 | 35 | floating: 36 | name: "Floating Dependencies" 37 | runs-on: ubuntu-latest 38 | timeout-minutes: 10 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: pnpm/action-setup@v4 43 | - uses: actions/setup-node@v4 44 | with: 45 | node-version: 20 46 | cache: pnpm 47 | - name: Install Dependencies 48 | run: pnpm install --no-lockfile 49 | - name: Run Tests 50 | run: pnpm test:ember 51 | 52 | try-scenarios: 53 | name: ${{ matrix.try-scenario }} 54 | runs-on: ubuntu-latest 55 | needs: "test" 56 | timeout-minutes: 10 57 | 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | try-scenario: 62 | - ember-lts-4.12 63 | - ember-lts-5.4 64 | - ember-lts-5.8 65 | - ember-lts-5.12 66 | - ember-release 67 | - ember-beta 68 | - ember-canary 69 | - embroider-safe 70 | - embroider-optimized 71 | 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: pnpm/action-setup@v4 75 | - name: Install Node 76 | uses: actions/setup-node@v4 77 | with: 78 | node-version: 20 79 | cache: pnpm 80 | - name: Install Dependencies 81 | run: pnpm install --frozen-lockfile 82 | - name: Run Tests 83 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} 84 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | release: 7 | name: Release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | persist-credentials: false 14 | - uses: pnpm/action-setup@v4 15 | - name: Install Node 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | cache: pnpm 20 | - name: Install Dependencies 21 | run: pnpm install --frozen-lockfile 22 | - name: Release on NPM 23 | run: pnpm semantic-release 24 | env: 25 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /declarations/ 4 | 5 | # dependencies 6 | /node_modules/ 7 | 8 | # misc 9 | /.env* 10 | /.pnp* 11 | /.eslintcache 12 | /coverage/ 13 | /npm-debug.log* 14 | /testem.log 15 | /yarn-error.log 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /npm-shrinkwrap.json.ember-try 20 | /package.json.ember-try 21 | /package-lock.json.ember-try 22 | /yarn.lock.ember-try 23 | 24 | # broccoli-debug 25 | /DEBUG/ 26 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | # skip in CI 2 | [ -n "$CI" ] && exit 0 3 | 4 | # lint commit message 5 | pnpm commitlint --edit "$1" 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | # skip in CI 2 | [ -n "$CI" ] && exit 0 3 | 4 | # lint commit message 5 | pnpm lint-staged 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # misc 6 | /.editorconfig 7 | /.ember-cli 8 | /.env* 9 | /.eslintcache 10 | /.eslintignore 11 | /.eslintrc.js 12 | /.git/ 13 | /.github/ 14 | /.gitignore 15 | /.prettierignore 16 | /.prettierrc.js 17 | /.stylelintignore 18 | /.stylelintrc.js 19 | /.template-lintrc.js 20 | /.travis.yml 21 | /.watchmanconfig 22 | /CONTRIBUTING.md 23 | /ember-cli-build.js 24 | /testem.js 25 | /tests/ 26 | /tsconfig.declarations.json 27 | /tsconfig.json 28 | /yarn-error.log 29 | /yarn.lock 30 | .gitkeep 31 | 32 | # ember-try 33 | /.node_modules.ember-try/ 34 | /npm-shrinkwrap.json.ember-try 35 | /package.json.ember-try 36 | /package-lock.json.ember-try 37 | /yarn.lock.ember-try 38 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # misc 8 | /coverage/ 9 | !.* 10 | .*/ 11 | 12 | # ember-try 13 | /.node_modules.ember-try/ 14 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | overrides: [ 5 | { 6 | files: "*.{js,ts}", 7 | options: { 8 | singleQuote: false, 9 | }, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # unconventional files 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # addons 8 | /.node_modules.ember-try/ 9 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extends: ["stylelint-config-standard", "stylelint-prettier/recommended"], 5 | }; 6 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | extends: "recommended", 5 | }; 6 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [7.0.4](https://github.com/adfinis/ember-simple-auth-oidc/compare/v7.0.3...v7.0.4) (2024-11-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update dependencies ([3d5eb10](https://github.com/adfinis/ember-simple-auth-oidc/commit/3d5eb1022250b498c16c0fa664859d7650d88695)) 7 | 8 | ## [7.0.3](https://github.com/adfinis/ember-simple-auth-oidc/compare/v7.0.2...v7.0.3) (2024-09-27) 9 | 10 | 11 | ### Reverts 12 | 13 | * Revert "fix(refresh): set nextURL on failed refresh" ([519c5fa](https://github.com/adfinis/ember-simple-auth-oidc/commit/519c5fa56b1ebd1f85c510fa9b07467c6bd0eac7)) 14 | 15 | ## [7.0.2](https://github.com/adfinis/ember-simple-auth-oidc/compare/v7.0.1...v7.0.2) (2024-09-27) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **refresh:** set nextURL on failed refresh ([7dee0f8](https://github.com/adfinis/ember-simple-auth-oidc/commit/7dee0f8addf358f828bc076d509c53a91bd8fe81)) 21 | 22 | ## [7.0.1](https://github.com/adfinis/ember-simple-auth-oidc/compare/v7.0.0...v7.0.1) (2024-07-26) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** add missing peer dependencies to ember data ([198d99d](https://github.com/adfinis/ember-simple-auth-oidc/commit/198d99d785699cae0107a5918d4b85aac2ecd4b3)) 28 | * **deps:** fix ember source peer dependency range ([dcf57c1](https://github.com/adfinis/ember-simple-auth-oidc/commit/dcf57c10e6d918dcda6a53e795bd07ca3906e653)) 29 | 30 | # [7.0.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v6.0.1...v7.0.0) (2024-06-28) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * **deps:** update dependencies ([d0644ad](https://github.com/adfinis/ember-simple-auth-oidc/commit/d0644ad8ea0b8421c106fa32f81cbe608592fa9f)) 36 | 37 | 38 | ### BREAKING CHANGES 39 | 40 | * **deps:** Drop support for Ember LTS < 4.12 and Node < 18. 41 | 42 | ## [6.0.1](https://github.com/adfinis/ember-simple-auth-oidc/compare/v6.0.0...v6.0.1) (2023-12-27) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * pass config.store to discoverEmberDataModels ([7959dd9](https://github.com/adfinis/ember-simple-auth-oidc/commit/7959dd9bb2a602975ffc1dd838cba12f0aa18a71)) 48 | 49 | # [6.0.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v5.1.0...v6.0.0) (2023-08-30) 50 | 51 | 52 | ### chore 53 | 54 | * **deps:** update dependencies ([7fd699a](https://github.com/adfinis/ember-simple-auth-oidc/commit/7fd699a873b3d56813b119fc49b62cdae7507fbb)) 55 | * **deps:** update dependencies ([2db3d5b](https://github.com/adfinis/ember-simple-auth-oidc/commit/2db3d5be7450dffcc6ffd977654410d1235bc19b)) 56 | 57 | 58 | ### BREAKING CHANGES 59 | 60 | * **deps:** Drop support for `ember-simple-auth` v5. 61 | * **deps:** Drop support for Ember v3, Node < v18 and 62 | `ember-simple-auth` v4. 63 | 64 | # [5.1.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v5.0.3...v5.1.0) (2023-04-20) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * debounce redirect in unauthorized handler ([dd18217](https://github.com/adfinis/ember-simple-auth-oidc/commit/dd18217bee6431f99e947955e09af454e2289b69)) 70 | * prevent race condition in unauthorized handler ([dd4e63c](https://github.com/adfinis/ember-simple-auth-oidc/commit/dd4e63c800d314069912929338d7a93f8e45a61e)) 71 | 72 | 73 | ### Features 74 | 75 | * add config option for debouncing unauthorized handler ([0bafd98](https://github.com/adfinis/ember-simple-auth-oidc/commit/0bafd9800bceb7b455eacdc21067e84a51cb7ba9)) 76 | 77 | ## [5.0.3](https://github.com/adfinis/ember-simple-auth-oidc/compare/v5.0.2...v5.0.3) (2023-03-22) 78 | 79 | 80 | ### Bug Fixes 81 | 82 | * prevent redirect loops to login route ([4e5d909](https://github.com/adfinis/ember-simple-auth-oidc/commit/4e5d909f53a11d4d3be2c4ef2c025051a04129b1)) 83 | 84 | ## [5.0.2](https://github.com/adfinis/ember-simple-auth-oidc/compare/v5.0.1...v5.0.2) (2023-03-16) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * always reconnect on unauthorized responses ([4a2f0a6](https://github.com/adfinis/ember-simple-auth-oidc/commit/4a2f0a6d29cb4900fe0c52a4cb76c7e2fd8174dc)) 90 | 91 | ## [5.0.1](https://github.com/adfinis/ember-simple-auth-oidc/compare/v5.0.0...v5.0.1) (2022-10-20) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * **apollo:** make apollo client a dependency ([31aca2e](https://github.com/adfinis/ember-simple-auth-oidc/commit/31aca2e7ff849d1d7e467d0a6f3eb1609c88ce7c)), closes [#597](https://github.com/adfinis/ember-simple-auth-oidc/issues/597) 97 | 98 | # [5.0.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v4.1.1...v5.0.0) (2022-09-02) 99 | 100 | 101 | ### chore 102 | 103 | * **deps:** update dependencies ([2c298f5](https://github.com/adfinis/ember-simple-auth-oidc/commit/2c298f5015e8090ae3eb6641873a970a9b9f27cf)) 104 | 105 | 106 | ### Features 107 | 108 | * add pkce generation ([e9bbfc3](https://github.com/adfinis/ember-simple-auth-oidc/commit/e9bbfc38e699e8af10ff81bf0c86420a8e1aa341)) 109 | 110 | 111 | ### BREAKING CHANGES 112 | 113 | * **deps:** Drop support for Node v12 and Ember LTS v3.24. 114 | 115 | ## [4.1.1](https://github.com/adfinis/ember-simple-auth-oidc/compare/v4.1.0...v4.1.1) (2022-04-28) 116 | 117 | ### Bug Fixes 118 | 119 | - **auth-route:** store query params of attempted transition ([4a8406d](https://github.com/adfinis/ember-simple-auth-oidc/commit/4a8406d2f8db68d8b7f2f39912232a243bc81dbe)) 120 | 121 | # [4.1.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v4.0.0...v4.1.0) (2022-03-22) 122 | 123 | ### Features 124 | 125 | - **apollo:** add middleware for ember-apollo-client ([7e22f17](https://github.com/adfinis/ember-simple-auth-oidc/commit/7e22f17172d02df77a8390013cabf81ab9cbc04e)) 126 | 127 | # [4.0.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v3.0.1...v4.0.0) (2022-02-04) 128 | 129 | ### Bug Fixes 130 | 131 | - **authentication:** fix collection of attempted transition url ([503a0d5](https://github.com/adfinis/ember-simple-auth-oidc/commit/503a0d5eb0efe0e2f3ba5fad556237cb8b386cfd)) 132 | - **config:** remove usage of ember-get-config ([74a9c0d](https://github.com/adfinis/ember-simple-auth-oidc/commit/74a9c0de5fed30dbedd966e19e8059f9d7699ea9)) 133 | - **debug:** remove console log statements ([eb3af4b](https://github.com/adfinis/ember-simple-auth-oidc/commit/eb3af4b983f45805c57cdc21a5675367057e993a)) 134 | - **dummy:** correct session setup and fix serializer deprecation ([aae998d](https://github.com/adfinis/ember-simple-auth-oidc/commit/aae998da253c7edfe24abcabf242630d816fe5e3)) 135 | - **lint:** add missing linter deps and fix linting errors ([d21c18e](https://github.com/adfinis/ember-simple-auth-oidc/commit/d21c18e6b867aedd7d6f28c3b0d4c0aad2bf2c0e)) 136 | - minor fixes and requested changes ([28a67ac](https://github.com/adfinis/ember-simple-auth-oidc/commit/28a67ac9ef8ffdfe7a6906659e3e02ebaad586ce)) 137 | 138 | - feat(adapter)!: add oidc rest adapter and refactor adapter naming ([2c9f446](https://github.com/adfinis/ember-simple-auth-oidc/commit/2c9f4466a4e36d25c49395e131719cb9b61e8d5d)) 139 | - refactor(octane)!: refactor to native js classes and remove mixins ([b3610e8](https://github.com/adfinis/ember-simple-auth-oidc/commit/b3610e824df94fcf2bd008d9deb96b6ae48b6aa2)) 140 | 141 | ### BREAKING CHANGES 142 | 143 | - Include an adapter subclass of the Ember 144 | RestAdapter to handle OIDC token refreshes and unauthorized 145 | request handling. The existing OIDCadapter is renamed to 146 | OIDCJSONAPIAdapter to clarify the base class origin. 147 | - mixins can no longer be used, requires migration 148 | of consuming ember applications. 149 | 150 | # [4.0.0-beta.2](https://github.com/adfinis/ember-simple-auth-oidc/compare/v4.0.0-beta.1...v4.0.0-beta.2) (2022-02-04) 151 | 152 | ### Bug Fixes 153 | 154 | - **config:** remove usage of ember-get-config ([74a9c0d](https://github.com/adfinis/ember-simple-auth-oidc/commit/74a9c0de5fed30dbedd966e19e8059f9d7699ea9)) 155 | 156 | # [4.0.0-beta.1](https://github.com/adfinis/ember-simple-auth-oidc/compare/v3.0.1...v4.0.0-beta.1) (2022-01-11) 157 | 158 | ### Bug Fixes 159 | 160 | - **authentication:** fix collection of attempted transition url ([503a0d5](503a0d5)) 161 | - **debug:** remove console log statements ([eb3af4b](eb3af4b)) 162 | - **dummy:** correct session setup and fix serializer deprecation ([aae998d](aae998d)) 163 | - **lint:** add missing linter deps and fix linting errors ([d21c18e](d21c18e)) 164 | - minor fixes and requested changes ([28a67ac](28a67ac)) 165 | 166 | - feat(adapter)!: add oidc rest adapter and refactor adapter naming ([2c9f446](2c9f446)) 167 | - refactor(octane)!: refactor to native js classes and remove mixins ([b3610e8](b3610e8)) 168 | 169 | ### BREAKING CHANGES 170 | 171 | - Include an adapter subclass of the Ember 172 | RestAdapter to handle OIDC token refreshes and unauthorized 173 | request handling. The existing OIDCadapter is renamed to 174 | OIDCJSONAPIAdapter to clarify the base class origin. 175 | - mixins can no longer be used, requires migration 176 | of consuming ember applications. 177 | 178 | ## [3.0.1](https://github.com/adfinis/ember-simple-auth-oidc/compare/v3.0.0...v3.0.1) (2020-11-19) 179 | 180 | ### Bug Fixes 181 | 182 | - **deps:** update ember and other dependencies ([c911827](https://github.com/adfinis/ember-simple-auth-oidc/commit/c911827779b323f3ad9b3181e6d2911eec133e49)) 183 | 184 | # [3.0.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v2.0.0...v3.0.0) (2020-08-18) 185 | 186 | ### Features 187 | 188 | - **single-logout:** separate session invalidate and oidc logout ([628eecb](https://github.com/adfinis/ember-simple-auth-oidc/commit/628eecb77a518122b5c877cccf4fed2bcf279530)) 189 | 190 | ### BREAKING CHANGES 191 | 192 | - **single-logout:** Since v1.0.0 this addon will always perform a single 193 | logout on the authorization server. With this change the default 194 | behaviour is "only" a logout on the current application. If the single 195 | logout should be preserved the consuming application needs to manually 196 | call the new `singleLogout` function. 197 | 198 | # [2.0.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v1.1.1...v2.0.0) (2020-06-18) 199 | 200 | ### Bug Fixes 201 | 202 | - **config:** allow configuration URLs to be absolute and relative ([3477cbc](https://github.com/adfinis/ember-simple-auth-oidc/commit/3477cbcaab839283fc01beac59f9d9a7e5694493)), closes [#189](https://github.com/adfinis/ember-simple-auth-oidc/issues/189) 203 | - **mixin:** correctly recompute `headers` in the `oidc-adapter-mixin` ([d994a6e](https://github.com/adfinis/ember-simple-auth-oidc/commit/d994a6e0b6b0ef2fd587989d3bd1d64aaf972a0a)) 204 | - **mixin:** restore error handling ([31671f5](https://github.com/adfinis/ember-simple-auth-oidc/commit/31671f530d78d980092d77f1fb814f0da9e0be0c)) 205 | 206 | ### chore 207 | 208 | - **deps:** update ember and other dependencies ([4d3bad3](https://github.com/adfinis/ember-simple-auth-oidc/commit/4d3bad3ecc087b95e9dab9ef43083564d91505e9)) 209 | 210 | ### Features 211 | 212 | - add support for ember-simple-auth 3 ([e86f571](https://github.com/adfinis/ember-simple-auth-oidc/commit/e86f571aded982619b1c2b147c4b4447d1e519d0)) 213 | 214 | ### BREAKING CHANGES 215 | 216 | - **deps:** Support for the old ember LTS 3.8 is dropped 217 | 218 | ## [1.1.1](https://github.com/adfinis/ember-simple-auth-oidc/compare/v1.1.0...v1.1.1) (2020-04-22) 219 | 220 | ### Bug Fixes 221 | 222 | - **mixin:** store id_token for use as id_token_hint on logout ([f6adf36](https://github.com/adfinis/ember-simple-auth-oidc/commit/f6adf36deca6bf66e5cd8e780f3d193eab83175a)) 223 | 224 | # [1.1.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v1.0.0...v1.1.0) (2020-01-22) 225 | 226 | ### Bug Fixes 227 | 228 | - **logout:** prevent overriding continueTransition if it's already set ([5080a03](https://github.com/adfinis/ember-simple-auth-oidc/commit/5080a03bb0f9a124905b9fbefe58ba6a6e72256a)) 229 | 230 | ### Features 231 | 232 | - add function to handle unauthorized responses ([5d131c3](https://github.com/adfinis/ember-simple-auth-oidc/commit/5d131c37b9ce9abdc31641dc6d9dd43e7e30b931)) 233 | 234 | # [1.0.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v0.4.3...v1.0.0) (2020-01-22) 235 | 236 | ### Bug Fixes 237 | 238 | - **adapter:** remove deprecated usage of authorize on adapter mixin ([fdd3de4](https://github.com/adfinis/ember-simple-auth-oidc/commit/fdd3de4df98c00998d192517e60c1e6b642b1fcb)) 239 | 240 | ### Features 241 | 242 | - remove support for node 8 ([9cc76a4](https://github.com/adfinis/ember-simple-auth-oidc/commit/9cc76a4691fea05a3fb1d05bb03f094d5c9761af)) 243 | - store redirect URL before logout ([9ae445e](https://github.com/adfinis/ember-simple-auth-oidc/commit/9ae445e76d7dbba7a968cd99f4b2d13c8ff9c1d0)) 244 | - **license:** move from MIT to LGPL-3.0-or-later license ([ce3e635](https://github.com/adfinis/ember-simple-auth-oidc/commit/ce3e6356936243bb3dc86ba0b89cb4f57a365124)) 245 | 246 | ### BREAKING CHANGES 247 | 248 | - **license:** This project is now licensed under the LGPL-3.0-or-later 249 | license instead of the MIT license. 250 | - This removes the need for the `OIDCEndSessionRouteMixin`. It can simply be replaced by the ESA native call of `session.invalidate()` 251 | 252 | This enables the user to store the source URL after logging out. The user will then be redirected to that source after the next login. 253 | 254 | - Node version 8.x is not supported anymore since it's 255 | not a maintained LTS version. 256 | 257 | ## [0.4.3](https://github.com/adfinis/ember-simple-auth-oidc/compare/v0.4.2...v0.4.3) (2019-10-04) 258 | 259 | ### Changes 260 | 261 | - **dependencies:** update dependencies 262 | 263 | ## [0.4.2](https://github.com/adfinis/ember-simple-auth-oidc/compare/v0.4.1...v0.4.2) (2019-09-09) 264 | 265 | ### Bug Fixes 266 | 267 | - **authenticator:** await successful retry before setting the session ([18b9c1f](https://github.com/adfinis/ember-simple-auth-oidc/commit/18b9c1f)) 268 | 269 | ## [0.4.1](https://github.com/adfinis/ember-simple-auth-oidc/compare/v0.4.0...v0.4.1) (2019-09-06) 270 | 271 | ### Bug Fixes 272 | 273 | - **authenticator:** retry token refresh on error ([63cd8d3](https://github.com/adfinis/ember-simple-auth-oidc/commit/63cd8d3)) 274 | 275 | # [0.4.0](https://github.com/adfinis/ember-simple-auth-oidc/compare/v0.3.0...v0.4.0) (2019-07-25) 276 | 277 | ### Bug Fixes 278 | 279 | - **continue-transition:** do not trigger intercepted transition twice ([1fafa76](https://github.com/adfinis/ember-simple-auth-oidc/commit/1fafa76)) 280 | - **dummy-app:** fix queryParams handling in dummy ([76ab8ef](https://github.com/adfinis/ember-simple-auth-oidc/commit/76ab8ef)) 281 | 282 | ### Features 283 | 284 | - **redirect:** add support for login_hint ([9074063](https://github.com/adfinis/ember-simple-auth-oidc/commit/9074063)) 285 | 286 | # Change Log 287 | 288 | All notable changes to this project will be documented in this file. 289 | 290 | The format is based on [Keep a Changelog](https://keepachangelog.com/) 291 | and this project adheres to [Semantic Versioning](https://semver.org/). 292 | 293 | ## [0.2.0] - 2019-02-04 294 | 295 | ### Changed 296 | 297 | - Remove `realm` part as this is keycloak and not OIDC specific. In the case 298 | of a keycloak implementation, the `realm` should be part of the `host`. 299 | This change is not backwards compatible! Just remove the `realm` property 300 | from your configuration and add it directly to the `host` property. 301 | - Add required config option `scope` as scope is required by OIDC standard and 302 | is now always delivered to the auth endpoint 303 | - Add required config option `userinfoEndpoint` 304 | - Add optional config option `expiresIn` 305 | - Remove default values for all endpoint config options. They need to be set 306 | specifically in the project config file. 307 | - No longer parse the `access_token` for user information instead request the 308 | user information from the userinfo endpoint. Make sure the userinfo endpoint 309 | is available and correctly configured! 310 | - Use the `expires_in` time from the token endpoint if available otherwise 311 | fallback to the config `expiresIn` value. 312 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | - `git clone ` 6 | - `cd ember-simple-auth-oidc` 7 | - `pnpm install` 8 | 9 | ## Linting 10 | 11 | Automatic linting via `husky` _pre-commit_ is setup. For manual linting use: 12 | 13 | - `pnpm lint` 14 | - `pnpm lint:fix` 15 | 16 | ## Formatting 17 | 18 | Please stick to [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages to make our semantic release versioning work. 19 | 20 | ## Running tests 21 | 22 | - `pnpm test` – Runs the test suite on the current Ember version 23 | - `pnpm test:ember --server` – Runs the test suite in "watch mode" 24 | - `pnpm test:ember-compatibility` – Runs the test suite against multiple Ember versions 25 | 26 | ## Running the dummy application 27 | 28 | - `pnpm start` 29 | - Visit the dummy application at [http://localhost:4200](http://localhost:4200). 30 | 31 | For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/). 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-simple-auth-oidc 2 | 3 | [![npm version](https://badge.fury.io/js/ember-simple-auth-oidc.svg)](https://www.npmjs.com/package/ember-simple-auth-oidc) 4 | [![Test](https://github.com/adfinis/ember-simple-auth-oidc/actions/workflows/ci.yml/badge.svg)](https://github.com/adfinis/ember-simple-auth-oidc/actions/workflows/ci.yml) 5 | [![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 7 | 8 | A [Ember Simple Auth](http://ember-simple-auth.com) addon which implements the 9 | OpenID Connect [Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth). 10 | 11 | ## Installation 12 | 13 | - Ember.js v4.12 or above 14 | - Ember CLI v4.12 or above 15 | - Node.js v18 or above 16 | 17 | Note: The addon uses [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 18 | in its implementation, if IE browser support is necessary, a polyfill needs to be provided. 19 | 20 | ```bash 21 | $ ember install ember-simple-auth-oidc 22 | ``` 23 | 24 | If you're upgrading from 3.x to 4.x see the [upgrade guide](docs/migration-v4.md). 25 | 26 | ## Usage 27 | 28 | To use the oidc authorization code flow the following elements need to be added 29 | to the Ember application. 30 | 31 | The login / authentication route (for example the Ember Simple Auth default `/login`) 32 | needs to extend from the `OIDCAuthenticationRoute`, which handles the authentication 33 | procedure. In case the user is already authenticated, the transition is aborted. 34 | 35 | ```js 36 | // app/routes/login.js 37 | 38 | import OIDCAuthenticationRoute from "ember-simple-auth-oidc/routes/oidc-authentication"; 39 | 40 | export default class LoginRoute extends OIDCAuthenticationRoute {} 41 | ``` 42 | 43 | Authenticated routes need to call `session.requireAuthentication` in their 44 | respective `beforeModel`, to ensure that unauthenticated transitions are 45 | prevented and redirected to the authentication route. It's recommended to 46 | await the `beforeModel` hook, to make sure authentication is handled before 47 | other API calls are triggered (which might lead to `401` responses, potentially 48 | causing redirect loops). 49 | 50 | ```js 51 | // app/routes/protected.js 52 | 53 | import Route from "@ember/routing/route"; 54 | import { inject as service } from "@ember/service"; 55 | 56 | export default class ProtectedRoute extends Route { 57 | @service session; 58 | 59 | async beforeModel(transition) { 60 | await this.session.requireAuthentication(transition, "login"); 61 | } 62 | } 63 | ``` 64 | 65 | To include authorization info in all Ember Data requests override `headers` in 66 | the application adapter and include `session.headers` alongside any other 67 | necessary headers. By extending the application adapter from either of the 68 | provided `OIDCJSONAPIAdapter` or `OIDCRESTAdapter`, the `access_token` is 69 | refreshed before Ember Data requests, if necessary. Both the `OIDCJSONAPIAdapter` 70 | and the `OIDCRESTAdapter` also provide default headers with the authorization 71 | header included. 72 | 73 | ```js 74 | // app/adapters/application.js 75 | 76 | import { inject as service } from "@ember/service"; 77 | import OIDCJSONAPIAdapter from "ember-simple-auth-oidc/adapters/oidc-json-api-adapter"; 78 | 79 | export default class ApplicationAdapter extends OIDCJSONAPIAdapter { 80 | @service session; 81 | 82 | get headers() { 83 | return { ...this.session.headers, "Content-Language": "en-us" }; 84 | } 85 | } 86 | ``` 87 | 88 | `ember-simple-auth-oidc` also provides a middleware which handles authorization 89 | and unauthorization on the apollo service provided by `ember-apollo-client`. 90 | Simply, wrap the http link in `apolloMiddleware` like so: 91 | 92 | ```js 93 | // app/services/apollo.js 94 | 95 | import { inject as service } from "@ember/service"; 96 | import ApolloService from "ember-apollo-client/services/apollo"; 97 | import { apolloMiddleware } from "ember-simple-auth-oidc"; 98 | 99 | export default class CustomApolloService extends ApolloService { 100 | @service session; 101 | 102 | link() { 103 | const httpLink = super.link(); 104 | 105 | return apolloMiddleware(httpLink, this.session); 106 | } 107 | } 108 | ``` 109 | 110 | The provided adapters and the apollo middleware already handle authorization and 111 | unauthorized requests properly. If you want the same behaviour for other request 112 | services as well, you can use the `handleUnauthorized` function and the 113 | `refreshAuthentication.perform` method on the session. The following snippet 114 | shows an example of a custom fetch service with proper authentication handling: 115 | 116 | ```js 117 | import Service, { inject as service } from "@ember/service"; 118 | import { handleUnauthorized } from "ember-simple-auth-oidc"; 119 | import fetch from "fetch"; 120 | 121 | export default class FetchService extends Service { 122 | @service session; 123 | 124 | async fetch(url) { 125 | await this.session.refreshAuthentication.perform(); 126 | 127 | const response = await fetch(url, { headers: this.session.headers }); 128 | 129 | if (!response.ok && response.status === 401) { 130 | handleUnauthorized(this.session); 131 | } 132 | 133 | return response; 134 | } 135 | } 136 | ``` 137 | 138 | Ember Simple Auth encourages the manual setup of the session service in the `beforeModel` of the 139 | application route, starting with [version 4.1.0](https://github.com/simplabs/ember-simple-auth/releases/tag/4.1.0). 140 | The relevant changes are described in their [upgrade to v4 guide](https://github.com/simplabs/ember-simple-auth/blob/master/guides/upgrade-to-v4.md). 141 | 142 | ### Logout / Explicit invalidation 143 | 144 | There are two ways to invalidate (logout) the current session: 145 | 146 | ```js 147 | session.invalidate(); 148 | ``` 149 | 150 | The session `invalidate` method ends the current ember-simple-auth session and therefore performs a 151 | logout on the ember application. Note that the session on the authorization server is not invalidated 152 | this way and a new token/session can simply be obtained when doing the authentication process again. 153 | 154 | ```js 155 | session.singleLogout(); 156 | ``` 157 | 158 | The session `singleLogout` method will invalidate the current ember-simple-auth session and after that 159 | call the `end-session` endpoint of the authorization server. This will result in a logout of the 160 | ember application and additionally invalidate the session on the authorization server which will logout 161 | the user of all applications using this authorization server! 162 | 163 | ## Configuration 164 | 165 | The addon can be configured in the project's `environment.js` file with the key `ember-simple-auth-oidc`. 166 | 167 | A minimal configuration includes the following options: 168 | 169 | ```js 170 | // config/environment.js 171 | 172 | module.exports = function (environment) { 173 | let ENV = { 174 | // ... 175 | "ember-simple-auth-oidc": { 176 | host: "http://authorization.server/openid", 177 | clientId: "test", 178 | authEndpoint: "/authorize", 179 | tokenEndpoint: "/token", 180 | userinfoEndpoint: "/userinfo", 181 | }, 182 | // ... 183 | }; 184 | return ENV; 185 | }; 186 | ``` 187 | 188 | Here is a complete list of all possible config options: 189 | 190 | **host** `` 191 | A relative or absolute URI of the authorization server. 192 | 193 | **clientId** `` 194 | The oidc client identifier valid at the authorization server. 195 | 196 | **authEndpoint** `` 197 | Authorization endpoint at the authorization server. This can be a path which 198 | will be appended to `host` or an absolute URL. 199 | 200 | **tokenEndpoint** `` 201 | Token endpoint at the authorization server. This can be a path which will be 202 | appended to `host` or an absolute URL. 203 | 204 | **endSessionEndpoint** `` (optional) 205 | End session endpoint endpoint at the authorization server. This can be a path 206 | which will be appended to `host` or an absolute URL. 207 | 208 | **userinfoEndpoint** `` 209 | Userinfo endpoint endpoint at the authorization server. This can be a path 210 | which will be appended to `host` or an absolute URL. 211 | 212 | **afterLogoutUri** `` (optional) 213 | A relative or absolute URI to which will be redirected after logout / end session. 214 | 215 | **scope** `` (optional) 216 | The oidc scope value. Default is `"openid"`. 217 | 218 | **expiresIn** `` (optional) 219 | Milliseconds after which the token expires. This is only a fallback value if the authorization server does not return a `expires_in` value. Default is `3600000` (1h). 220 | 221 | **refreshLeeway** `` (optional) 222 | Milliseconds before expire time at which the token is refreshed. Default is `30000` (30s). 223 | 224 | **tokenPropertyName** `` (optional) 225 | Name of the property which holds the token in a successful authenticate request. Default is `"access_token"`. 226 | 227 | **authHeaderName** `` (optional) 228 | Name of the authentication header holding the token used in requests. Default is `"Authorization"`. 229 | 230 | **authPrefix** `` (optional) 231 | Prefix of the authentication token. Default is `"Bearer"`. 232 | 233 | **loginHintName** `` (optional) 234 | Name of the `login_hint` query paramter which is being forwarded to the authorization server if it is present. This option allows overriding the default name `login_hint`. 235 | 236 | **amountOfRetries** `` (optional) 237 | Amount of retries should be made if the request to fetch a new token fails. Default is `3`. 238 | 239 | **retryTimeout** `` (optional) 240 | Timeout in milliseconds between each retry if a token refresh should fail. Default is `3000`. 241 | 242 | **enablePkce** `` (optional) 243 | Enables PKCE mechanism to provide additional protection during code to token exchanges. Default is `false`. 244 | 245 | **unauthorizedRequestRedirectTimeout** `` (optional) 246 | Debounce timeout for redirection after (multiple) `401` responses are received to prevent redirect loops (at the cost of a small delay). Set to `0` to disable debouncing. Default is `1000`. 247 | 248 | ## License 249 | 250 | This project is licensed under the [LGPL-3.0-or-later license](LICENSE). 251 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/addon/.gitkeep -------------------------------------------------------------------------------- /addon/adapters/oidc-json-api-adapter.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import JSONAPIAdapter from "@ember-data/adapter/json-api"; 3 | import { handleUnauthorized } from "ember-simple-auth-oidc"; 4 | 5 | export default class OIDCJSONAPIAdapter extends JSONAPIAdapter { 6 | @service session; 7 | 8 | constructor(...args) { 9 | super(...args); 10 | 11 | // Proxy ember-data requests to ensure prior token refresh 12 | return new Proxy(this, { 13 | get(target, prop, receiver) { 14 | if ( 15 | [ 16 | "findRecord", 17 | "createRecord", 18 | "updateRecord", 19 | "deleteRecord", 20 | "findAll", 21 | "query", 22 | "findMany", 23 | ].includes(prop) 24 | ) { 25 | return new Proxy(target[prop], { 26 | async apply(...args) { 27 | await target.session.refreshAuthentication.perform(); 28 | return Reflect.apply(...args); 29 | }, 30 | }); 31 | } 32 | return Reflect.get(target, prop, receiver); 33 | }, 34 | }); 35 | } 36 | 37 | get headers() { 38 | return this.session.headers; 39 | } 40 | 41 | handleResponse(status, ...args) { 42 | if (status === 401) { 43 | handleUnauthorized(this.session); 44 | } 45 | return super.handleResponse(status, ...args); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /addon/adapters/oidc-rest-adapter.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import RESTAdapter from "@ember-data/adapter/rest"; 3 | import { handleUnauthorized } from "ember-simple-auth-oidc"; 4 | 5 | export default class OIDCRESTAdapter extends RESTAdapter { 6 | @service session; 7 | 8 | constructor(...args) { 9 | super(...args); 10 | 11 | // Proxy ember-data requests to ensure prior token refresh 12 | return new Proxy(this, { 13 | get(target, prop, receiver) { 14 | if ( 15 | [ 16 | "findRecord", 17 | "createRecord", 18 | "updateRecord", 19 | "deleteRecord", 20 | "findAll", 21 | "query", 22 | "findMany", 23 | ].includes(prop) 24 | ) { 25 | return new Proxy(target[prop], { 26 | async apply(...args) { 27 | await target.session.refreshAuthentication.perform(); 28 | return Reflect.apply(...args); 29 | }, 30 | }); 31 | } 32 | return Reflect.get(target, prop, receiver); 33 | }, 34 | }); 35 | } 36 | 37 | get headers() { 38 | return this.session.headers; 39 | } 40 | 41 | handleResponse(status, ...args) { 42 | if (status === 401) { 43 | handleUnauthorized(this.session); 44 | } 45 | return super.handleResponse(status, ...args); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /addon/apollo.js: -------------------------------------------------------------------------------- 1 | import { setContext } from "@apollo/client/link/context"; 2 | import { onError } from "@apollo/client/link/error"; 3 | import { handleUnauthorized } from "ember-simple-auth-oidc"; 4 | 5 | export default function apolloMiddleware(httpLink, session) { 6 | const authMiddleware = setContext(async (_, context) => { 7 | await session.refreshAuthentication.perform(); 8 | 9 | return { 10 | ...context, 11 | headers: { 12 | ...context.headers, 13 | ...session.headers, 14 | }, 15 | }; 16 | }); 17 | 18 | const authAfterware = onError((error) => { 19 | if (error.networkError && error.networkError.statusCode === 401) { 20 | handleUnauthorized(session); 21 | } 22 | }); 23 | 24 | return authMiddleware.concat(authAfterware).concat(httpLink); 25 | } 26 | -------------------------------------------------------------------------------- /addon/authenticators/oidc.js: -------------------------------------------------------------------------------- 1 | import { later } from "@ember/runloop"; 2 | import { inject as service } from "@ember/service"; 3 | import { 4 | isServerErrorResponse, 5 | isAbortError, 6 | isBadRequestResponse, 7 | } from "ember-fetch/errors"; 8 | import BaseAuthenticator from "ember-simple-auth/authenticators/base"; 9 | import fetch from "fetch"; 10 | import { resolve } from "rsvp"; 11 | import { TrackedObject } from "tracked-built-ins"; 12 | 13 | import config from "ember-simple-auth-oidc/config"; 14 | import getAbsoluteUrl from "ember-simple-auth-oidc/utils/absolute-url"; 15 | 16 | export default class OidcAuthenticator extends BaseAuthenticator { 17 | @service router; 18 | @service session; 19 | 20 | @config config; 21 | 22 | /** 23 | * Authenticate the client with the given authentication code. The 24 | * authentication call will return an access and refresh token which will 25 | * then authenticate the client against the API. 26 | * 27 | * @param {Object} options The authentication options 28 | * @param {String} options.code The authentication code 29 | * @returns {Object} The parsed response data 30 | */ 31 | async authenticate({ code, redirectUri, codeVerifier, isRefresh }) { 32 | if (!this.config.tokenEndpoint || !this.config.userinfoEndpoint) { 33 | throw new Error( 34 | "Please define all OIDC endpoints (auth, token, userinfo)", 35 | ); 36 | } 37 | 38 | if (isRefresh) { 39 | return await this._refresh( 40 | this.session.data.authenticated.refresh_token, 41 | redirectUri, 42 | ); 43 | } 44 | 45 | const bodyObject = { 46 | code, 47 | client_id: this.config.clientId, 48 | grant_type: "authorization_code", 49 | redirect_uri: redirectUri, 50 | }; 51 | 52 | if (this.config.enablePkce) { 53 | bodyObject.code_verifier = codeVerifier; 54 | } 55 | 56 | const body = Object.keys(bodyObject) 57 | .map((k) => `${k}=${encodeURIComponent(bodyObject[k])}`) 58 | .join("&"); 59 | 60 | const response = await fetch( 61 | getAbsoluteUrl(this.config.tokenEndpoint, this.config.host), 62 | { 63 | method: "POST", 64 | headers: { 65 | Accept: "application/json", 66 | "Content-Type": "application/x-www-form-urlencoded", 67 | }, 68 | body, 69 | }, 70 | ); 71 | 72 | const isServerError = isServerErrorResponse(response); 73 | if (isServerError) throw new Error(response.message); 74 | 75 | const data = await response.json(); 76 | 77 | // Failed request 78 | const isBadRequest = isBadRequestResponse(response); 79 | if (isBadRequest) throw data; 80 | 81 | // Store the redirect URI in the session for the restore call 82 | data.redirectUri = redirectUri; 83 | 84 | return this._handleAuthResponse(data); 85 | } 86 | 87 | /** 88 | * Invalidate the current ember simple auth session 89 | * 90 | * @return {Promise} The invalidate promise 91 | */ 92 | invalidate() { 93 | return resolve(true); 94 | } 95 | 96 | /** 97 | * Invalidates the current session (of this application) and calls the 98 | * `end-session` endpoint of the authorization server, which will 99 | * invalidate all sessions which are handled by the authorization server 100 | * (possible for multiple applications). 101 | * 102 | * @param {String} idToken The id_token of the session to invalidate 103 | */ 104 | singleLogout(idToken) { 105 | if (!this.config.endSessionEndpoint) { 106 | return; 107 | } 108 | 109 | const params = []; 110 | 111 | if (this.config.afterLogoutUri) { 112 | params.push( 113 | `post_logout_redirect_uri=${getAbsoluteUrl( 114 | this.config.afterLogoutUri, 115 | )}`, 116 | ); 117 | } 118 | 119 | if (idToken) { 120 | params.push(`id_token_hint=${idToken}`); 121 | } 122 | 123 | this._redirectToUrl( 124 | `${getAbsoluteUrl( 125 | this.config.endSessionEndpoint, 126 | this.config.host, 127 | )}?${params.join("&")}`, 128 | ); 129 | } 130 | 131 | _redirectToUrl(url) { 132 | location.replace(url); 133 | } 134 | 135 | /** 136 | * Restore the session after a page refresh. This will check if an access 137 | * token exists and tries to refresh said token. If the refresh token is 138 | * already expired, the auth backend will throw an error which will cause a 139 | * new login. 140 | * 141 | * @param {Object} sessionData The current session data 142 | * @param {String} sessionData.access_token The raw access token 143 | * @param {String} sessionData.refresh_token The raw refresh token 144 | * @returns {Promise} A promise which resolves with the session data 145 | */ 146 | async restore(sessionData) { 147 | const { refresh_token, expireTime, redirectUri } = sessionData; 148 | 149 | if (!refresh_token) { 150 | throw new Error("Refresh token is missing"); 151 | } 152 | 153 | if (expireTime && expireTime <= new Date().getTime()) { 154 | return await this._refresh(refresh_token, redirectUri); 155 | } 156 | 157 | return sessionData; 158 | } 159 | 160 | /** 161 | * Refresh the access token 162 | * 163 | * @param {String} refresh_token The refresh token 164 | * @returns {Object} The parsed response data 165 | */ 166 | async _refresh(refresh_token, redirectUri, retryCount = 0) { 167 | let isServerError = false; 168 | try { 169 | const bodyObject = { 170 | refresh_token, 171 | client_id: this.config.clientId, 172 | grant_type: "refresh_token", 173 | redirect_uri: redirectUri, 174 | }; 175 | const body = Object.keys(bodyObject) 176 | .map((k) => `${k}=${encodeURIComponent(bodyObject[k])}`) 177 | .join("&"); 178 | 179 | const response = await fetch( 180 | getAbsoluteUrl(this.config.tokenEndpoint, this.config.host), 181 | { 182 | method: "POST", 183 | headers: { 184 | Accept: "application/json", 185 | "Content-Type": "application/x-www-form-urlencoded", 186 | }, 187 | body, 188 | }, 189 | ); 190 | isServerError = isServerErrorResponse(response); 191 | if (isServerError) throw new Error(response.message); 192 | 193 | const data = await response.json(); 194 | 195 | // Failed refresh 196 | const isBadRequest = isBadRequestResponse(response); 197 | if (isBadRequest) return Promise.reject(data); 198 | 199 | // Store the redirect URI in the session for the restore call 200 | data.redirectUri = redirectUri; 201 | 202 | return this._handleAuthResponse(data); 203 | } catch (e) { 204 | if ( 205 | (isServerError || isAbortError(e)) && 206 | retryCount < this.config.amountOfRetries - 1 207 | ) { 208 | return new Promise((resolve) => { 209 | later( 210 | this, 211 | () => 212 | resolve( 213 | this._refresh(refresh_token, redirectUri, retryCount + 1), 214 | ), 215 | this.config.retryTimeout, 216 | ); 217 | }); 218 | } 219 | throw e; 220 | } 221 | } 222 | 223 | /** 224 | * Request user information from the openid userinfo endpoint 225 | * 226 | * @param {String} accessToken The raw access token 227 | * @returns {Object} Object containing the user information 228 | */ 229 | async _getUserinfo(accessToken) { 230 | const response = await fetch( 231 | getAbsoluteUrl(this.config.userinfoEndpoint, this.config.host), 232 | { 233 | headers: { 234 | Authorization: `${this.config.authPrefix} ${accessToken}`, 235 | Accept: "application/json", 236 | }, 237 | }, 238 | ); 239 | 240 | const userinfo = await response.json(); 241 | 242 | return userinfo; 243 | } 244 | 245 | /** 246 | * Handle an auth response. This method parses the token and schedules a 247 | * token refresh before the received token expires. 248 | * 249 | * @param {Object} response The raw response data 250 | * @param {String} response.access_token The raw access token 251 | * @param {String} response.refresh_token The raw refresh token 252 | * @param {Number} response.expires_in Seconds until access_token expires 253 | * @returns {Object} The authentication data 254 | */ 255 | async _handleAuthResponse({ 256 | access_token, 257 | refresh_token, 258 | expires_in, 259 | id_token, 260 | redirectUri, 261 | }) { 262 | const userinfo = await this._getUserinfo(access_token); 263 | 264 | const expireInMilliseconds = expires_in 265 | ? expires_in * 1000 266 | : this.config.expiresIn; 267 | const expireTime = 268 | new Date().getTime() + expireInMilliseconds - this.config.refreshLeeway; 269 | 270 | return new TrackedObject({ 271 | access_token, 272 | refresh_token, 273 | userinfo, 274 | id_token, 275 | expireTime, 276 | redirectUri, 277 | }); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /addon/config.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from "@ember/application"; 2 | 3 | export function getConfig(owner) { 4 | return { 5 | host: "http://localhost:4200", 6 | clientId: "client", 7 | authEndpoint: null, 8 | tokenEndpoint: null, 9 | endSessionEndpoint: null, 10 | afterLogoutUri: null, 11 | userinfoEndpoint: null, 12 | scope: "openid", 13 | // expiresIn is the fallback expire time if none is given 14 | expiresIn: 3600 * 1000, 15 | refreshLeeway: 1000 * 30, 16 | tokenPropertyName: "access_token", 17 | authHeaderName: "Authorization", 18 | authPrefix: "Bearer", 19 | amountOfRetries: 3, 20 | retryTimeout: 3000, 21 | enablePkce: false, 22 | ...(owner.resolveRegistration("config:environment")[ 23 | "ember-simple-auth-oidc" 24 | ] ?? {}), 25 | unauthorizedRequestRedirectTimeout: 1000, 26 | }; 27 | } 28 | 29 | export default function config() { 30 | return { 31 | get() { 32 | return getConfig(getOwner(this)); 33 | }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /addon/index.js: -------------------------------------------------------------------------------- 1 | export { default as handleUnauthorized } from "ember-simple-auth-oidc/unauthorized"; 2 | export { default as apolloMiddleware } from "ember-simple-auth-oidc/apollo"; 3 | -------------------------------------------------------------------------------- /addon/routes/oidc-authentication.js: -------------------------------------------------------------------------------- 1 | import { assert } from "@ember/debug"; 2 | import Route from "@ember/routing/route"; 3 | import { inject as service } from "@ember/service"; 4 | import { v4 } from "uuid"; 5 | 6 | import config from "ember-simple-auth-oidc/config"; 7 | import getAbsoluteUrl from "ember-simple-auth-oidc/utils/absolute-url"; 8 | import { 9 | generatePkceChallenge, 10 | generateCodeVerifier, 11 | } from "ember-simple-auth-oidc/utils/pkce"; 12 | export default class OIDCAuthenticationRoute extends Route { 13 | @service session; 14 | @service router; 15 | 16 | @config config; 17 | 18 | queryParams = { 19 | code: { refreshModel: false }, 20 | state: { refreshModel: false }, 21 | }; 22 | 23 | get redirectUri() { 24 | const { protocol, host } = location; 25 | const path = this.router.urlFor(this.routeName); 26 | return `${protocol}//${host}${path}`; 27 | } 28 | 29 | _redirectToUrl(url) { 30 | location.replace(url); 31 | } 32 | 33 | beforeModel(transition) { 34 | if (transition.from) { 35 | this.session.prohibitAuthentication(transition.from.name); 36 | } 37 | 38 | // PKCE Verifier has to be set in session, because we redirect 39 | if (this.config.enablePkce) { 40 | let pkceCodeVerifier = this.session.data.pkceCodeVerifier; 41 | 42 | if (!pkceCodeVerifier) { 43 | pkceCodeVerifier = generateCodeVerifier(96); 44 | this.session.set("data.pkceCodeVerifier", pkceCodeVerifier); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * Handle unauthenticated requests 51 | * 52 | * This handles two cases: 53 | * 54 | * 1. The URL contains an authentication code and a state. In this case the 55 | * client will try to authenticate with the given parameters. 56 | * 57 | * 2. The URL does not contain an authentication. In this case the client 58 | * will be redirected to the configured identity provider login mask, which will 59 | * then redirect to this route after a successful login. 60 | * 61 | * @param {*} model The model of the route 62 | * @param {Ember.Transition} transition The current transition 63 | * @param {Object} transition.to The destination of the transition 64 | * @param {Object} transition.to.queryParams The query params of the transition 65 | * @param {String} transition.to.queryParams.code The authentication code given by the identity provider 66 | * @param {String} transition.to.queryParams.state The state given by the identity provider 67 | */ 68 | async afterModel(_, transition) { 69 | if (!this.config.authEndpoint) { 70 | throw new Error( 71 | "Please define all OIDC endpoints (auth, token, logout, userinfo)", 72 | ); 73 | } 74 | 75 | const queryParams = transition.to 76 | ? transition.to.queryParams 77 | : transition.queryParams; 78 | 79 | if (queryParams.code) { 80 | return await this._handleCallbackRequest( 81 | queryParams.code, 82 | queryParams.state, 83 | transition, 84 | ); 85 | } 86 | 87 | return this._handleRedirectRequest(queryParams); 88 | } 89 | 90 | /** 91 | * Authenticate with the authentication code given by the identity provider in the redirect. 92 | * 93 | * This will check if the passed state equals the state in the application to 94 | * prevent from CSRF attacks. 95 | * 96 | * If the authentication fails, it will redirect to this route again but 97 | * remove application state and query params. This is very unlikely to happen. 98 | * 99 | * If the authentication succeeds the default behaviour of ember-simple-auth 100 | * will apply and redirect to the entry point of the authenticated part of 101 | * the application. 102 | * 103 | * @param {String} code The authentication code passed by the identity provider 104 | * @param {String} state The state (uuid4) passed by the identity provider 105 | */ 106 | async _handleCallbackRequest(code, state) { 107 | if (state !== this.session.data.state) { 108 | assert("State did not match"); 109 | } 110 | 111 | this.session.set("data.state", undefined); 112 | 113 | const data = { 114 | code, 115 | redirectUri: this.redirectUri, 116 | }; 117 | 118 | if (this.config.enablePkce) { 119 | data.codeVerifier = this.session.data.pkceCodeVerifier; 120 | } 121 | 122 | await this.session.authenticate("authenticator:oidc", data); 123 | } 124 | 125 | /** 126 | * Redirect the client to the configured identity provider login. 127 | * 128 | * This will also generate a uuid4 state which the application stores to the 129 | * local storage. When authenticating, the state passed by the identity provider needs to 130 | * match this state, otherwise the authentication will fail to prevent from 131 | * CSRF attacks. 132 | */ 133 | _handleRedirectRequest(queryParams) { 134 | const state = v4(); 135 | 136 | // Store state to session data 137 | this.session.set("data.state", state); 138 | 139 | /** 140 | * Store the `nextURL` in the localstorage so when the user returns after 141 | * the login he can be sent to the initial destination. 142 | */ 143 | if (!this.session.data.nextURL) { 144 | const url = this.session.attemptedTransition?.intent?.url; 145 | this.session.set("data.nextURL", url); 146 | } 147 | 148 | // forward `login_hint` query param if present 149 | const key = this.config.loginHintName || "login_hint"; 150 | 151 | let search = [ 152 | `client_id=${this.config.clientId}`, 153 | `redirect_uri=${this.redirectUri}`, 154 | `response_type=code`, 155 | `state=${state}`, 156 | `scope=${this.config.scope}`, 157 | queryParams[key] ? `${key}=${queryParams[key]}` : null, 158 | ]; 159 | 160 | if (this.config.enablePkce) { 161 | const pkceChallenge = generatePkceChallenge( 162 | this.session.data.pkceCodeVerifier, 163 | ); 164 | search.push(`code_challenge=${pkceChallenge}`); 165 | search.push("code_challenge_method=S256"); 166 | } 167 | 168 | search = search.filter(Boolean).join("&"); 169 | 170 | this._redirectToUrl( 171 | `${getAbsoluteUrl(this.config.host)}${ 172 | this.config.authEndpoint 173 | }?${search}`, 174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /addon/services/session.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import { enqueueTask } from "ember-concurrency"; 3 | import SessionServiceESA from "ember-simple-auth/services/session"; 4 | 5 | import config from "ember-simple-auth-oidc/config"; 6 | 7 | export default class Service extends SessionServiceESA { 8 | @service router; 9 | 10 | @config config; 11 | 12 | singleLogout() { 13 | const session = this.session; // InternalSession 14 | const authenticator = session._lookupAuthenticator(session.authenticator); 15 | const idToken = this.data.authenticated.id_token; 16 | 17 | // Invalidate the ember-simple-auth session 18 | this.invalidate(); 19 | 20 | // Trigger a single logout on the authorization server 21 | return authenticator.singleLogout(idToken); 22 | } 23 | 24 | get redirectUri() { 25 | const { protocol, host } = location; 26 | const path = this.router.currentURL; 27 | return `${protocol}//${host}${path}`; 28 | } 29 | 30 | /** 31 | * Watch the `data.authenticated.id_token` to recomputed the headers as 32 | * according to the openid-connect specification the `id_token` must always 33 | * be included. 34 | * https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse 35 | */ 36 | get headers() { 37 | const headers = {}; 38 | 39 | if (this.isAuthenticated) { 40 | const token = this.data.authenticated[this.config.tokenPropertyName]; 41 | Object.assign(headers, { 42 | [this.config.authHeaderName]: `${this.config.authPrefix} ${token}`, 43 | }); 44 | } 45 | 46 | return headers; 47 | } 48 | 49 | @enqueueTask 50 | *refreshAuthentication() { 51 | const expireTime = this.data.authenticated.expireTime; 52 | const isExpired = expireTime && expireTime <= new Date().getTime(); 53 | 54 | if (this.isAuthenticated && isExpired) { 55 | try { 56 | return yield this.session.authenticate("authenticator:oidc", { 57 | redirectUri: this.redirectUri, 58 | isRefresh: true, 59 | }); 60 | } catch { 61 | console.warn("Token is invalid. Re-authentification is required."); 62 | } 63 | } 64 | } 65 | 66 | async requireAuthentication(transition, routeOrCallback) { 67 | await this.refreshAuthentication.perform(); 68 | return super.requireAuthentication(transition, routeOrCallback); 69 | } 70 | 71 | /** 72 | * This method is called after a successful authentication and continues an 73 | * intercepted transition if a URL is stored in `nextURL` in the 74 | * localstorage. Otherwise call the parent/super to invoke the normal 75 | * behavior of the `sessionAuthenticated` method. 76 | * 77 | * @method handleAuthentication 78 | * @public 79 | */ 80 | handleAuthentication(routeAfterAuthentication) { 81 | const nextURL = this.data.nextURL; 82 | // nextURL is stored to the localStorage using the 83 | // session service's set method 84 | // eslint-disable-next-line ember/classic-decorator-no-classic-methods 85 | this.set("data.nextURL", undefined); 86 | 87 | if (nextURL) { 88 | this.router.replaceWith(nextURL); 89 | } else { 90 | super.handleAuthentication(routeAfterAuthentication); 91 | } 92 | } 93 | 94 | /** 95 | * Overwriting the standard behavior of handleInvalidation, 96 | * which is redirecting to the rootURL of the app. Since the OIDC addon 97 | * also triggers a redirect in some cases and this could lead to conflicts 98 | * we disable the ember-simple-auth behavior. 99 | * If you wish to redirect after invalidating the session, please handle 100 | * this by overwriting this method in your application route or at the 101 | * exact location where the session is invalidated. 102 | */ 103 | handleInvalidation() {} 104 | } 105 | -------------------------------------------------------------------------------- /addon/unauthorized.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from "@ember/application"; 2 | import { debounce } from "@ember/runloop"; 3 | import { isTesting, macroCondition } from "@embroider/macros"; 4 | 5 | import { getConfig } from "ember-simple-auth-oidc/config"; 6 | import getAbsoluteUrl from "ember-simple-auth-oidc/utils/absolute-url"; 7 | 8 | const replaceUri = (session) => { 9 | location.replace( 10 | getAbsoluteUrl(getConfig(getOwner(session)).afterLogoutUri || ""), 11 | ); 12 | }; 13 | 14 | export default function handleUnauthorized(session) { 15 | if (session.isAuthenticated) { 16 | // Only store current location for redirect if the session is not 17 | // invalidated yet. 18 | session.set("data.nextURL", location.href.replace(location.origin, "")); 19 | session.invalidate(); 20 | } 21 | if (macroCondition(isTesting())) { 22 | // don't redirect in tests 23 | } else { 24 | // Debounce the redirect, so we can collect all unauthorized requests and trigger a final 25 | // redirect. We don't want to interrupt calls to the authorization endpoint nor create race 26 | // conditions when multiple requests land in this unauthorized handler. 27 | debounce( 28 | this, 29 | replaceUri, 30 | session, 31 | getConfig(getOwner(session)).unauthorizedRequestRedirectTimeout, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /addon/utils/absolute-url.js: -------------------------------------------------------------------------------- 1 | export default (url, host = `${location.protocol}//${location.host}`) => { 2 | return /^http(s)?/.test(url) ? url : `${host}${url}`; 3 | }; 4 | -------------------------------------------------------------------------------- /addon/utils/pkce.js: -------------------------------------------------------------------------------- 1 | import base64 from "base64-js"; 2 | import { sha256 } from "js-sha256"; 3 | 4 | // Generate Proof Key for Code Exchange (PKCE, pronounced "pixy"). Based on RFC 7636 5 | 6 | export const RANDOM_DATA_ALPHABET = 7 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 8 | 9 | /** 10 | * Generates a cryptographically safe random string 11 | * from RANDOM_DATA_ALPHABET with the given length. 12 | * 13 | * @param {int} len Length of the PKCE code verifier between 43 and 128. 14 | * @returns {String} PKCE code verifier 15 | */ 16 | export function generateCodeVerifier(len = 43) { 17 | const randomData = crypto.getRandomValues(new Uint8Array(len)); 18 | /** 19 | * cannot use new Array(len).map because map does not work for indexes which have never been set 20 | * [...new Array(len)].map should work but is not ideal for large arrays 21 | */ 22 | const chars = new Array(len); 23 | for (let i = 0; i < len; i++) { 24 | chars[i] = RANDOM_DATA_ALPHABET.charCodeAt( 25 | randomData[i] % RANDOM_DATA_ALPHABET.length, 26 | ); 27 | } 28 | 29 | return String.fromCharCode.apply(null, chars); 30 | } 31 | 32 | /** 33 | * Transforms the given PKCE code verifier to the format given by the RFC of 34 | * BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) 35 | * 36 | * @param {String} codeVerifier PKCE code verifier 37 | * @returns {String} PKCE code challenge 38 | */ 39 | export function generatePkceChallenge(codeVerifier) { 40 | return base64 41 | .fromByteArray(new Uint8Array(sha256.arrayBuffer(codeVerifier))) 42 | .replace(/\+/g, "-") 43 | .replace(/\//g, "_") 44 | .replace(/=+$/, ""); 45 | } 46 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/app/.gitkeep -------------------------------------------------------------------------------- /app/adapters/oidc-json-api-adapter.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-simple-auth-oidc/adapters/oidc-json-api-adapter"; 2 | -------------------------------------------------------------------------------- /app/adapters/oidc-rest-adapter.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-simple-auth-oidc/adapters/oidc-rest-adapter"; 2 | -------------------------------------------------------------------------------- /app/authenticators/oidc.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-simple-auth-oidc/authenticators/oidc"; 2 | -------------------------------------------------------------------------------- /app/routes/oidc-authentication.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-simple-auth-oidc/routes/oidc-authentication"; 2 | -------------------------------------------------------------------------------- /app/services/session.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-simple-auth-oidc/services/session"; 2 | -------------------------------------------------------------------------------- /docs/migration-v4.md: -------------------------------------------------------------------------------- 1 | # Migration to v4 2 | 3 | ember-simple-auth-oidc v4 includes an upgrade to Ember v4 and Ember Simple 4 | Auth v4, which entails the removal of the deprecated usage of mixins. This 5 | results in a number of breaking changes, which are described in the following 6 | sections. Refer to the [Ember Simple Auth](https://github.com/simplabs/ember-simple-auth) 7 | and [Ember v4](https://blog.emberjs.com/the-road-to-ember-4-0/) documentation 8 | for more information. 9 | 10 | In addition, the access token is no longer refreshed through a timer-based 11 | approach and requires an explicit refresh to ensure that the access token 12 | hasn't expired. Although a refresh is ensured by the addon in certain 13 | scenarios, the consuming application needs to be aware of these behavioral 14 | changes, especially when performing authorized requests. 15 | 16 | ## Mixin removal and replacement 17 | 18 | The mixin `AuthenticatedRouteMixin` from Ember Simple Auth should no longer be 19 | used. Instead, authenticated routes can make use of the method `session.requireAuthentication` 20 | of the session service. It ensures that unauthenticated access is prohibited on 21 | the route and any of its subroutes, in which case the user is redirected to the 22 | specified authentication route. If the access is authenticated, it refreshes 23 | the access token before accessing the authenticated route. 24 | 25 | ```diff 26 | // app/routes/protected.js 27 | 28 | import Route from "@ember/routing/route"; 29 | - import AuthenticatedRouteMixin from "ember-simple-auth/mixins/authenticated-route-mixin"; 30 | + import { inject as service } from "@ember/service"; 31 | 32 | - export default class ProtectedRoute extends Route.extend( 33 | - AuthenticatedRouteMixin 34 | - ) {} 35 | + export default class ProtectedRoute extends Route { 36 | + @service session; 37 | + 38 | + beforeModel(transition) { 39 | + this.session.requireAuthentication(transition, "login"); 40 | + } 41 | + } 42 | ``` 43 | 44 | The `OIDCApplicationRouteMixin` is no longer needed, it's functionality is now 45 | handled through the session service. The mixin and the properties 46 | `routeAfterAuthentication` and `routeIfAlreadyAuthenticated` can be removed. 47 | 48 | ```diff 49 | // app/routes/application.js 50 | 51 | import Route from "@ember/routing/route"; 52 | - import OIDCApplicationRouteMixin from "ember-simple-auth-oidc/mixins/oidc-application-route-mixin"; 53 | 54 | - export default class ApplicationRoute extends Route.extend( 55 | - OIDCApplicationRouteMixin 56 | - ) { 57 | - routeAfterAuthentication = "protected"; 58 | - routeIfAlreadyAuthenticated = "protected"; 59 | + export default class ApplicationRoute extends Route {} 60 | ``` 61 | 62 | Instead of using the `OIDCAuthenticationRouteMixin`, the authentication 63 | route should extend from the `OIDCAuthenticationRoute`. It handles the OIDC 64 | authentication process as before and ensures that access to the route is 65 | prohibited to already authenticated users. 66 | 67 | ```diff 68 | // app/routes/login.js 69 | 70 | - import Route from "@ember/routing/route"; 71 | - import OIDCAuthenticationRouteMixin from "ember-simple-auth-oidc/mixins/oidc-authentication-route-mixin"; 72 | + import OIDCAuthenticationRoute from "ember-simple-auth-oidc/routes/oidc-authentication"; 73 | 74 | - export default class LoginRoute extends Route.extend( 75 | - OIDCAuthenticationRouteMixin 76 | - ) {} 77 | + export default class LoginRoute extends OIDCAuthenticationRoute {} 78 | ``` 79 | 80 | The mixin `OIDCAdapterMixin` is no longer needed and can be replaced by either 81 | extending the application adapter from the `OIDCJSONAPIAdapter` or 82 | `OIDCRESTAdapter`. The provided adapters ensure that outgoing Ember Data 83 | requests first trigger an access token refresh, to ensure that the authorization 84 | token is up-to-date. By default, the adapters simply provide the authorization 85 | headers necessary to authorize the Ember Data requests. The headers are also 86 | available through the session service and can be used when overriding the 87 | adapter's headers. The provided adapters contain the necessary logic to handle 88 | 401 responses appropriately. 89 | 90 | ```diff 91 | // app/adapters/application.js 92 | 93 | - import JSONAPIAdapter from "@ember-data/adapter/json-api"; 94 | - import OIDCAdapterMixin from "ember-simple-auth-oidc/mixins/oidc-adapter-mixin"; 95 | + import { inject as service } from "@ember/service"; 96 | + import OIDCJSONAPIAdapter from "ember-simple-auth-oidc/adapters/oidc-json-api-adapter"; 97 | 98 | - export default class ApplicationAdapter extends JSONAPIAdapter.extend( 99 | - OIDCAdapterMixin 100 | - ) {} 101 | + export default class ApplicationAdapter extends OIDCJSONAPIAdapter { 102 | + @service session; 103 | + 104 | + get headers() { 105 | + return { ...this.session.headers, "Content-Language": "en-us" }; 106 | + } 107 | + } 108 | ``` 109 | 110 | ## Session setup 111 | 112 | Ember Simple Auth encourages setting up the session service in the `beforeModel` 113 | of the application route starting with [version 4.1.0](https://github.com/simplabs/ember-simple-auth/releases/tag/4.1.0). 114 | For more information visit their [upgrade to v4 guide](https://github.com/simplabs/ember-simple-auth/blob/master/guides/upgrade-to-v4.md). 115 | 116 | ## Proxy usage and IE11 support 117 | 118 | The new implementation of `OIDCJSONAPIAdapter` and `OIDCRESTAdapter` include 119 | the usage of [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 120 | to ensure that an access token refresh is performed before issuing Ember Data 121 | requests. When using these adapters and requiring IE11 support, a polyfill needs 122 | to be provided. 123 | 124 | ## Access token refresh 125 | 126 | Previous implementations included a timer-based access token refresh mechanism, 127 | which ensured that the access token never expired as long as a valid refresh 128 | token was available. The new implementation automatically refreshes the access 129 | token before transitioning to an authenticated route and before issuing Ember 130 | Data requests. When other kinds of authorized requests are performed, a token 131 | refresh needs to be ensured before making the request, by performing the task 132 | `session.refreshAuthentication` provided through the session service. This will 133 | ensure that the access token is valid and will prevent any unnecessary 401 134 | responses. 135 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const EmberAddon = require("ember-cli/lib/broccoli/ember-addon"); 4 | 5 | module.exports = function (defaults) { 6 | const app = new EmberAddon(defaults, { 7 | // Add options here 8 | "ember-fetch": { 9 | nativePromise: true, 10 | }, 11 | }); 12 | 13 | /* 14 | This build file specifies the options for the dummy test app of this 15 | addon, located in `/tests/dummy` 16 | This build file does *not* influence how the addon or the app using it 17 | behave. You most likely want to be modifying `./index.js` or app's build file 18 | */ 19 | 20 | const { maybeEmbroider } = require("@embroider/test-setup"); 21 | return maybeEmbroider(app, { 22 | skipBabel: [ 23 | { 24 | package: "qunit", 25 | }, 26 | ], 27 | // https://github.com/embroider-build/embroider/issues/1322#issuecomment-1386857904 28 | packageRules: [ 29 | { 30 | package: "@ember-data/store", 31 | addonModules: { 32 | "-private.js": { 33 | dependsOnModules: [], 34 | }, 35 | "-private/system/core-store.js": { 36 | dependsOnModules: [], 37 | }, 38 | "-private/system/model/internal-model.js": { 39 | dependsOnModules: [], 40 | }, 41 | }, 42 | }, 43 | ], 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | import { FlatCompat } from "@eslint/eslintrc"; 5 | import js from "@eslint/js"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all, 13 | }); 14 | 15 | export default [ 16 | { 17 | ignores: [ 18 | "blueprints/*/files/", 19 | "declarations/", 20 | "dist/", 21 | "coverage/", 22 | "!**/.*", 23 | "**/.*/", 24 | ".node_modules.ember-try/", 25 | ], 26 | }, 27 | ...compat.extends("@adfinis/eslint-config/ember-addon"), 28 | { 29 | settings: { 30 | "import/internal-regex": "^ember-simple-auth-oidc/", 31 | }, 32 | 33 | rules: { 34 | "ember/no-runloop": "warn", 35 | }, 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | name: require("./package").name, 5 | options: { 6 | babel: { 7 | plugins: [ 8 | require.resolve("ember-concurrency/async-arrow-task-transform"), 9 | ], 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-simple-auth-oidc", 3 | "version": "7.0.4", 4 | "description": "A Ember Simple Auth addon which implements the OpenID Connect Authorization Code Flow.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "license": "LGPL-3.0-or-later", 9 | "author": "", 10 | "directories": { 11 | "doc": "doc", 12 | "test": "tests" 13 | }, 14 | "repository": "https://github.com/adfinis/ember-simple-auth-oidc", 15 | "scripts": { 16 | "build": "ember build --environment=production", 17 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"", 18 | "lint:css": "stylelint \"**/*.css\"", 19 | "lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"", 20 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"", 21 | "lint:hbs": "ember-template-lint .", 22 | "lint:hbs:fix": "ember-template-lint . --fix", 23 | "lint:js": "eslint . --cache", 24 | "lint:js:fix": "eslint . --fix", 25 | "start": "ember serve", 26 | "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\"", 27 | "test:ember": "ember test", 28 | "test:ember-compatibility": "ember try:each", 29 | "prepare": "husky" 30 | }, 31 | "dependencies": { 32 | "@apollo/client": "^3.13.0", 33 | "@babel/core": "^7.26.0", 34 | "@embroider/macros": "^1.16.11", 35 | "base64-js": "^1.5.1", 36 | "ember-auto-import": "^2.10.0", 37 | "ember-cli-babel": "^8.2.0", 38 | "ember-concurrency": "^4.0.2", 39 | "ember-fetch": "^8.1.2", 40 | "ember-simple-auth": "^6.0.0", 41 | "js-sha256": "^0.11.0", 42 | "tracked-built-ins": "^3.3.0", 43 | "uuid": "^11.0.3" 44 | }, 45 | "devDependencies": { 46 | "@adfinis/eslint-config": "2.1.1", 47 | "@adfinis/semantic-release-config": "5.0.0", 48 | "@babel/eslint-parser": "7.25.9", 49 | "@babel/plugin-proposal-decorators": "7.25.9", 50 | "@commitlint/cli": "19.6.0", 51 | "@commitlint/config-conventional": "19.6.0", 52 | "@ember/optional-features": "2.2.0", 53 | "@ember/string": "4.0.0", 54 | "@ember/test-helpers": "4.0.4", 55 | "@embroider/test-setup": "4.0.0", 56 | "@glimmer/tracking": "1.1.2", 57 | "broccoli-asset-rev": "3.0.0", 58 | "concurrently": "9.1.0", 59 | "ember-apollo-client": "4.1.1", 60 | "ember-cli": "6.0.1", 61 | "ember-cli-clean-css": "3.0.0", 62 | "ember-cli-dependency-checker": "3.3.3", 63 | "ember-cli-htmlbars": "6.3.0", 64 | "ember-cli-inject-live-reload": "2.1.0", 65 | "ember-cli-mirage": "3.0.4", 66 | "ember-cli-sri": "2.1.1", 67 | "ember-cli-terser": "4.0.2", 68 | "ember-data": "5.3.9", 69 | "ember-load-initializers": "3.0.1", 70 | "ember-qunit": "8.1.1", 71 | "ember-resolver": "13.1.0", 72 | "ember-source": "6.0.1", 73 | "ember-source-channel-url": "3.0.0", 74 | "ember-template-lint": "6.0.0", 75 | "ember-try": "3.0.0", 76 | "eslint": "9.17.0", 77 | "eslint-config-prettier": "9.1.0", 78 | "eslint-plugin-ember": "12.3.3", 79 | "eslint-plugin-import": "2.31.0", 80 | "eslint-plugin-n": "17.14.0", 81 | "eslint-plugin-prettier": "5.2.1", 82 | "eslint-plugin-qunit": "8.1.2", 83 | "graphql": "16.9.0", 84 | "graphql-tag": "2.12.6", 85 | "husky": "9.1.7", 86 | "lint-staged": "15.2.10", 87 | "loader.js": "4.7.0", 88 | "miragejs": "0.1.48", 89 | "prettier": "3.5.3", 90 | "qunit": "2.23.1", 91 | "qunit-dom": "3.3.0", 92 | "semantic-release": "24.2.0", 93 | "stylelint": "16.12.0", 94 | "stylelint-config-standard": "36.0.1", 95 | "stylelint-prettier": "5.0.2", 96 | "webpack": "5.98.0" 97 | }, 98 | "peerDependencies": { 99 | "@ember-data/adapter": "~4.12.0 || >= 5.0.0", 100 | "ember-data": "~4.12.0 || >= 5.0.0", 101 | "ember-source": ">= 4.0.0" 102 | }, 103 | "packageManager": "pnpm@9.14.2", 104 | "engines": { 105 | "node": ">= 18" 106 | }, 107 | "ember": { 108 | "edition": "octane" 109 | }, 110 | "ember-addon": { 111 | "configPath": "tests/dummy/config", 112 | "after": "ember-simple-auth" 113 | }, 114 | "release": { 115 | "extends": "@adfinis/semantic-release-config" 116 | }, 117 | "commitlint": { 118 | "extends": [ 119 | "@commitlint/config-conventional" 120 | ] 121 | }, 122 | "lint-staged": { 123 | "*.js": "eslint --cache --fix", 124 | "*.hbs": "ember-template-lint --fix", 125 | "*.css": "stylelint --fix", 126 | "*.{json,md}": "prettier --write" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | test_page: "tests/index.html?hidepassed", 5 | disable_watching: true, 6 | launch_in_ci: ["Chrome"], 7 | launch_in_dev: [], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? "--no-sandbox" : null, 14 | "--headless", 15 | "--disable-dev-shm-usage", 16 | "--disable-software-rasterizer", 17 | "--mute-audio", 18 | "--remote-debugging-port=0", 19 | "--window-size=1440,900", 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | 3 | import OIDCJSONAPIAdapter from "ember-simple-auth-oidc/adapters/oidc-json-api-adapter"; 4 | 5 | export default class ApplicationAdapter extends OIDCJSONAPIAdapter { 6 | @service session; 7 | 8 | get headers() { 9 | return { ...this.session.headers, "Content-Language": "en-us" }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from "@ember/application"; 2 | import config from "dummy/config/environment"; 3 | import loadInitializers from "ember-load-initializers"; 4 | import Resolver from "ember-resolver"; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { action } from "@ember/object"; 3 | import { inject as service } from "@ember/service"; 4 | 5 | export default class ApplicationController extends Controller { 6 | @service session; 7 | 8 | @action 9 | logout() { 10 | this.session.singleLogout(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/protected/apollo.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { action } from "@ember/object"; 3 | import { queryManager } from "ember-apollo-client"; 4 | import { gql } from "graphql-tag"; 5 | 6 | export default class ApolloController extends Controller { 7 | @queryManager apollo; 8 | 9 | @action 10 | triggerUnauthenticated(e) { 11 | this.apollo.mutate({ 12 | mutation: gql` 13 | mutation { 14 | mutate { 15 | clientMutationId 16 | } 17 | } 18 | `, 19 | }); 20 | 21 | e.preventDefault(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/protected/users.js: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { action } from "@ember/object"; 3 | import { inject as service } from "@ember/service"; 4 | import { tracked } from "@glimmer/tracking"; 5 | 6 | export default class ProtectedUsersController extends Controller { 7 | @service store; 8 | @tracked users; 9 | 10 | @action 11 | async fetchUsers() { 12 | this.users = await this.store.findAll("user"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dummy 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | 11 | 12 | 13 | 14 | {{content-for "head-footer"}} 15 | 16 | 17 | {{content-for "body"}} 18 | 19 | 20 | 21 | 22 | {{content-for "body-footer"}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/models/user.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from "@ember-data/model"; 2 | 3 | export default class extends Model { 4 | @attr username; 5 | @attr email; 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from "@ember/routing/router"; 2 | import config from "dummy/config/environment"; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | // eslint-disable-next-line array-callback-return 10 | Router.map(function () { 11 | this.route("login"); 12 | this.route("protected", function () { 13 | this.route("users"); 14 | this.route("profile"); 15 | this.route("secret"); 16 | this.route("apollo"); 17 | }); 18 | this.route("oidc", { 19 | path: "realms/test-realm/protocol/openid-connect/auth", 20 | }); 21 | this.route("oidcend", { 22 | path: "realms/test-realm/protocol/openid-connect/logout", 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | 4 | export default class ApplicationRoute extends Route { 5 | @service session; 6 | 7 | async beforeModel() { 8 | await this.session.setup(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class IndexRoute extends Route {} 4 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/login.js: -------------------------------------------------------------------------------- 1 | import OIDCAuthenticationRoute from "ember-simple-auth-oidc/routes/oidc-authentication"; 2 | 3 | export default class LoginRoute extends OIDCAuthenticationRoute {} 4 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/oidc.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class OidcRoute extends Route { 4 | redirect(_, transition) { 5 | const { redirect_uri, state } = transition.to 6 | ? transition.to.queryParams 7 | : transition.queryParams; 8 | window.location.replace(`${redirect_uri}?code=123456789&state=${state}`); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/oidcend.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class OidcendRoute extends Route { 4 | redirect(_, transition) { 5 | const { post_logout_redirect_uri } = transition.to 6 | ? transition.to.queryParams 7 | : transition.queryParams; 8 | window.location.replace(post_logout_redirect_uri); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/protected.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | 4 | export default class ProtectedRoute extends Route { 5 | @service session; 6 | 7 | beforeModel(transition) { 8 | this.session.requireAuthentication(transition, "login"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/protected/apollo.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { queryManager } from "ember-apollo-client"; 3 | import { gql } from "graphql-tag"; 4 | 5 | export default class ApolloRoute extends Route { 6 | @queryManager apollo; 7 | 8 | model() { 9 | return this.apollo.watchQuery({ 10 | query: gql` 11 | query { 12 | items { 13 | id 14 | name 15 | } 16 | } 17 | `, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/protected/profile.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | 4 | export default class ProtectedProfileRoute extends Route { 5 | @service store; 6 | 7 | model() { 8 | return this.store.findRecord("user", 1); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/protected/secret.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class ProtectedSecretRoute extends Route { 4 | model() {} 5 | } 6 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/protected/users.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class ProtectedUsersRoute extends Route { 4 | model() {} 5 | 6 | setupController(controller, model) { 7 | controller.users = null; 8 | super.setupController(controller, model); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import JSONAPISerializer from "@ember-data/serializer/json-api"; 2 | 3 | export default class ApplicationSerializer extends JSONAPISerializer {} 4 | -------------------------------------------------------------------------------- /tests/dummy/app/services/apollo.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import ApolloService from "ember-apollo-client/services/apollo"; 3 | import { apolloMiddleware } from "ember-simple-auth-oidc"; 4 | 5 | export default class CustomApolloService extends ApolloService { 6 | @service session; 7 | 8 | link() { 9 | const httpLink = super.link(); 10 | 11 | return apolloMiddleware(httpLink, this.session); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/dummy/app/services/store.js: -------------------------------------------------------------------------------- 1 | export { default } from "ember-data/store"; 2 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | /* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | Index 2 | Internal 3 | {{#if this.session.isAuthenticated}} 4 | Profile (401 handling demo) 5 | Users 6 | Apollo 7 | Logout 8 | {{else}} 9 | Login 10 | {{/if}} 11 | 12 | {{outlet}} 13 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |

Public!

2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/protected.hbs: -------------------------------------------------------------------------------- 1 |

Protected!

2 | 3 | {{outlet}} 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/protected/apollo.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#each @model.items as |item|}} 3 |
  • {{item.name}}
  • 4 | {{/each}} 5 |
6 | 7 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/protected/profile.hbs: -------------------------------------------------------------------------------- 1 |

2 | Profile 3 |

-------------------------------------------------------------------------------- /tests/dummy/app/templates/protected/secret.hbs: -------------------------------------------------------------------------------- 1 | You found the secret! 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/protected/users.hbs: -------------------------------------------------------------------------------- 1 |

2 | Users 3 |

4 | 7 | {{#if this.users}} 8 |

9 | List of users 10 |

11 |
    12 | {{#each this.users as |user|}} 13 |
  • 14 |
    15 | {{concat user.username ", " user.email}} 16 |
    17 |
  • 18 | {{/each}} 19 |
20 | {{/if}} -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "6.0.1", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": ["--pnpm", "--no-welcome"] 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-try.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { embroiderSafe, embroiderOptimized } = require("@embroider/test-setup"); 4 | const getChannelURL = require("ember-source-channel-url"); 5 | 6 | module.exports = async function () { 7 | return { 8 | usePnpm: true, 9 | scenarios: [ 10 | { 11 | name: "ember-lts-4.12", 12 | npm: { 13 | devDependencies: { 14 | "ember-source": "~4.12.0", 15 | }, 16 | }, 17 | }, 18 | { 19 | name: "ember-lts-5.4", 20 | npm: { 21 | devDependencies: { 22 | "ember-source": "~5.4.0", 23 | }, 24 | }, 25 | }, 26 | { 27 | name: "ember-lts-5.8", 28 | npm: { 29 | devDependencies: { 30 | "ember-source": "~5.8.0", 31 | }, 32 | }, 33 | }, 34 | { 35 | name: "ember-lts-5.12", 36 | npm: { 37 | devDependencies: { 38 | "ember-source": "~5.12.0", 39 | }, 40 | }, 41 | }, 42 | { 43 | name: "ember-release", 44 | npm: { 45 | devDependencies: { 46 | "ember-source": await getChannelURL("release"), 47 | }, 48 | }, 49 | }, 50 | { 51 | name: "ember-beta", 52 | npm: { 53 | devDependencies: { 54 | "ember-source": await getChannelURL("beta"), 55 | }, 56 | }, 57 | }, 58 | { 59 | name: "ember-canary", 60 | npm: { 61 | devDependencies: { 62 | "ember-source": await getChannelURL("canary"), 63 | }, 64 | }, 65 | }, 66 | embroiderSafe(), 67 | embroiderOptimized(), 68 | ], 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (environment) { 4 | const ENV = { 5 | modulePrefix: "dummy", 6 | environment, 7 | rootURL: "/", 8 | locationType: "history", 9 | apollo: { 10 | apiURL: "http://localhost:4200/graphql", 11 | }, 12 | "ember-simple-auth-oidc": { 13 | host: "http://localhost:4200/realms/test-realm", 14 | clientId: "test-client", 15 | authEndpoint: "/protocol/openid-connect/auth", 16 | tokenEndpoint: "/protocol/openid-connect/token", 17 | endSessionEndpoint: "/protocol/openid-connect/logout", 18 | userinfoEndpoint: "/protocol/openid-connect/userinfo", 19 | afterLogoutUri: "http://localhost:4200", 20 | loginHintName: "custom_login_hint", 21 | expiresIn: 60000, // Short expire time (60s) for testing purpose 22 | refreshLeeway: 1000, 23 | }, 24 | 25 | EmberENV: { 26 | EXTEND_PROTOTYPES: false, 27 | FEATURES: { 28 | // Here you can enable experimental features on an ember canary build 29 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 30 | }, 31 | }, 32 | 33 | APP: { 34 | // Here you can pass flags/options to your application instance 35 | // when it is created 36 | }, 37 | }; 38 | 39 | if (environment === "development") { 40 | // ENV.APP.LOG_RESOLVER = true; 41 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 42 | // ENV.APP.LOG_TRANSITIONS = true; 43 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 44 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 45 | } 46 | 47 | if (environment === "test") { 48 | // Testem prefers this... 49 | ENV.locationType = "none"; 50 | 51 | // keep test console output quieter 52 | ENV.APP.LOG_ACTIVE_GENERATION = false; 53 | ENV.APP.LOG_VIEW_LOOKUPS = false; 54 | 55 | ENV.APP.rootElement = "#ember-testing"; 56 | ENV.APP.autoboot = false; 57 | } 58 | 59 | if (environment === "production") { 60 | // here you can enable a production-specific feature 61 | } 62 | 63 | return ENV; 64 | }; 65 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const browsers = [ 4 | "last 1 Chrome versions", 5 | "last 1 Firefox versions", 6 | "last 1 Safari versions", 7 | ]; 8 | 9 | module.exports = { 10 | browsers, 11 | }; 12 | -------------------------------------------------------------------------------- /tests/dummy/mirage/config.js: -------------------------------------------------------------------------------- 1 | import { discoverEmberDataModels } from "ember-cli-mirage"; 2 | import { createServer, Response } from "miragejs"; 3 | 4 | export default function makeServer(config) { 5 | return createServer({ 6 | ...config, 7 | models: { ...discoverEmberDataModels(config.store), ...config.models }, 8 | routes() { 9 | this.urlPrefix = "http://localhost:4200"; 10 | this.namespace = ""; 11 | this.timing = 0; 12 | 13 | this.post("/realms/test-realm/protocol/openid-connect/token", { 14 | access_token: "access.token", 15 | refresh_token: "refresh.token", 16 | id_token: "id.token", 17 | }); 18 | this.get("/realms/test-realm/protocol/openid-connect/userinfo", { 19 | sub: 1, 20 | }); 21 | this.get("/users"); 22 | this.get("/users/1", {}, 401); 23 | 24 | this.post("/graphql", (_, request) => { 25 | const { query } = JSON.parse(request.requestBody); 26 | 27 | if (query.startsWith("mutation")) { 28 | return new Response(401); 29 | } 30 | 31 | return new Response( 32 | 200, 33 | {}, 34 | { 35 | data: { items: [{ id: 1, name: "Test" }] }, 36 | }, 37 | ); 38 | }); 39 | }, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /tests/dummy/mirage/scenarios/default.js: -------------------------------------------------------------------------------- 1 | export default function (server) { 2 | server.create("user", { username: "user1", email: "user1@example.com" }); 3 | server.create("user", { username: "user2", email: "user2@example.com" }); 4 | } 5 | -------------------------------------------------------------------------------- /tests/dummy/mirage/serializers/application.js: -------------------------------------------------------------------------------- 1 | import { JSONAPISerializer } from "miragejs"; 2 | 3 | export default JSONAPISerializer.extend({}); 4 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | setupApplicationTest as upstreamSetupApplicationTest, 3 | setupRenderingTest as upstreamSetupRenderingTest, 4 | setupTest as upstreamSetupTest, 5 | } from "ember-qunit"; 6 | 7 | // This file exists to provide wrappers around ember-qunit's 8 | // test setup functions. This way, you can easily extend the setup that is 9 | // needed per test type. 10 | 11 | function setupApplicationTest(hooks, options) { 12 | upstreamSetupApplicationTest(hooks, options); 13 | 14 | // Additional setup for application tests can be done here. 15 | // 16 | // For example, if you need an authenticated session for each 17 | // application test, you could do: 18 | // 19 | // hooks.beforeEach(async function () { 20 | // await authenticateSession(); // ember-simple-auth 21 | // }); 22 | // 23 | // This is also a good place to call test setup functions coming 24 | // from other addons: 25 | // 26 | // setupIntl(hooks, 'en-us'); // ember-intl 27 | // setupMirage(hooks); // ember-cli-mirage 28 | } 29 | 30 | function setupRenderingTest(hooks, options) { 31 | upstreamSetupRenderingTest(hooks, options); 32 | 33 | // Additional setup for rendering tests can be done here. 34 | } 35 | 36 | function setupTest(hooks, options) { 37 | upstreamSetupTest(hooks, options); 38 | 39 | // Additional setup for unit tests can be done here. 40 | } 41 | 42 | export { setupApplicationTest, setupRenderingTest, setupTest }; 43 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dummy Tests 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | {{content-for "test-head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | {{content-for "test-head-footer"}} 18 | 19 | 20 | {{content-for "body"}} 21 | {{content-for "test-body"}} 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{content-for "body-footer"}} 37 | {{content-for "test-body-footer"}} 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import { setApplication } from "@ember/test-helpers"; 2 | import Application from "dummy/app"; 3 | import config from "dummy/config/environment"; 4 | import { start } from "ember-qunit"; 5 | import * as QUnit from "qunit"; 6 | import { setup } from "qunit-dom"; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adfinis/ember-simple-auth-oidc/1b0255a91f6c9f58c4c57fac1c46d5fc8a3700d0/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/adapters/oidc-json-api-adapter-test.js: -------------------------------------------------------------------------------- 1 | import { set } from "@ember/object"; 2 | import setupMirage from "ember-cli-mirage/test-support/setup-mirage"; 3 | import { setupTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Unit | Adapter | oidc json api adapter", function (hooks) { 7 | setupTest(hooks); 8 | setupMirage(hooks); 9 | 10 | test("it sets the correct headers", async function (assert) { 11 | const adapter = this.owner.lookup("adapter:oidc-json-api-adapter"); 12 | const session = this.owner.lookup("service:session"); 13 | set(session, "session.isAuthenticated", true); 14 | session.data.authenticated.access_token = "access.token"; 15 | 16 | assert.deepEqual(adapter.headers, { Authorization: "Bearer access.token" }); 17 | }); 18 | 19 | test("it refreshes the access token before ember-data requests", async function (assert) { 20 | const adapter = this.owner.lookup("adapter:oidc-json-api-adapter"); 21 | 22 | adapter.session.refreshAuthentication.perform = () => { 23 | assert.step("refresh"); 24 | }; 25 | 26 | const mockAndTest = async (funcName) => { 27 | adapter[funcName] = () => { 28 | assert.step(funcName); 29 | }; 30 | 31 | await adapter[funcName](); 32 | assert.verifySteps(["refresh", funcName]); 33 | }; 34 | 35 | await mockAndTest("findRecord"); 36 | await mockAndTest("createRecord"); 37 | await mockAndTest("updateRecord"); 38 | await mockAndTest("deleteRecord"); 39 | await mockAndTest("findAll"); 40 | await mockAndTest("query"); 41 | await mockAndTest("findMany"); 42 | }); 43 | 44 | test("it invalidates the session correctly on a 401 response", function (assert) { 45 | const adapter = this.owner.lookup("adapter:oidc-json-api-adapter"); 46 | const session = adapter.session; 47 | session.session.content = {}; 48 | session.session.isAuthenticated = true; 49 | session.invalidate = () => { 50 | assert.step("invalidate"); 51 | }; 52 | 53 | adapter.handleResponse(401, {}, {}, {}); 54 | 55 | assert.strictEqual( 56 | adapter.session.data.nextURL, 57 | location.href.replace(location.origin, ""), 58 | ); 59 | 60 | assert.verifySteps(["invalidate"]); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/unit/adapters/oidc-rest-adapter-test.js: -------------------------------------------------------------------------------- 1 | import { set } from "@ember/object"; 2 | import setupMirage from "ember-cli-mirage/test-support/setup-mirage"; 3 | import { setupTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Unit | Adapter | oidc rest adapter", function (hooks) { 7 | setupTest(hooks); 8 | setupMirage(hooks); 9 | 10 | test("it sets the correct headers", async function (assert) { 11 | const adapter = this.owner.lookup("adapter:oidc-rest-adapter"); 12 | const session = this.owner.lookup("service:session"); 13 | set(session, "session.isAuthenticated", true); 14 | session.data.authenticated.access_token = "access.token"; 15 | 16 | assert.deepEqual(adapter.headers, { Authorization: "Bearer access.token" }); 17 | }); 18 | 19 | test("it refreshes the access token before ember-data requests", async function (assert) { 20 | const adapter = this.owner.lookup("adapter:oidc-rest-adapter"); 21 | 22 | adapter.session.refreshAuthentication.perform = () => { 23 | assert.step("refresh"); 24 | }; 25 | 26 | const mockAndTest = async (funcName) => { 27 | adapter[funcName] = () => { 28 | assert.step(funcName); 29 | }; 30 | 31 | await adapter[funcName](); 32 | assert.verifySteps(["refresh", funcName]); 33 | }; 34 | 35 | await mockAndTest("findRecord"); 36 | await mockAndTest("createRecord"); 37 | await mockAndTest("updateRecord"); 38 | await mockAndTest("deleteRecord"); 39 | await mockAndTest("findAll"); 40 | await mockAndTest("query"); 41 | await mockAndTest("findMany"); 42 | }); 43 | 44 | test("it invalidates the session correctly on a 401 response", function (assert) { 45 | const adapter = this.owner.lookup("adapter:oidc-rest-adapter"); 46 | const session = adapter.session; 47 | session.session.content = {}; 48 | session.session.isAuthenticated = true; 49 | session.invalidate = () => { 50 | assert.step("invalidate"); 51 | }; 52 | 53 | adapter.handleResponse(401, {}, {}, {}); 54 | 55 | assert.strictEqual( 56 | adapter.session.data.nextURL, 57 | location.href.replace(location.origin, ""), 58 | ); 59 | 60 | assert.verifySteps(["invalidate"]); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/unit/authenticators/oidc-test.js: -------------------------------------------------------------------------------- 1 | import { set } from "@ember/object"; 2 | import setupMirage from "ember-cli-mirage/test-support/setup-mirage"; 3 | import { setupTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | import { getConfig } from "ember-simple-auth-oidc/config"; 7 | 8 | const getTokenBody = (expired) => { 9 | const time = expired ? -30 : 120; 10 | return btoa( 11 | JSON.stringify({ 12 | exp: Date.now() + time, 13 | }), 14 | ); 15 | }; 16 | 17 | module("Unit | Authenticator | OIDC", function (hooks) { 18 | setupTest(hooks); 19 | setupMirage(hooks); 20 | 21 | test("it can authenticate", async function (assert) { 22 | const subject = this.owner.lookup("authenticator:oidc"); 23 | 24 | set(subject, "redirectUri", "test"); 25 | 26 | const data = await subject.authenticate({ code: "test" }); 27 | 28 | assert.ok(data.access_token, "Returns an access token"); 29 | assert.ok(data.refresh_token, "Returns a refresh token"); 30 | assert.ok(data.userinfo, "Returns the user info"); 31 | assert.ok(data.expireTime, "Returns the time at which the token expires"); 32 | }); 33 | 34 | test("it can restore a session", async function (assert) { 35 | const subject = this.owner.lookup("authenticator:oidc"); 36 | 37 | const data = await subject.restore({ 38 | refresh_token: `refresh.${getTokenBody(false)}.token`, 39 | expireTime: new Date().getTime(), 40 | redirectUri: "test", 41 | }); 42 | 43 | assert.ok(data.access_token, "Returns an access token"); 44 | assert.ok(data.refresh_token, "Returns a refresh token"); 45 | assert.ok(data.userinfo, "Returns the user info"); 46 | assert.ok(data.expireTime, "Returns the time at which the token expires"); 47 | }); 48 | 49 | test("it can invalidate a session", async function (assert) { 50 | const subject = this.owner.lookup("authenticator:oidc"); 51 | 52 | assert.ok(await subject.invalidate()); 53 | }); 54 | 55 | test("it can refresh a session", async function (assert) { 56 | const subject = this.owner.lookup("authenticator:oidc"); 57 | 58 | const data = await subject._refresh("x.y.z"); 59 | 60 | assert.ok(data.access_token, "Returns an access token"); 61 | assert.ok(data.refresh_token, "Returns a refresh token"); 62 | assert.ok(data.userinfo, "Returns the user info"); 63 | assert.ok(data.expireTime, "Returns the time at which the token expires"); 64 | }); 65 | 66 | test("it can make a single logout", async function (assert) { 67 | const { endSessionEndpoint, afterLogoutUri } = getConfig(this.owner); 68 | const subject = this.owner.lookup("authenticator:oidc"); 69 | 70 | subject._redirectToUrl = (url) => { 71 | assert.ok(new RegExp(endSessionEndpoint).test(url)); 72 | assert.ok( 73 | new RegExp(`post_logout_redirect_uri=${afterLogoutUri}`).test(url), 74 | ); 75 | assert.ok(new RegExp("id_token_hint=myIdToken").test(url)); 76 | }; 77 | 78 | subject.singleLogout("myIdToken"); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/unit/routes/oidc-authentication-test.js: -------------------------------------------------------------------------------- 1 | import { set } from "@ember/object"; 2 | import setupMirage from "ember-cli-mirage/test-support/setup-mirage"; 3 | import { setupTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | import { getConfig } from "ember-simple-auth-oidc/config"; 7 | 8 | module("Unit | Route | oidc-authentication", function (hooks) { 9 | setupTest(hooks); 10 | setupMirage(hooks); 11 | 12 | hooks.beforeEach(function () { 13 | this.config = getConfig(this.owner); 14 | }); 15 | 16 | test("it can handle already authenticated requests", function (assert) { 17 | const router = this.owner.lookup("route:oidc-authentication"); 18 | router.urlFor = () => "/test"; 19 | 20 | const route = this.owner.lookup("route:oidc-authentication"); 21 | route.session = this.owner.lookup("service:session"); 22 | route.session.prohibitAuthentication = (route) => { 23 | assert.ok(route.includes("test")); 24 | assert.step("prohibitAuthentication"); 25 | }; 26 | 27 | route.beforeModel({ from: { name: "test" } }); 28 | assert.verifySteps(["prohibitAuthentication"]); 29 | }); 30 | 31 | test("it can handle an unauthenticated request", function (assert) { 32 | const router = this.owner.lookup("service:router"); 33 | router.urlFor = () => "/test"; 34 | 35 | const route = this.owner.lookup("route:oidc-authentication"); 36 | route.session = this.owner.lookup("service:session"); 37 | set(route.session, "data.authenticated", {}); 38 | set(route.session, "attemptedTransition", { to: {} }); 39 | route._redirectToUrl = (url) => { 40 | assert.ok(url.includes(this.config.authEndpoint)); 41 | assert.ok(url.includes(`client_id=${this.config.clientId}`)); 42 | const { protocol, host } = location; 43 | assert.ok(url.includes(`redirect_uri=${protocol}//${host}/test`)); 44 | }; 45 | 46 | route.afterModel(null, { to: { queryParams: {} } }); 47 | }); 48 | 49 | test("it can handle a request with an authentication code", function (assert) { 50 | const routeFactory = this.owner.factoryFor("route:oidc-authentication"); 51 | this.owner.register( 52 | "route:custom-oidc-authentication", 53 | class extends routeFactory.class { 54 | redirectUri = "test"; 55 | session = { 56 | data: { 57 | authenticated: {}, 58 | }, 59 | async authenticate(_, { code }) { 60 | assert.strictEqual(code, "sometestcode"); 61 | }, 62 | set() {}, 63 | }; 64 | }, 65 | ); 66 | const route = this.owner.lookup("route:custom-oidc-authentication"); 67 | 68 | route.afterModel(null, { to: { queryParams: { code: "sometestcode" } } }); 69 | }); 70 | 71 | test("it can handle older version of router_js", function (assert) { 72 | const routeFactory = this.owner.factoryFor("route:oidc-authentication"); 73 | this.owner.register( 74 | "route:custom-oidc-authentication", 75 | class extends routeFactory.class { 76 | redirectUri = "test"; 77 | session = { 78 | data: { 79 | authenticated: {}, 80 | }, 81 | async authenticate(_, { code }) { 82 | assert.strictEqual(code, "sometestcode"); 83 | }, 84 | set() {}, 85 | }; 86 | }, 87 | ); 88 | const route = this.owner.lookup("route:custom-oidc-authentication"); 89 | 90 | route.afterModel(null, { queryParams: { code: "sometestcode" } }); 91 | }); 92 | 93 | test("it can handle a failing authentication", function (assert) { 94 | const routeFactory = this.owner.factoryFor("route:oidc-authentication"); 95 | this.owner.register( 96 | "route:custom-oidc-authentication", 97 | class extends routeFactory.class { 98 | redirectUri = "test"; 99 | session = { 100 | data: { 101 | authenticated: {}, 102 | state: "state2", 103 | }, 104 | async authenticate() { 105 | return true; 106 | }, 107 | }; 108 | }, 109 | ); 110 | const route = this.owner.lookup("route:custom-oidc-authentication"); 111 | 112 | // fails because the state is not correct (CSRF) 113 | route 114 | .afterModel(null, { 115 | to: { 116 | queryParams: { code: "sometestcode", state: "state1" }, 117 | }, 118 | }) 119 | .catch((e) => { 120 | assert.ok(/State did not match/.test(e.message)); 121 | }); 122 | 123 | route.session.authenticate = async () => { 124 | throw new Error(); 125 | }; 126 | 127 | // fails because of the error in authenticate 128 | assert.rejects( 129 | route.afterModel(null, { 130 | to: { 131 | queryParams: { code: "sometestcode", state: "state2" }, 132 | }, 133 | }), 134 | Error, 135 | ); 136 | 137 | route.session.authenticate = async () => { 138 | throw { error: "unauthorized_client" }; 139 | }; 140 | 141 | // fails due to bad request response from authentication server 142 | assert.rejects( 143 | route.afterModel(null, { 144 | to: { 145 | queryParams: { code: "sometestcode", state: "state2" }, 146 | }, 147 | }), 148 | Error, 149 | ); 150 | }); 151 | 152 | test("it forwards customized login_hint param", function (assert) { 153 | const routeFactory = this.owner.factoryFor("route:oidc-authentication"); 154 | this.owner.register( 155 | "route:custom-oidc-authentication", 156 | class extends routeFactory.class { 157 | redirectUri = "test"; 158 | session = { 159 | data: { authenticated: {} }, 160 | set() {}, 161 | attemptedTransition: { to: {} }, 162 | }; 163 | _redirectToUrl(url) { 164 | assert.ok(url.includes(this.config.authEndpoint)); 165 | 166 | assert.ok(url.includes(`client_id=${this.config.clientId}`)); 167 | assert.ok(url.includes("redirect_uri=test")); 168 | assert.ok(url.includes("custom_login_hint=my-idp")); 169 | } 170 | }, 171 | ); 172 | const route = this.owner.lookup("route:custom-oidc-authentication"); 173 | 174 | route.afterModel(null, { 175 | to: { queryParams: { custom_login_hint: "my-idp" } }, 176 | }); 177 | }); 178 | 179 | test("it stores an intercepted transition with query params", function (assert) { 180 | const router = this.owner.lookup("service:router"); 181 | const routeFactory = this.owner.factoryFor("route:oidc-authentication"); 182 | this.owner.register( 183 | "route:custom-oidc-authentication", 184 | class extends routeFactory.class { 185 | router = router; 186 | redirectUri = "test"; 187 | session = { 188 | data: { authenticated: {} }, 189 | attemptedTransition: { 190 | intent: { 191 | url: "/protected/users?param0=value0¶m1=value1", 192 | }, 193 | }, 194 | set(key, value) { 195 | set(this, key, value); 196 | }, 197 | }; 198 | _redirectToUrl() { 199 | assert.strictEqual( 200 | this.session.data.nextURL, 201 | "/protected/users?param0=value0¶m1=value1", 202 | ); 203 | } 204 | }, 205 | ); 206 | const route = this.owner.lookup("route:custom-oidc-authentication"); 207 | 208 | route.afterModel(null, { to: { queryParams: {} } }); 209 | }); 210 | }); 211 | -------------------------------------------------------------------------------- /tests/unit/services/session-test.js: -------------------------------------------------------------------------------- 1 | import { set } from "@ember/object"; 2 | import setupMirage from "ember-cli-mirage/test-support/setup-mirage"; 3 | import { setupTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Unit | Service | session", function (hooks) { 7 | setupTest(hooks); 8 | setupMirage(hooks); 9 | 10 | test("it can trigger a single logout", async function (assert) { 11 | const authenticator = this.owner.lookup("authenticator:oidc"); 12 | const session = this.owner.lookup("service:session"); 13 | set(session, "session.authenticator", "authenticator:oidc"); 14 | set(session, "data.authenticated.id_token", "x.y.z"); 15 | 16 | authenticator.singleLogout = () => { 17 | assert.step("singleLogout"); 18 | }; 19 | session.invalidate = () => { 20 | assert.step("invalidate"); 21 | }; 22 | 23 | await session.singleLogout(); 24 | assert.verifySteps(["invalidate", "singleLogout"]); 25 | }); 26 | 27 | test("it can trigger an authentication refresh", async function (assert) { 28 | const authenticator = this.owner.lookup("authenticator:oidc"); 29 | authenticator._refresh = () => { 30 | assert.step("refresh"); 31 | }; 32 | 33 | const session = this.owner.lookup("service:session"); 34 | session.session.isAuthenticated = true; 35 | session.session.content = { authenticated: { expireTime: 1 } }; 36 | 37 | await session.refreshAuthentication.perform(); 38 | assert.verifySteps(["refresh"]); 39 | assert.notStrictEqual( 40 | session.data.authenticated.expireTime, 41 | 1, 42 | "Updates expiry time", 43 | ); 44 | 45 | set(session, "data.authenticated.expireTime", new Date().getTime() + 10000); 46 | await session.refreshAuthentication.perform(); 47 | assert.verifySteps([]); 48 | }); 49 | 50 | test("it can compute authentication headers", function (assert) { 51 | const session = this.owner.lookup("service:session"); 52 | const internalSession = session.session; 53 | internalSession.isAuthenticated = true; 54 | internalSession.content = { 55 | authenticated: { access_token: "SOMESECRETTOKEN" }, 56 | }; 57 | 58 | assert.deepEqual(session.headers, { 59 | Authorization: "Bearer SOMESECRETTOKEN", 60 | }); 61 | 62 | set(internalSession, "isAuthenticated", false); 63 | 64 | assert.deepEqual(session.headers, {}); 65 | }); 66 | 67 | test("it continues a stored transition", function (assert) { 68 | const session = this.owner.lookup("service:session"); 69 | set(session, "data.nextURL", "/protected/secret"); 70 | 71 | const router = this.owner.lookup("service:router"); 72 | router.replaceWith = (url) => { 73 | assert.strictEqual(url, "/protected/secret"); 74 | }; 75 | 76 | session.handleAuthentication(); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/unit/utils/absolute-url-test.js: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | 4 | import getAbsoluteUrl from "ember-simple-auth-oidc/utils/absolute-url"; 5 | 6 | module("Unit | Utils | absoluteUrl", function (hooks) { 7 | setupTest(hooks); 8 | 9 | test("it transforms a relative url to an absolute one", function (assert) { 10 | const url = "/login"; 11 | const host = "https://myTestHost"; 12 | 13 | assert.strictEqual( 14 | getAbsoluteUrl(url), 15 | `${location.protocol}//${location.host}/login`, 16 | ); 17 | 18 | assert.strictEqual(getAbsoluteUrl(url, host), `${host}/login`); 19 | }); 20 | 21 | test("it does not transform an absolute url", function (assert) { 22 | const url = "http://myTestHost/login"; 23 | assert.strictEqual(getAbsoluteUrl(url), url); 24 | 25 | const urlSSL = "https://myTestHost/login"; 26 | assert.strictEqual(getAbsoluteUrl(urlSSL), urlSSL); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/unit/utils/apollo-test.js: -------------------------------------------------------------------------------- 1 | import setupMirage from "ember-cli-mirage/test-support/setup-mirage"; 2 | import { setupTest } from "ember-qunit"; 3 | import { authenticateSession } from "ember-simple-auth/test-support"; 4 | import { gql } from "graphql-tag"; 5 | import { module, test } from "qunit"; 6 | 7 | module("Unit | apollo", function (hooks) { 8 | setupTest(hooks); 9 | setupMirage(hooks); 10 | 11 | hooks.beforeEach(async function () { 12 | this.apollo = this.owner.lookup("service:apollo"); 13 | await authenticateSession({ access_token: "test" }); 14 | }); 15 | 16 | test("it handles authorization", async function (assert) { 17 | this.server.post( 18 | "/graphql", 19 | (_, request) => { 20 | assert.strictEqual(request.requestHeaders.authorization, "Bearer test"); 21 | return { data: { foo: 1 } }; 22 | }, 23 | 200, 24 | ); 25 | 26 | const response = await this.apollo.query({ 27 | query: gql` 28 | query { 29 | foo 30 | } 31 | `, 32 | }); 33 | 34 | assert.strictEqual(response.foo, 1); 35 | }); 36 | 37 | test("it handles 401 errors", async function (assert) { 38 | this.server.post("/graphql", {}, 401); 39 | 40 | try { 41 | await this.apollo.query({ 42 | query: gql` 43 | query { 44 | foo 45 | } 46 | `, 47 | }); 48 | } catch { 49 | assert.false(this.owner.lookup("service:session").isAuthenticated); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/unit/utils/pkce-test.js: -------------------------------------------------------------------------------- 1 | import base64 from "base64-js"; 2 | import { sha256 } from "js-sha256"; 3 | import { module, test } from "qunit"; 4 | 5 | import { 6 | generatePkceChallenge, 7 | generateCodeVerifier, 8 | } from "ember-simple-auth-oidc/utils/pkce"; 9 | 10 | module("Unit | Utility | pkce", function () { 11 | test("it generates pkce code challenge correctly", function (assert) { 12 | const codeVerifier = generateCodeVerifier(); 13 | const codeChallenge = generatePkceChallenge(codeVerifier); 14 | assert.deepEqual( 15 | codeChallenge, 16 | base64 17 | .fromByteArray(new Uint8Array(sha256.arrayBuffer(codeVerifier))) 18 | .replace(/\+/g, "-") 19 | .replace(/\//g, "_") 20 | .replace(/=+$/, ""), 21 | ); 22 | }); 23 | 24 | test("it generates the pkce code verifier", function (assert) { 25 | const codeVerifier = generateCodeVerifier(96); 26 | assert.deepEqual(codeVerifier.length, 96); 27 | }); 28 | }); 29 | --------------------------------------------------------------------------------