├── addon ├── .gitkeep └── services │ └── ember-oauth2.js ├── tests ├── helpers │ └── .gitkeep ├── unit │ ├── .gitkeep │ └── ember-oauth-test.js ├── integration │ └── .gitkeep ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── templates │ │ │ └── application.hbs │ │ ├── router.js │ │ ├── app.js │ │ └── index.html │ ├── public │ │ └── robots.txt │ └── config │ │ ├── optional-features.json │ │ ├── ember-cli-update.json │ │ ├── targets.js │ │ └── environment.js ├── test-helper.js └── index.html ├── .watchmanconfig ├── public ├── robots.txt └── crossdomain.xml ├── .prettierrc.js ├── app └── services │ └── ember-oauth2.js ├── .template-lintrc.js ├── index.js ├── config ├── environment.js └── ember-try.js ├── .ember-cli ├── .prettierignore ├── .eslintignore ├── yuidoc.json ├── .gitignore ├── .npmignore ├── testem.js ├── .editorconfig ├── CONTRIBUTING.md ├── ember-cli-build.js ├── LICENSE.md ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── package.json ├── CHANGELOG.md └── README.md /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/services/ember-oauth2.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-oauth2/services/ember-oauth2'; 2 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name, 5 | }; 6 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Dummy"}} 2 | 3 |

Welcome to Ember

4 | 5 | {{outlet}} -------------------------------------------------------------------------------- /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/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 | Router.map(function () {}); 10 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /package.json.ember-try 23 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /yuidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ember.OAuth2", 3 | "description": "OAuth2 library for Emberjs that stores tokens in the browsers localStorage", 4 | "version": "2.0.5-beta", 5 | "url": "https://github.com/amkirwan/ember-oauth2", 6 | "options": { 7 | "exclude": "node_modules,bower_components,scripts,tmp,vendor", 8 | "paths": "./addon/services/ember-oauth2.js", 9 | "outdir": "./doc" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /package.json.ember-try 27 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.28.6", 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": [ 14 | "--yarn", 15 | "--no-welcome" 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.gitignore 18 | /.prettierignore 19 | /.prettierrc.js 20 | /.template-lintrc.js 21 | /.travis.yml 22 | /.watchmanconfig 23 | /bower.json 24 | /config/ember-try.js 25 | /CONTRIBUTING.md 26 | /ember-cli-build.js 27 | /testem.js 28 | /tests/ 29 | /yarn-error.log 30 | /yarn.lock 31 | .gitkeep 32 | 33 | # ember-try 34 | /.node_modules.ember-try/ 35 | /bower.json.ember-try 36 | /package.json.ember-try 37 | -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /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: ['Chrome'], 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 | -------------------------------------------------------------------------------- /.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 | [*.js] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.hbs] 20 | insert_final_newline = false 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.css] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | [*.html] 29 | indent_style = space 30 | indent_size = 2 31 | 32 | [*.{diff,md}] 33 | trim_trailing_whitespace = false 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-oauth2` 7 | * `yarn install` 8 | 9 | ## Linting 10 | 11 | * `yarn lint` 12 | * `yarn lint:fix` 13 | 14 | ## Running tests 15 | 16 | * `ember test` – Runs the test suite on the current Ember version 17 | * `ember test --server` – Runs the test suite in "watch mode" 18 | * `ember try:each` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the dummy application 21 | 22 | * `ember serve` 23 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 26 | -------------------------------------------------------------------------------- /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 | // Ember's browser support policy is changing, and IE11 support will end in 10 | // v4.0 onwards. 11 | // 12 | // See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy 13 | // 14 | // If you need IE11 support on a version of Ember that still offers support 15 | // for it, uncomment the code block below. 16 | // 17 | // const isCI = Boolean(process.env.CI); 18 | // const isProduction = process.env.EMBER_ENV === 'production'; 19 | // 20 | // if (isCI || isProduction) { 21 | // browsers.push('ie 11'); 22 | // } 23 | 24 | module.exports = { 25 | browsers, 26 | }; 27 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /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 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | // Use `app.import` to add additional libraries to the generated 11 | // output files. 12 | // 13 | // If you need to use different assets in different 14 | // environments, specify an object as the first parameter. That 15 | // object's keys should be the environment name and the values 16 | // should be the asset to use in that environment. 17 | // 18 | // If the library that you are including contains AMD or ES6 19 | // modules that you would like to import into your application 20 | // please specify an object with the list of modules as keys 21 | // along with the exports of each module as its value. 22 | 23 | const { maybeEmbroider } = require('@embroider/test-setup'); 24 | return maybeEmbroider(app, { 25 | skipBabel: [ 26 | { 27 | package: 'qunit', 28 | }, 29 | ], 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | rules: {}, 23 | overrides: [ 24 | // node files 25 | { 26 | files: [ 27 | './.eslintrc.js', 28 | './.prettierrc.js', 29 | './.template-lintrc.js', 30 | './ember-cli-build.js', 31 | './index.js', 32 | './testem.js', 33 | './blueprints/*/index.js', 34 | './config/**/*.js', 35 | './tests/dummy/config/**/*.js', 36 | ], 37 | parserOptions: { 38 | sourceType: 'script', 39 | }, 40 | env: { 41 | browser: false, 42 | node: true, 43 | }, 44 | plugins: ['node'], 45 | extends: ['plugin:node/recommended'], 46 | }, 47 | { 48 | // Test files: 49 | files: ['tests/**/*-test.{js,ts}'], 50 | extends: ['plugin:qunit/recommended'], 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false, 17 | }, 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /.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@v3 22 | - name: Install Node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 18 26 | cache: yarn 27 | - name: Install Dependencies 28 | run: yarn install --frozen-lockfile 29 | - name: Lint 30 | run: yarn lint 31 | - name: Run Tests 32 | run: yarn test:ember 33 | 34 | floating: 35 | name: 'Floating Dependencies' 36 | runs-on: ubuntu-latest 37 | timeout-minutes: 10 38 | 39 | steps: 40 | - uses: actions/checkout@v3 41 | - uses: actions/setup-node@v3 42 | with: 43 | node-version: 18 44 | cache: yarn 45 | - name: Install Dependencies 46 | run: yarn install --no-lockfile 47 | - name: Run Tests 48 | run: yarn test:ember 49 | 50 | try-scenarios: 51 | name: ${{ matrix.try-scenario }} 52 | runs-on: ubuntu-latest 53 | needs: 'test' 54 | timeout-minutes: 10 55 | 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | try-scenario: 60 | - ember-lts-3.24 61 | - ember-lts-3.28 62 | - ember-release 63 | - ember-beta 64 | - ember-canary 65 | - embroider-safe 66 | - embroider-optimized 67 | 68 | steps: 69 | - uses: actions/checkout@v3 70 | - name: Install Node 71 | uses: actions/setup-node@v3 72 | with: 73 | node-version: 18 74 | cache: yarn 75 | - name: Install Dependencies 76 | run: yarn install --frozen-lockfile 77 | - name: Run Tests 78 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} 79 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | useYarn: true, 9 | scenarios: [ 10 | { 11 | name: 'ember-lts-3.24', 12 | npm: { 13 | devDependencies: { 14 | 'ember-source': '~3.24.3', 15 | }, 16 | }, 17 | }, 18 | { 19 | name: 'ember-lts-3.28', 20 | npm: { 21 | devDependencies: { 22 | 'ember-source': '~3.28.0', 23 | }, 24 | }, 25 | }, 26 | { 27 | name: 'ember-release', 28 | npm: { 29 | devDependencies: { 30 | 'ember-source': await getChannelURL('release'), 31 | }, 32 | }, 33 | }, 34 | { 35 | name: 'ember-beta', 36 | npm: { 37 | devDependencies: { 38 | 'ember-source': await getChannelURL('beta'), 39 | }, 40 | }, 41 | }, 42 | { 43 | name: 'ember-canary', 44 | npm: { 45 | devDependencies: { 46 | 'ember-source': await getChannelURL('canary'), 47 | }, 48 | }, 49 | }, 50 | { 51 | name: 'ember-default-with-jquery', 52 | env: { 53 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 54 | 'jquery-integration': true, 55 | }), 56 | }, 57 | npm: { 58 | devDependencies: { 59 | '@ember/jquery': '^1.1.0', 60 | }, 61 | }, 62 | }, 63 | { 64 | name: 'ember-classic', 65 | env: { 66 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 67 | 'application-template-wrapper': true, 68 | 'default-async-observers': false, 69 | 'template-only-glimmer-components': false, 70 | }), 71 | }, 72 | npm: { 73 | devDependencies: { 74 | 'ember-source': '~3.28.0', 75 | }, 76 | ember: { 77 | edition: 'classic', 78 | }, 79 | }, 80 | }, 81 | embroiderSafe(), 82 | embroiderOptimized(), 83 | ], 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-oauth2", 3 | "version": "2.0.5-beta", 4 | "description": "OAuth2 library for Emberjs that stores tokens in the browsers localStorage", 5 | "keywords": [ 6 | "ember-addon", 7 | "ember-oauth2", 8 | "oauth2", 9 | "oauth2.0", 10 | "ember", 11 | "ember.js", 12 | "emberjs" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/amkirwan/ember-oauth2.git" 17 | }, 18 | "license": "MIT", 19 | "author": "Anthony Kirwan", 20 | "directories": { 21 | "doc": "doc", 22 | "test": "tests" 23 | }, 24 | "scripts": { 25 | "build": "ember build --environment=production", 26 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 27 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 28 | "lint:hbs": "ember-template-lint .", 29 | "lint:hbs:fix": "ember-template-lint . --fix", 30 | "lint:js": "eslint . --cache", 31 | "lint:js:fix": "eslint . --fix", 32 | "start": "ember serve", 33 | "test": "npm-run-all lint test:*", 34 | "test:ember": "ember test", 35 | "test:ember-compatibility": "ember try:each" 36 | }, 37 | "dependencies": { 38 | "ember-cli-babel": "^7.26.10" 39 | }, 40 | "devDependencies": { 41 | "@ember/optional-features": "^2.0.0", 42 | "@ember/string": "^3.1.1", 43 | "@ember/test-helpers": "^2.6.0", 44 | "@embroider/test-setup": "^0.48.1", 45 | "@glimmer/component": "^1.0.4", 46 | "@glimmer/tracking": "^1.0.4", 47 | "babel-eslint": "^10.1.0", 48 | "broccoli-asset-rev": "^3.0.0", 49 | "ember-auto-import": "^2.7.2", 50 | "ember-cli": "~3.28.6", 51 | "ember-cli-dependency-checker": "^3.2.0", 52 | "ember-cli-htmlbars": "^5.7.2", 53 | "ember-cli-inject-live-reload": "^2.1.0", 54 | "ember-cli-sri": "^2.1.1", 55 | "ember-cli-terser": "^4.0.2", 56 | "ember-load-initializers": "^2.1.2", 57 | "ember-maybe-import-regenerator": "^0.1.6", 58 | "ember-page-title": "^6.2.2", 59 | "ember-qunit": "^5.1.5", 60 | "ember-resolver": "^8.0.3", 61 | "ember-sinon": "^2.2.0", 62 | "ember-source": "~3.28.8", 63 | "ember-source-channel-url": "^3.0.0", 64 | "ember-template-lint": "^3.15.0", 65 | "ember-try": "^1.4.0", 66 | "eslint": "^7.32.0", 67 | "eslint-config-prettier": "^8.3.0", 68 | "eslint-plugin-ember": "^10.5.8", 69 | "eslint-plugin-node": "^11.1.0", 70 | "eslint-plugin-prettier": "^3.4.1", 71 | "eslint-plugin-qunit": "^6.2.0", 72 | "loader.js": "^4.7.0", 73 | "npm-run-all": "^4.1.5", 74 | "prettier": "^2.5.1", 75 | "qunit": "^2.17.2", 76 | "qunit-dom": "^1.6.0", 77 | "webpack": "^5.89.0" 78 | }, 79 | "engines": { 80 | "node": "12.* || 14.* || >= 16" 81 | }, 82 | "homepage": "https://github.com/amkirwan/ember-ouath2", 83 | "ember": { 84 | "edition": "octane" 85 | }, 86 | "ember-addon": { 87 | "configPath": "tests/dummy/config" 88 | }, 89 | "bugs": { 90 | "url": "https://github.com/amkirwan/ember-oauth2/issues" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog Ember-OAuth2 2 | 3 | ## v2.0.5-beta 4 | (Full Changelog)[https://github.com/amkirwan/ember-oauth2/compare/v2.0.4-beta...v2.0.5-beta] 5 | - Update ember-cli 6 | - Use ES6 7 | - Update Tests 8 | - cleanup 9 | 10 | ## v2.0.4-beta 11 | (Full Changelog)[https://github.com/amkirwan/ember-oauth2/compare/v2.0.3-beta...v2.0.4-beta] 12 | - Move ember-cli-babel to dev dependencies 13 | 14 | ## v2.0.3-beta 15 | (Full Changelog)[https://github.com/amkirwan/ember-oauth2/compare/v2.0.2-beta...v2.0.3-beta] 16 | - fix typo in scope 17 | 18 | ## v2.0.2-beta 19 | (Full Changelog)[https://github.com/amkirwan/ember-oauth2/compare/v2.0.1-beta...v2.0.2-beta] 20 | 21 | - add setProvider method 22 | - change init to not take providerId 23 | 24 | ## v2.0.1-beta 25 | (Full Changelog)[https://github.com/amkirwan/ember-oauth2/compare/v2.0.0-beta...v2.0.1-beta] 26 | 27 | - Update NPM author info 28 | 29 | ## v2.0.0-beta 30 | (Full Changelog)[https://github.com/amkirwan/ember-oauth2/compare/v1.1.0...v2.0.0-beta] 31 | 32 | - Converted to EmberAddon and turned EmberOAuth2 into a service 33 | - Updated testing to use Ember-Qunit 34 | - Update project README 35 | 36 | ## v1.1.0 37 | - Add verifyToken method to handle mitigation of the confused deputy 38 | - fix bug with checking of state 39 | 40 | ## v1.0.1 41 | - Update getState function to not take a param argument and use the configure statePrefix to find the state from localStorage. 42 | - This makes getState and getToken perform the same way and avoids confusion with the api 43 | - Fix bug where handleRedircect could not find the stateObj 44 | 45 | ## v1.0.0 46 | - Drop support for deprecated callback methods onSuccess, onError, and onRedirect. Callbacks can be called using Ember.Evented trigger with the targets success, error, and redirect. 47 | ## v0.7.0 48 | - Add support Authorization Grant Flow 49 | ## v0.6.0 50 | - Remove global distribution 51 | - Bump project bower and node dependencies 52 | - Bump Ember to ~0.13.0 53 | 54 | ## v0.5.5 55 | - Provide funtion to remove the token from localstorage 56 | - Provide funtion to remove the state from localstorage 57 | - Funtion to get the stateKeyName and tokenKeyName 58 | 59 | ## v0.5.4 60 | - Added support for versions of Ember >= 1.7 61 | ## v0.5.3 62 | - Can now use window.EmberENV for config along with window.ENV. 63 | - Fixed issue where mini files had wrong version header. 64 | - Better error reporting when configuration file is not formatted correctly. 65 | ## v0.5.2 66 | - Update Ember dependeny to 1.7 67 | - Moved bower install dependences from vendor to bower_components 68 | - Add dist dir to repo for bower installs 69 | ## v0.5.1 70 | - Fixed bug where the random UUID for the state was not being set. 71 | - openWindow resolves with a reference to the dialog window and rejects 72 | with an error if the dialog window fails to open. 73 | ## v0.5.0 74 | - Update library to use ES6 module 75 | - Export to both Global and AMD module 76 | - Use window.ENV['ember-oauth2'] for config 77 | - OpenWindow now returns reference to dialog on resolve, on reject object with reference to dialog and error. 78 | ## v0.4.0 79 | - Isolates function for opening window so that it can be overridden 80 | - Login dialog window returns a promise, it resolves on success and reject on error 81 | ## v0.3.2 82 | - Remove files not needed in package. 83 | - Add build files to dist dir for package manaagers. 84 | ## v0.3.1 85 | - Initial relase to Bower 86 | ## v0.3.0 87 | - Added YUIDoc to src. 88 | - Published to NPM. 89 | - Using get and set for properties on the instances. 90 | ## v0.2.4 91 | - Change to using Ember.Evented class for handling 'redirct', 'success' and 'error' callback. 92 | - Depricated old callback methods. 93 | ## v0.2.3 94 | - Using grunt-versioner for updating and building project 95 | - Fixed incorrect path to jQuery when build from source 96 | ## v0.2.2 (Jan 11, 2014) 97 | - [a132c65](https://github.com/amkirwan/ember-oauth2/commit/a132c657ae0a5173fc78ab192c6db11e4074232c) updated patch version to v0.2.2 98 | - [ffd5069](https://github.com/amkirwan/ember-oauth2/commit/ffd50691721e96091e3642c1ecc871d66c2f48f8) grunt-bump commit changes 99 | - [a051d44](https://github.com/amkirwan/ember-oauth2/commit/a051d44a15c3e27fbcafe07e5fee43695e4fd68c) config bump, added custom task to update readme version 100 | - [aabf4e0](https://github.com/amkirwan/ember-oauth2/commit/aabf4e055d1cec84a904033fdb4889283544f32d) added grunt bump 101 | 102 | ## v0.2.1 (Jan 9, 2014) 103 | - [b925f2e](https://github.com/amkirwan/ember-oauth2/commit/b925f2ea303930785227c424ecba5f7c772275a8) version bump to v0.2.1 added dist dir for bower and node 104 | - [0d5e7ed](https://github.com/amkirwan/ember-oauth2/commit/0d5e7eddfe5483853476def213caa999354a09dc) do not ignore dist dir 105 | - [ea17257](https://github.com/amkirwan/ember-oauth2/commit/ea172578b6dbb6ab13b22583de3368fd7a5aae95) moved ember to dependencies 106 | - [7d5e752](https://github.com/amkirwan/ember-oauth2/commit/7d5e75227aa304c77385f24ff4e66408c58d1498) Update README.md 107 | 108 | ## v0.2.0 (Jan 9, 2014) 109 | - [9769bf1](https://github.com/amkirwan/ember-oauth2/commit/9769bf1daae3c9035b03a27b7ceabda4e53b6874) version bump v0.2.0 110 | - [1305ae8](https://github.com/amkirwan/ember-oauth2/commit/1305ae8504eff1961e4c09d10611e9ce2dbdf4a2) updated Gruntfile to new directory layout structure 111 | - [c00930f](https://github.com/amkirwan/ember-oauth2/commit/c00930f58ed8e384071320a04b1fb87091b1a041) moved lib and spec to src 112 | - [114d85d](https://github.com/amkirwan/ember-oauth2/commit/114d85d729bb28056d624b2df94cc88013ec3973) renamed packages dir to src 113 | 114 | ## v.0.1.0 (Jan 7, 2014) 115 | - [8914512](https://github.com/amkirwan/ember-oauth2/commit/8914512d4cffb9c0de8f1a2455948569846d291a) version bump 116 | - [a3cb3e2](https://github.com/amkirwan/ember-oauth2/commit/a3cb3e289a9fda3930e6f90c103aa010af41a1c0) fixed jshint warnings 117 | - [b2c4aea](https://github.com/amkirwan/ember-oauth2/commit/b2c4aea31e4f80153d7a7217a50fd1a1caf244ee) added missing var 118 | - [f1befdf](https://github.com/amkirwan/ember-oauth2/commit/f1befdfb5040c757090e48826bbf0c936dc34b5c) removed dist directory 119 | - [ba25b1e](https://github.com/amkirwan/ember-oauth2/commit/ba25b1e79f9eae2d3e992633189821392987bf7b) remove dist js files 120 | - [8de0dc3](https://github.com/amkirwan/ember-oauth2/commit/8de0dc3f6cb2d93a0a16752e9306b18e998a906b) adjusted readme.md 121 | - [c4e5829](https://github.com/amkirwan/ember-oauth2/commit/c4e58292a43190fd6e9b7af7d1b3bde900e7776f) travis should run tests using grunt task 122 | - [17eb7f7](https://github.com/amkirwan/ember-oauth2/commit/17eb7f71a275c94b3053d334f77004fbf6ef03b4) fixed ember-oauth2.png image 123 | 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/amkirwan/ember-oauth2/actions/workflows/ci.yml/badge.svg)](https://github.com/amkirwan/ember-oauth2/actions/workflows/ci.yml) 2 | 3 | ember-oauth2 4 | ============ 5 | 6 | JavaScript library for using OAuth 2.0 Implicit Grant flow (Client-Side Flow) or Authorization Grant flow (Server-Side Flow) for Ember.js 7 | 8 | This creates an OAuth 2.0 Ember object class for handling authentication with OAuth 2.0 providers. 9 | 10 | Current Version: **[2.0.5-beta](https://github.com/amkirwan/ember-oauth2/releases/tag/v2.0.5-beta)** 11 | 12 | The EmberCli addon [EmberTokenAuth](https://github.com/amkirwan/ember-token-auth) demonstrates how to use Ember-OAuth2 library for authentication. 13 | 14 | ## Dependencies 15 | 16 | Ember-OAuth2 requires Ember and jQuery. 17 | 18 | 19 | ## Browser Support 20 | 21 | Ember-OAuth2 uses localStorage for saving the tokens, localStorage is supported in Firefox 3.5+, Safari 4+, IE9+, and Chrome. 22 | 23 | The latest version of Ember-OAuth2 is an Ember Addon and uses the ES6 modules. This allows Ember-OAuth2 to be used in projects like [EmberCLI](https://github.com/stefanpenner/ember-cli) easier. 24 | 25 | 26 | ## Installation 27 | 28 | Ember-OAuth2 is an Ember Addon that can be installed with the following command from your ember project. 29 | 30 | ```bash 31 | $ ember install ember-oauth2 32 | ``` 33 | 34 | Ember-OAuth2 is an Ember [service](https://guides.emberjs.com/v2.8.0/applications/services/) that you can inject to different parts of your app using the inject syntax 35 | 36 | ```js 37 | import Controller from '@ember/controller'; 38 | import { inject as service } from '@ember/service'; 39 | 40 | export default class ApplicationController extends Controller { 41 | @service emberOauth2; 42 | } 43 | ``` 44 | 45 | 46 | ## Configure 47 | 48 | First you must configure your OAuth provider. For Google you would configure it like this. 49 | 50 | ```js 51 | window.EmberENV['ember-oauth2'] = { 52 | google: { 53 | clientId: 'xxxxxxxxxxxx', 54 | authBaseUri: 'https://accounts.google.com/o/oauth2/auth', 55 | redirectUri: 'https://oauth2-login-demo.appspot.com/oauth/callback', 56 | scope: 'public write' 57 | } 58 | } 59 | ``` 60 | 61 | If using ember-cli, you can add the configuration to `config/environment.js`: 62 | 63 | ```js 64 | EmberENV: { 65 | FEATURES: { 66 | // Here you can enable experimental features on an ember canary build 67 | // e.g. 'with-controller': true 68 | }, 69 | 'ember-oauth2': { 70 | google: { 71 | clientId: 'xxxxxxxxxxxx', 72 | authBaseUri: 'https://accounts.google.com/o/oauth2/auth', 73 | redirectUri: 'https://oauth2-login-demo.appspot.com/oauth/callback', 74 | scope: 'public write' 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | The example above sets *google* as a *providerId* along with configuration information for the provider. The following params are required for configuring a valid provider *clientId*, *authBaseUri* and *redirectUri*. Depending on the provider you might need to provide additional and/or optional configuration key/values. 81 | 82 | The configuration object allows you to also customize the prefix for the state and token that are stored in the browsers localStorage. The default value for the state prefix is *state* and the default for token is *token*. Using the previous example you can customize the prefixes by doing the following. 83 | 84 | ```js 85 | window.ENV['ember-oauth2'] = { 86 | google: { 87 | clientId: 'xxxxxxxxxxxx', 88 | authBaseUri: 'https://accounts.google.com/o/oauth2/auth', 89 | redirectUri: 'https://oauth2-login-demo.appspot.com/oauth/callback', 90 | scope: 'public write', 91 | statePrefix: 'foobar', 92 | tokenPrefix: 'qux' 93 | } 94 | } 95 | ``` 96 | 97 | The following are the options available for configuring a provider: 98 | 99 | * `clientId`: (required) The client identifier that is used by the provider. Ember-OAuth2 uses the Implicit Grant flow (Client-Side Flow). 100 | * `authBaseUri`: (required) The authorization url for the OAuth2 provider. 101 | * `redirectUri`: (required) The URI that the OAuth2 provider will redirect back to when completed. 102 | * `scope`: Access your application is requesting from the OAuth2 provider. 103 | * `responseType`: The type of authorization your application is requesting. The default is `token` but can be set to `code` if using the Authorization Grant flow. 104 | * `statePrefix`: The prefix name for state key stored in the localStorage. The default value is `state` and the key would be `state-the_state_number` 105 | * `tokenPrefix`: The prefix name for token key stored in the localStorage. The default value is `token` and the key would be `token-the_provider_id` 106 | 107 | 108 | 109 | ## Authorization 110 | 111 | To sign into the OAuth2 provider create by injecting the service, set the provider with `setProvider` and call the `authorize`. You can inject this addon into your controller for example and when the user clicks a button fire the action to handle the request and set the service providerId and call authorize. This is a simple example and you would probably want to wrap this functionality in a session model. Checkout [ember-token-auth](https://github.com/amkirwan/ember-token-auth) for a full example. 112 | 113 | ```js 114 | // login route 115 | import Controller from '@ember/controller'; 116 | import { inject as service } from '@ember/service'; 117 | import { action } from '@ember/object'; 118 | 119 | export default class LoginController extends Controller { 120 | @service emberOauth2; 121 | 122 | @action 123 | async authenticate(providerId) { 124 | this.emberOauth2.setProvider(providerId); 125 | 126 | try { 127 | let response = await this.emberOauth2.authorize(); 128 | this.emberOauth2.trigger('redirect', response.location.hash); 129 | } catch (error) { 130 | this.emberOauth2.trigger('error', error); 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | Calling `authorize()` will open a new window and the OAuth provider's OAuth dialog will be displayed. If the user chooses to authenticate with your website upon authorization by OAuth provider the user will be redirected back to the redirectUri with the params access_token, token_type and state. 137 | 138 | At the redirectURI add the following to process the params returned from the OAuth provider 139 | 140 | ```html 141 | 142 | 143 | 144 | Authorize 145 | 150 | 151 | 152 | ``` 153 | 154 | 155 | 156 | 157 | ## Implicit Grant Flow (Client-side flow) 158 | 159 | This will process the returned params and save the `provider_id`, `access_token`, `scope` and `expires_in` (the time the access_token will expire) to the localStorage. This localStorage can be accessed with the key `token-the_provider_id`. 160 | 161 | 162 | After successful authorization and saving the access_token to the localStorage the `success` event will be called. This will allow the user to do any cleanup necessary or to retrieve user information from the OAuth provider. To configure the callback bind event handlers to the `success` and `error` events. 163 | 164 | The `authorize` call returns a `Ember.RSVP.Promise`. Authorize will `resolve` with a reference to the dialog window when it opens successfully and `rejects` with an error when the window fails to open. 165 | 166 | }) 167 | 168 | When using the client-side flow it is vital to validate the token received from the endpoint, failure to do so will make your application vulnerable to the [confused deputy problem](https://en.wikipedia.org/wiki/Confused_deputy_problem). As of version `v1.0.2` Ember-OAuth2 supports the `verifyToken` method for validating tokens when using the client-side flow. The user will need to override this method for validating the different server endpoints. 169 | 170 | Here is an example of how this might be accomplished in an Ember-CLI instance initializer using the Google token validation endpoint. 171 | 172 | ```js 173 | import Ember from 'ember'; 174 | import EmberOAuth2 from 'ember-oauth2'; 175 | import env from 'ember-pacu/config/environment'; 176 | 177 | export function initialize(app) { 178 | verifyTokenInit(app); 179 | } 180 | 181 | function verifyTokenInit(app) { 182 | EmberOAuth2.reopen({ 183 | // mitigate confused deputy 184 | verifyToken: function() { 185 | return new Ember.RSVP.Promise((resolve, reject) => { 186 | // implement the adapter with the url to the google tokeinfo endpoint 187 | var adapter = app.lookup('adapter:session'); 188 | adapter.google_tokeninfo().then(function(response) { 189 | if (response.audience === env.APP.GOOGLE_CLIENT_ID) { 190 | resolve(response); 191 | } else { 192 | reject('app uid does not match'); 193 | } 194 | }, function(error) { 195 | reject(error); 196 | }); 197 | }); 198 | } 199 | }); 200 | } 201 | 202 | export default { 203 | name: 'ember-oauth2', 204 | initialize: initialize 205 | }; 206 | ``` 207 | 208 | 209 | 210 | 211 | 212 | ## Authorization Grant flow 213 | 214 | If using the Authorization Grant flow with your provider your backend server will need to handle the final steps of authorizing your application. Your success handler will need to send the `AUTHORIZATON_CODE` returned from OAuth2 provider to your backend server which can then retrieve an access token using the client_id, client_secret, and authorization_code. 215 | 216 | To enable the Authorization Grant flow for a provider set the `responseType` value to `code`. 217 | 218 | ```js 219 | window.ENV = window.ENV || {}; 220 | window.ENV['ember-oauth2'] = { 221 | google: { 222 | clientId: 'xxxxxxxxxxxx', 223 | authBaseUri: 'https://accounts.google.com/o/oauth2/auth', 224 | redirectUri: 'https://oauth2-login-demo.appspot.com/oauth/callback', 225 | responseType: 'code' 226 | } 227 | } 228 | ``` 229 | 230 | To build Ember.Oauth2 on your system you will need to have [Node.js](http://nodejs.org), and [npm](https://npmjs.org) installed. 231 | 232 | ```bash 233 | $ git clone https://github.com/amkirwan/ember-oauth2 234 | $ cd ember-oauth2 235 | $ npm install 236 | $ bower install 237 | ``` 238 | 239 | ## Tests 240 | 241 | To run the tests you can run one of the following commands. 242 | 243 | ```bash 244 | $ ember test 245 | $ ember test --serve 246 | $ npm test 247 | ``` 248 | 249 | ## Building API Docs 250 | 251 | The API Docs provide a detailed collection of methods and properties for Ember.OAuth2. To build the documentation for the project from the project directory run the following command. 252 | 253 | Requires node.js and yuidocjs to build. Follow the steps in [build](https://github.com/amkirwan/ember-oauth2#building) to install the dependencies before buiding the docs. 254 | 255 | ```bash 256 | $ yuidoc . 257 | ``` 258 | 259 | 260 | ## Contributors 261 | 262 | [Contributors](https://github.com/amkirwan/ember-oauth2/graphs/contributors) to this project. 263 | 264 | 265 | ## Credits 266 | 267 | #### Thanks to the following projects for ideas on how to make this work. 268 | 269 | * [backbone-oauth](http://github.com/ptnplanet/backbone-oauth) 270 | -------------------------------------------------------------------------------- /addon/services/ember-oauth2.js: -------------------------------------------------------------------------------- 1 | import { Promise as EmberPromise } from 'rsvp'; 2 | import Evented, { on } from '@ember/object/evented'; 3 | import Service from '@ember/service'; 4 | import { warn } from '@ember/debug'; 5 | 6 | /** 7 | * @overview OAuth2 addon for Emberjs that stores tokens in the browsers localStorage 8 | * @license Licensed under MIT license 9 | * See https://raw.github.com/amkirwan/ember-oauth2/master/LICENSE 10 | * @version 2.0.5-beta 11 | * 12 | * @module ember-oauth2 13 | * @class ember-oauth2 14 | */ 15 | // eslint-disable-next-line ember/no-classic-classes 16 | export default Service.extend(Evented, { 17 | VERSION: '2.0.5-beta', 18 | /** 19 | * initialize with the providerId to find in 20 | * EmberENV['ember-oauth2'] config 21 | */ 22 | init() { 23 | this._super(...arguments); 24 | if (!window.EmberENV['ember-oauth2']) { 25 | window.EmberENV['ember-oauth2'] = {}; 26 | } 27 | this.set('config', window.EmberENV['ember-oauth2']); 28 | 29 | this.set('statePrefix', 'state'); 30 | this.set('tokenPrefix', 'token'); 31 | this.set('responseType', 'token'); 32 | }, 33 | 34 | /** 35 | * Set the provider for the ember-oauth2 service with the providerId configured 36 | * in EmberENV['ember-oauth2']. 37 | * 38 | * @method setProvider 39 | * @param {String} providerId the provider Id configured in EmberENV['ember-oauth2'] 40 | */ 41 | setProvider(providerId) { 42 | this.set('providerId', providerId); 43 | // if the provider id doesn't exist in the config throw an error 44 | if (!this.config[this.providerId]) { 45 | throw new Error( 46 | `Cannot find the providerId: ${this.providerId} in the config.` 47 | ); 48 | } else { 49 | this.set('providerConfig', this.config[this.providerId]); 50 | this.setProperties(this.providerConfig); 51 | return this; 52 | } 53 | }, 54 | 55 | /** 56 | * Open authorize window if the configuration object is valid. 57 | * 58 | * @method authorize 59 | * @return {Promise} 60 | */ 61 | authorize() { 62 | if (!this.providerId) { 63 | throw new Error('No provider id given.'); 64 | } 65 | if (!this.clientId) { 66 | throw new Error('No client id given.'); 67 | } 68 | if (!this.authBaseUri) { 69 | throw new Error('No auth base uri given.'); 70 | } 71 | if (!this.redirectUri) { 72 | throw new Error('No redirect uri given.'); 73 | } 74 | this.clearStates(); 75 | this.saveState(this.requestObj()); 76 | return this.openWindow(this.authUri()); 77 | }, 78 | 79 | /** 80 | * Isolated function responsible for opening windows, to make it 81 | * easier to override this part in some environments (e.g. Phonegap) 82 | 83 | @param {String} url 84 | @return {Object} On resolve returns reference to the opened window. 85 | On reject returns Object with reference to dialog and error. 86 | */ 87 | openWindow(url, windowWidth = 600, windowHeight = 800) { 88 | const windowLeft = window.screen.width / 2 - windowWidth / 2; 89 | const windowTop = window.screen.height / 2 - windowHeight / 2; 90 | const options = `menubar, width=${windowWidth}, height=${windowHeight}, top=${windowTop}, left=${windowLeft}`; 91 | const dialog = window.open(url, 'Authorize', options); 92 | if (window.focus && dialog) { 93 | dialog.focus(); 94 | } 95 | return new EmberPromise(function (resolve, reject) { 96 | if (dialog) { 97 | resolve(dialog); 98 | } else { 99 | reject(new Error('Opening dialog login window failed.')); 100 | } 101 | }); 102 | }, 103 | 104 | /** 105 | * 106 | * Check if the token returned is valid and if so trigger `success` event else trigger `error` 107 | * 108 | * @method handleRedirect 109 | * @param {Object} hash The window location hash callback url 110 | * @param {Function} callback Optional callback 111 | */ 112 | 113 | handleRedirect: on('redirect', function (hash, callback) { 114 | const self = this; 115 | const params = self.parseCallback(hash); 116 | 117 | if (self.authSuccess(params) && self.checkState(params.state)) { 118 | if (self.get('responseType') === 'token') { 119 | self.saveToken(self.generateToken(params)); 120 | // verify the token on the client end 121 | self.verifyToken().then( 122 | function () { 123 | self.trigger('success'); 124 | }, 125 | function () { 126 | self.removeToken(); 127 | self.trigger('error', 'Error: verifying token', params); 128 | } 129 | ); 130 | } else { 131 | self.trigger('success', params.code); 132 | } 133 | } else { 134 | self.trigger('error', 'Error: authorization', params); 135 | } 136 | 137 | if (callback && typeof callback === 'function') { 138 | callback(); 139 | } 140 | }), 141 | 142 | /** 143 | @method authSuccess 144 | @param {Object} The params returned from the OAuth2 callback 145 | @return {Boolean} True if success false otherwise 146 | */ 147 | authSuccess(params) { 148 | return ( 149 | (this.responseType === 'token' && params.access_token) || 150 | (this.responseType === 'code' && params.code) 151 | ); 152 | }, 153 | 154 | /** 155 | * The key name to use for saving the token to localstorage 156 | * 157 | * @method tokenKeyName 158 | * @return {String} The token key name used for localstorage 159 | */ 160 | tokenKeyName() { 161 | return this.tokenPrefix + '-' + this.providerId; 162 | }, 163 | 164 | /** 165 | * saveToken stores the token by the tokenPrefix and the providerId 166 | * access_token 167 | * expires : time that the token expires 168 | * providerId: the providerId 169 | * scopes: array of scopes 170 | * 171 | * @method saveToken 172 | * @param {Object} token Saves the params in the response from the OAuth2 server to localStorage with the key 'tokenPrefix-providerId 173 | */ 174 | saveToken(token) { 175 | window.localStorage.setItem(this.tokenKeyName(), JSON.stringify(token)); 176 | return window.localStorage.getItem(this.tokenKeyName()); 177 | }, 178 | 179 | /** 180 | * Token properties 181 | * providerId 182 | * expiresIn 183 | * scope 184 | * token 185 | 186 | @method generateToken 187 | @return {Object} The access_token object with info about the token 188 | */ 189 | generateToken(params) { 190 | const token = {}; 191 | token.provider_id = this.providerId; 192 | token.expires_in = this.expiresIn(params.expires_in); 193 | token.scope = this.scope; 194 | token.access_token = params.access_token; 195 | return token; 196 | }, 197 | 198 | /** 199 | * For Client-side flow verify the token with the endpoint. Mitigation for confused deputy. 200 | * This method should be replaced by the app using this library. 201 | * 202 | * @method verifyToken 203 | * @return {Promise} Checks with the endpoint if the token is valid 204 | */ 205 | verifyToken() { 206 | return EmberPromise.resolve(true); 207 | }, 208 | 209 | /** 210 | * Checks if the State returned from the server matches the state that was generated in the original request and saved in the browsers localStorage. 211 | * 212 | * @method checkState 213 | * @param {String} state The state to check 214 | * @return {Boolean} Will return true if the states false if they do not match 215 | */ 216 | checkState(state) { 217 | if (!state) { 218 | return false; 219 | } 220 | // check the state returned with state saved in localstorage 221 | if (state === this.readState().state) { 222 | this.removeState(this.stateKeyName()); 223 | return true; 224 | } else { 225 | warn( 226 | 'State returned from the server did not match the local saved state.', 227 | false, 228 | { 229 | id: 'ember-oauth2.invalid-state', 230 | } 231 | ); 232 | return false; 233 | } 234 | }, 235 | 236 | /** 237 | * Parse the callback function from the OAuth2 provider 238 | * 239 | * callback should have the following params if authentication is successful 240 | * state 241 | * access_token or code 242 | * token_type 243 | * expires_in 244 | * 245 | * @method parseCalback 246 | * @param {String} locationHash 247 | * @return {Object} The params returned from the OAuth2 provider 248 | */ 249 | parseCallback(locationHash) { 250 | const oauthParams = {}; 251 | const queryString = locationHash.substring(locationHash.indexOf('?')); 252 | const regex = /([^#?&=]+)=([^&]*)/g; 253 | let match; 254 | while ((match = regex.exec(queryString)) !== null) { 255 | oauthParams[decodeURIComponent(match[1])] = decodeURIComponent(match[2]); 256 | } 257 | return oauthParams; 258 | }, 259 | 260 | /** 261 | * @method authUri 262 | * @return {String} Authorization uri for generating an OAuth2 token 263 | */ 264 | authUri() { 265 | let uri = this.authBaseUri; 266 | uri += 267 | '?response_type=' + 268 | encodeURIComponent(this.responseType) + 269 | '&redirect_uri=' + 270 | encodeURIComponent(this.redirectUri) + 271 | '&client_id=' + 272 | encodeURIComponent(this.clientId) + 273 | '&state=' + 274 | encodeURIComponent(this.state); 275 | if (this.scope) { 276 | uri += '&scope=' + encodeURIComponent(this.scope).replace('%20', '+'); 277 | } 278 | return uri; 279 | }, 280 | 281 | /** 282 | * Creates and returns the request object. 283 | * 284 | * @method requestObj 285 | * @return {Object} request object 286 | */ 287 | requestObj() { 288 | const request = {}; 289 | request.response_type = this.responseType; 290 | request.providerId = this.providerId; 291 | request.clientId = this.clientId; 292 | request.state = this.generateState(); 293 | if (this.scope) { 294 | request.scope = this.scope; 295 | } 296 | return request; 297 | }, 298 | 299 | /** 300 | * @method saveState 301 | * @param {Object} requestObj Properties of the request state to save in localStorage 302 | */ 303 | saveState(requestObj) { 304 | window.localStorage.setItem( 305 | this.stateKeyName(), 306 | JSON.stringify(requestObj) 307 | ); 308 | }, 309 | 310 | /** 311 | * Remove any states from localStorage if they exist 312 | * @method clearStates 313 | * @return {Array} Keys used to remove states from localStorage 314 | */ 315 | clearStates() { 316 | const regex = new RegExp('^' + this.statePrefix + '-.*', 'g'); 317 | 318 | let name; 319 | const toRemove = []; 320 | for (let i = 0, l = window.localStorage.length; i < l; i++) { 321 | name = window.localStorage.key(i); 322 | if (name.match(regex)) { 323 | toRemove.push(name); 324 | } 325 | } 326 | 327 | for (let j = 0, len = toRemove.length; j < len; j++) { 328 | name = toRemove[j]; 329 | this.removeState(name); 330 | } 331 | return toRemove; 332 | }, 333 | 334 | /** 335 | * remove the state from localstorage 336 | * 337 | * @method removeState 338 | * @param {String} stateName The keyname of the state object in localstorage 339 | * @return {Object} The deleted state object from localstorage 340 | */ 341 | removeState(stateName) { 342 | if (stateName) { 343 | return window.localStorage.removeItem(stateName); 344 | } else { 345 | return window.localStorage.removeItem(this.stateKeyName()); 346 | } 347 | }, 348 | 349 | /** 350 | * Return the saved state object from localStoage. 351 | * 352 | * @method getState 353 | * @return {Object} Properties of the request state 354 | */ 355 | readState() { 356 | const stateObj = JSON.parse( 357 | window.localStorage.getItem(this.stateKeyName()) 358 | ); 359 | if (!stateObj) { 360 | return false; 361 | } 362 | 363 | return stateObj; 364 | }, 365 | 366 | /** 367 | * @method uuid 368 | * @return {String} A pseudo random uuid 369 | */ 370 | uuid() { 371 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( 372 | /[xy]/g, 373 | function (c) { 374 | const r = (Math.random() * 16) | 0, 375 | v = c === 'x' ? r : (r & 0x3) | 0x8; 376 | return v.toString(16); 377 | } 378 | ); 379 | }, 380 | 381 | /** 382 | * @method now 383 | * @return {Number} The current time in seconds rounded 384 | */ 385 | now() { 386 | return Math.round(new Date().getTime() / 1000.0); 387 | }, 388 | 389 | /** 390 | * The key name to use for saving state to localstorage 391 | * 392 | * @method stateKeyName 393 | * @return {String} The state key name used for localstorage 394 | */ 395 | stateKeyName() { 396 | if (!this.state) { 397 | this.generateState(); 398 | } 399 | return this.statePrefix + '-' + this.state; 400 | }, 401 | 402 | /** 403 | * @method generateState 404 | * @return {String} The state 405 | */ 406 | generateState(clear = false) { 407 | if (!this.state || clear === true) { 408 | this.set('state', this.uuid()); 409 | } 410 | return this.state; 411 | }, 412 | 413 | /** 414 | * @method getToken 415 | * @return {Object} The params from the OAuth2 response from localStorage with the key 'tokenPrefix-providerId'. 416 | */ 417 | getToken() { 418 | const token = JSON.parse(window.localStorage.getItem(this.tokenKeyName())); 419 | if (!token) { 420 | return null; 421 | } 422 | if (!token.access_token) { 423 | return null; 424 | } 425 | return token; 426 | }, 427 | 428 | /** 429 | * @method getAccessToken 430 | * @return {Object} The access_token param from the OAuth2 response from localStorage with the key 'tokenPrefix-providerId'. 431 | */ 432 | getAccessToken() { 433 | const token = this.getToken(); 434 | if (!token) { 435 | return null; 436 | } 437 | return token.access_token; 438 | }, 439 | 440 | /** 441 | * remove the token from localstorage 442 | * 443 | * @method removeToken 444 | * @return {Object} The token object in localstorage 445 | */ 446 | removeToken() { 447 | return window.localStorage.removeItem(this.tokenKeyName()); 448 | }, 449 | 450 | /** 451 | * @method expiresIn 452 | * @param {String} expires lifetime left of token in seconds 453 | * @return {Number} When the token expires in seconds. 454 | */ 455 | expiresIn(expires) { 456 | return this.now() + parseInt(expires, 10); 457 | }, 458 | 459 | /** 460 | * @method accessTokenIsExpired 461 | * @return {Boolean} Check if the access_token is expired. 462 | */ 463 | accessTokenIsExpired() { 464 | const token = this.getToken(); 465 | if (!token) { 466 | return true; 467 | } 468 | if (this.now() >= token.expires_in) { 469 | return true; 470 | } else { 471 | return false; 472 | } 473 | }, 474 | 475 | /** 476 | * Sets the access token expires_in time to 0 and saves the token to localStorage 477 | * @method expireAccessToken 478 | */ 479 | expireAccessToken() { 480 | const token = this.getToken(); 481 | if (!token) { 482 | return null; 483 | } 484 | token.expires_in = 0; 485 | this.saveToken(token); 486 | }, 487 | }); 488 | -------------------------------------------------------------------------------- /tests/unit/ember-oauth-test.js: -------------------------------------------------------------------------------- 1 | import { reject } from 'rsvp'; 2 | import { module, test } from 'qunit'; 3 | import { setupTest } from 'ember-qunit'; 4 | import sinon from 'sinon'; 5 | 6 | let service, responseType, clientId, authBaseUri, redirectUri, scopes; 7 | 8 | module('Unit | Service | EmberOAuth2', function (hooks) { 9 | setupTest(hooks); 10 | 11 | hooks.beforeEach(function () { 12 | responseType = 'token'; 13 | clientId = 'abcd'; 14 | authBaseUri = 'https://foobar.dev/oauth/authorize'; 15 | redirectUri = 'https://qux.dev/oauth/authorize/callback'; 16 | scopes = 'public'; 17 | // would be defined in the initializer of the app 18 | window.EmberENV['ember-oauth2'] = { 19 | model: 'user', 20 | test_auth: { 21 | clientId: clientId, 22 | authBaseUri: authBaseUri, 23 | redirectUri: redirectUri, 24 | scope: scopes, 25 | }, 26 | }; 27 | service = this.owner.lookup('service:ember-oauth2'); 28 | service.setProvider('test_auth'); 29 | }); 30 | 31 | test('EmberENV defined', function (assert) { 32 | assert.ok(window.EmberENV); 33 | }); 34 | 35 | test('adds ember-oauth2 object to EmberENV', function (assert) { 36 | assert.expect(2); 37 | assert.ok(window.EmberENV['ember-oauth2']); 38 | assert.equal(window.EmberENV['ember-oauth2'], service.get('config')); 39 | }); 40 | 41 | test('base properties token configuration', function (assert) { 42 | assert.ok(service.get('statePrefix')); 43 | assert.ok(service.get('tokenPrefix')); 44 | assert.ok(service.get('responseType')); 45 | }); 46 | 47 | test('#setProvider configures the provider from the providerId in the ember-oauth2 config', function (assert) { 48 | service.setProvider('test_auth'); 49 | assert.expect(5); 50 | assert.equal(service.get('providerId'), 'test_auth'); 51 | assert.deepEqual( 52 | service.get('providerConfig'), 53 | window.EmberENV['ember-oauth2']['test_auth'] 54 | ); 55 | // sets the properties from the providerConfig 56 | assert.equal(service.get('clientId'), clientId); 57 | assert.equal(service.get('authBaseUri'), authBaseUri); 58 | assert.equal(service.get('redirectUri'), redirectUri); 59 | }); 60 | 61 | test('#setProvider providerId does not exists in ember-oauth2 config', function (assert) { 62 | assert.throws(function () { 63 | service.setProvider('qux'); 64 | }, /Cannot find the providerId: qux in the config./); 65 | }); 66 | 67 | test('it returns the version', function (assert) { 68 | assert.ok(service.VERSION); 69 | }); 70 | 71 | test('#uuid returns a version 4 formatted uuid', function (assert) { 72 | let re = /[\d\w]{8}-[\d\w]{4}-4[\d\w]{3}-[\d\w]{4}-[\d\w]{12}/; 73 | assert.ok(re.test(service.uuid())); 74 | }); 75 | 76 | test('#now returns the time rounded to the closest second', function (assert) { 77 | let stub = sinon.stub(Date.prototype, 'getTime').callsFake(() => '1000'); 78 | assert.equal(service.now(), 1); 79 | stub.reset(); 80 | }); 81 | 82 | // tests #stateKeyName 83 | test('#statKeyName calls generateState if state empty', function (assert) { 84 | let spy = sinon.spy(service, 'generateState'); 85 | service.stateKeyName(); 86 | assert.ok(spy.calledOnce); 87 | }); 88 | 89 | test('#statKeyName returns the name for saving state to localstorage', function (assert) { 90 | service.set('state', '12345'); 91 | assert.equal(service.stateKeyName(), 'state-12345'); 92 | }); 93 | 94 | // tests #generateState 95 | test('#generateState creates a new state', function (assert) { 96 | assert.expect(3); 97 | let spy = sinon.spy(service, 'uuid'); 98 | assert.notOk(service.get('state')); 99 | service.generateState(); 100 | assert.ok(service.get('state')); 101 | assert.ok(spy.calledOnce); 102 | }); 103 | 104 | // #expiresIn 105 | test('#expiresIn returns when the token will expires', function (assert) { 106 | let stub = sinon.stub(service, 'now').callsFake(() => 1000); 107 | assert.equal(service.expiresIn('3600'), 4600); 108 | stub.reset(); 109 | }); 110 | 111 | test('#saveState', function (assert) { 112 | assert.expect(2); 113 | let spy = sinon.spy(service, 'stateKeyName'); 114 | let obj = { foo: 'bar' }; 115 | service.saveState(obj); 116 | assert.ok(spy.calledOnce); 117 | assert.equal( 118 | window.localStorage.getItem(service.stateKeyName()), 119 | JSON.stringify(obj) 120 | ); 121 | }); 122 | 123 | test('#removeState', function (assert) { 124 | assert.expect(4); 125 | 126 | window.localStorage.setItem('foobar', {}); 127 | assert.ok(window.localStorage.getItem('foobar')); 128 | service.removeState('foobar'); 129 | assert.notOk(window.localStorage.getItem('foobar')); 130 | 131 | // without stateName use saved stateKeyName; 132 | let obj = { foo: 'bar' }; 133 | service.saveState(obj); 134 | assert.ok(window.localStorage.getItem(service.stateKeyName())); 135 | service.removeState(); 136 | assert.notOk(window.localStorage.getItem(service.stateKeyName())); 137 | }); 138 | 139 | // clearStates all states with prefix 140 | test('remove any saved states with prefix', function (assert) { 141 | assert.expect(2); 142 | service = this.owner.lookup('service:ember-oauth2'); 143 | service.setProvider('test_auth'); 144 | let obj = { foo: 'bar' }; 145 | 146 | service.saveState(obj); 147 | assert.equal( 148 | window.localStorage.getItem(service.stateKeyName()), 149 | JSON.stringify(obj) 150 | ); 151 | 152 | service.clearStates(); 153 | let count = 0; 154 | let regex = new RegExp('^' + service.get('statePrefix') + '-.*', 'g'); 155 | for (let i = 0, l = window.localStorage.length; i < l; i++) { 156 | let name = window.localStorage.key(i); 157 | if (name.match(regex)) { 158 | count += 1; 159 | } 160 | } 161 | assert.equal(count, 0); 162 | }); 163 | 164 | // requestObj 165 | test('#requestObj', function (assert) { 166 | let obj = service.requestObj(); 167 | 168 | assert.equal(obj.response_type, 'token'); 169 | assert.equal(obj.providerId, 'test_auth'); 170 | assert.equal(obj.clientId, 'abcd'); 171 | assert.equal(obj.state, service.get('state')); 172 | assert.equal(obj.scope, 'public'); 173 | }); 174 | 175 | test('#authUri generates the authorization uri', function (assert) { 176 | let uri = service.get('authBaseUri'); 177 | uri += 178 | '?response_type=' + 179 | encodeURIComponent(responseType) + 180 | '&redirect_uri=' + 181 | encodeURIComponent(redirectUri) + 182 | '&client_id=' + 183 | encodeURIComponent(clientId) + 184 | '&state=' + 185 | encodeURIComponent(service.get('state')) + 186 | '&scope=' + 187 | encodeURIComponent(scopes); 188 | 189 | assert.equal(service.authUri(), uri); 190 | }); 191 | 192 | // #authorize 193 | test('#authorize success', function (assert) { 194 | let spyClearState = sinon.spy(service, 'clearStates'); 195 | let spySaveState = sinon.spy(service, 'saveState'); 196 | let spyOpenWindow = sinon.spy(service, 'openWindow'); 197 | let prom = service.authorize(); 198 | assert.strictEqual(prom.constructor.name, 'Promise'); 199 | assert.ok(spyClearState.calledOnce); 200 | assert.ok(spySaveState.calledOnce); 201 | assert.ok(spyOpenWindow.calledOnce); 202 | // close the popup window 203 | prom.then(function (win) { 204 | win.close(); 205 | }); 206 | }); 207 | 208 | // #authorize config errors 209 | test('should require a providerId in the config', function (assert) { 210 | service.set('providerId', null); 211 | assert.throws(function () { 212 | service.authorize(); 213 | }, /No provider id given./); 214 | }); 215 | 216 | test('should require a clientId in the config', function (assert) { 217 | service.set('clientId', null); 218 | assert.throws(function () { 219 | service.authorize(); 220 | }, /No client id given./); 221 | }); 222 | 223 | test('should require an authBaseUri in the config', function (assert) { 224 | service.set('authBaseUri', null); 225 | assert.throws(function () { 226 | service.authorize(); 227 | }, /No auth base uri given./); 228 | }); 229 | 230 | test('should require a redirectUri in the config', function (assert) { 231 | service.set('redirectUri', null); 232 | assert.throws(function () { 233 | service.authorize(); 234 | }, /No redirect uri given./); 235 | }); 236 | 237 | test('should error when dialog does not open', function (assert) { 238 | assert.expect(1); 239 | var stub = sinon.stub(window, 'open').returns(false); 240 | 241 | let prom = service.authorize(); 242 | prom.then( 243 | function () {}, 244 | function (error) { 245 | assert.equal(error.message, 'Opening dialog login window failed.'); 246 | } 247 | ); 248 | stub.reset(); 249 | }); 250 | 251 | // parse callback 252 | test('#parseCallback', function (assert) { 253 | let callbackUri = redirectUri; 254 | let state = service.generateState(); 255 | callbackUri += 256 | '#access_token=' + 257 | '12345abc' + 258 | '&token_type=' + 259 | 'Bearer' + 260 | '&expires_in=' + 261 | '3600' + 262 | '&state=' + 263 | state; 264 | 265 | assert.deepEqual(service.parseCallback(callbackUri), { 266 | access_token: '12345abc', 267 | token_type: 'Bearer', 268 | expires_in: '3600', 269 | state: state, 270 | }); 271 | }); 272 | 273 | test('#authSuccess', function (assert) { 274 | assert.expect(3); 275 | let params = { access_token: '12345abc' }; 276 | assert.ok(service.authSuccess(params)); 277 | 278 | service.set('responseType', 'code'); 279 | params = { code: 'abcdefg' }; 280 | assert.ok(service.authSuccess(params)); 281 | 282 | // response and type do not match 283 | params = { access_token: '12345abc' }; 284 | service.set('responseType', 'code'); 285 | assert.notOk(service.authSuccess(params)); 286 | }); 287 | 288 | test('#checkState', function (assert) { 289 | assert.expect(3); 290 | assert.notOk(service.checkState()); 291 | 292 | let state = '12345abcd'; 293 | service.generateState(); 294 | assert.notOk(service.checkState(state)); 295 | 296 | state = service.generateState(); 297 | service.saveState(service.requestObj()); 298 | assert.ok(service.checkState(state)); 299 | }); 300 | 301 | test('#readState', function (assert) { 302 | assert.notOk(service.readState()); 303 | 304 | let data = { foo: 'bar' }; 305 | service.generateState(); 306 | service.saveState(data); 307 | assert.deepEqual(service.readState(), data); 308 | }); 309 | 310 | test('#generateToken should generate the token that will be saved to the localStorage', function (assert) { 311 | let stub = sinon.stub(service, 'expiresIn').callsFake(() => 1000); 312 | let params = { expires_in: 1000, scope: scopes, access_token: 'abcd12345' }; 313 | let token = { 314 | provider_id: 'test_auth', 315 | expires_in: 1000, 316 | scope: scopes, 317 | access_token: 'abcd12345', 318 | }; 319 | 320 | assert.deepEqual(service.generateToken(params), token); 321 | stub.reset(); 322 | }); 323 | 324 | test('#tokenKeyName returns tokenPrefx with providerId', function (assert) { 325 | // should return token-google 326 | assert.equal(service.tokenKeyName(), 'token-test_auth'); 327 | }); 328 | 329 | test('#saveToken should generated the token localStorage', function (assert) { 330 | let token = { 331 | provider_id: 'test_auth', 332 | expires_in: 1000, 333 | scope: scopes, 334 | access_token: 'abcd12345', 335 | }; 336 | assert.deepEqual( 337 | service.saveToken(token), 338 | window.localStorage.getItem('token-test_auth') 339 | ); 340 | }); 341 | 342 | // handle redirect 343 | // success Implicit client-side flow 344 | test('#handleRedirect - success', function (assert) { 345 | let spy = sinon.spy(service, 'handleRedirect'); 346 | let triggerSpy = sinon.spy(service, 'trigger'); 347 | 348 | // create stubbed callback return 349 | let callbackUri = redirectUri; 350 | let state = service.generateState(); 351 | service.saveState(service.requestObj({})); 352 | callbackUri += 353 | '#access_token=' + 354 | '12345abc' + 355 | '&token_type=' + 356 | 'Bearer' + 357 | '&expires_in=' + 358 | '3600' + 359 | '&state=' + 360 | state; 361 | 362 | let parsed = { 363 | access_token: '12345abc', 364 | token_type: 'Bearer', 365 | expires_in: '3600', 366 | state: state, 367 | }; 368 | let stub = sinon.stub(service, 'parseCallback').callsFake(() => parsed); 369 | 370 | service.trigger('redirect', callbackUri); 371 | assert.ok(spy.calledOnce); 372 | assert.ok(triggerSpy.withArgs('success', parsed)); 373 | stub.reset(); 374 | }); 375 | 376 | // failure Implicit client-side flow 377 | // verifyToken failure 378 | test('#handleRedirect - verifyToken failure', function (assert) { 379 | let spy = sinon.spy(service, 'handleRedirect'); 380 | let triggerSpy = sinon.spy(service, 'trigger'); 381 | let verifyStub = sinon 382 | .stub(service, 'verifyToken') 383 | .callsFake(() => new reject('error')); 384 | 385 | // create stubbed callback return 386 | let callbackUri = redirectUri; 387 | let state = service.generateState(); 388 | service.saveState(service.requestObj({})); 389 | callbackUri += 390 | '#access_token=' + 391 | '12345abc' + 392 | '&token_type=' + 393 | 'Bearer' + 394 | '&expires_in=' + 395 | '3600' + 396 | '&state=' + 397 | state; 398 | 399 | let parsed = { 400 | access_token: '12345abc', 401 | token_type: 'Bearer', 402 | expires_in: '3600', 403 | state: state, 404 | }; 405 | let stub = sinon.stub(service, 'parseCallback').callsFake(() => parsed); 406 | 407 | service.trigger('redirect', callbackUri); 408 | assert.ok(spy.calledOnce); 409 | assert.ok(triggerSpy.withArgs('error', 'Error: authorization', parsed)); 410 | stub.reset(); 411 | verifyStub.reset(); 412 | }); 413 | 414 | // failure Implicit client-side flow 415 | // state does not match failure 416 | test('#handleRedirect - failure state does not match', function (assert) { 417 | let spy = sinon.spy(service, 'handleRedirect'); 418 | let triggerSpy = sinon.spy(service, 'trigger'); 419 | 420 | // create stubbed callback return 421 | let callbackUri = redirectUri; 422 | let state = service.generateState(); 423 | service.saveState(service.requestObj({})); 424 | callbackUri += 425 | '#access_token=' + 426 | '12345abc' + 427 | '&token_type=' + 428 | 'Bearer' + 429 | '&expires_in=' + 430 | '3600' + 431 | '&state=' + 432 | '12345'; 433 | 434 | let parsed = { 435 | access_token: '12345abc', 436 | token_type: 'Bearer', 437 | expires_in: '3600', 438 | state: state, 439 | }; 440 | let stub = sinon.stub(service, 'parseCallback').callsFake(() => parsed); 441 | 442 | service.trigger('redirect', callbackUri); 443 | assert.ok(spy.calledOnce); 444 | assert.ok(triggerSpy.withArgs('error', 'Error: authorization', parsed)); 445 | stub.reset(); 446 | }); 447 | 448 | // failure Implicit client-side flow 449 | // responseType is 'token' but response of the 450 | // callbackUri is 'code' instead of 'token' 451 | test('#handleRedirect - tokenType is incorrect', function (assert) { 452 | let spy = sinon.spy(service, 'handleRedirect'); 453 | let triggerSpy = sinon.spy(service, 'trigger'); 454 | 455 | // create stubbed callback return 456 | let callbackUri = redirectUri; 457 | let state = service.generateState(); 458 | service.saveState(service.requestObj({})); 459 | callbackUri += 460 | '#code=' + 461 | '12345abc' + 462 | '&token_type=' + 463 | 'Bearer' + 464 | '&expires_in=' + 465 | '3600' + 466 | '&state=' + 467 | state; 468 | 469 | let parsed = { 470 | code: '12345abc', 471 | token_type: 'Bearer', 472 | expires_in: '3600', 473 | state: state, 474 | }; 475 | let stub = sinon.stub(service, 'parseCallback').callsFake(() => parsed); 476 | 477 | service.trigger('redirect', callbackUri); 478 | assert.ok(spy.calledOnce); 479 | assert.ok(triggerSpy.withArgs('error', 'Error: authorization', parsed)); 480 | stub.reset(); 481 | }); 482 | 483 | // success authorization flow 484 | test('#handleRedirect - success authorization flow', function (assert) { 485 | service = this.owner 486 | .factoryFor('service:ember-oauth2') 487 | .create({ providerId: 'test_auth', responseType: 'code' }); 488 | let spy = sinon.spy(service, 'handleRedirect'); 489 | let triggerSpy = sinon.spy(service, 'trigger'); 490 | 491 | // create stubbed callback return 492 | let callbackUri = redirectUri; 493 | let state = service.generateState(); 494 | service.saveState(service.requestObj({})); 495 | callbackUri += 496 | '#code=' + 497 | '12345abc' + 498 | '&token_type=' + 499 | 'Bearer' + 500 | '&expires_in=' + 501 | '3600' + 502 | '&state=' + 503 | state; 504 | 505 | let parsed = { 506 | code: '12345abc', 507 | token_type: 'Bearer', 508 | expires_in: '3600', 509 | state: state, 510 | }; 511 | let stub = sinon.stub(service, 'parseCallback').callsFake(() => parsed); 512 | 513 | service.trigger('redirect', callbackUri); 514 | assert.ok(spy.calledOnce); 515 | assert.ok(triggerSpy.withArgs('success', parsed.code)); 516 | stub.reset(); 517 | }); 518 | 519 | test('#getToken should return the token from localStorage', function (assert) { 520 | assert.expect(3); 521 | let invalidToken = { foo: 'bar' }; 522 | let validToken = { access_token: 'abcd', foo: 'bar' }; 523 | window.localStorage.removeItem(service.tokenKeyName()); 524 | assert.notOk(service.getToken()); 525 | 526 | service.saveToken(invalidToken); 527 | assert.notOk(service.getToken()); 528 | 529 | service.saveToken(validToken); 530 | assert.deepEqual(service.getToken(), validToken); 531 | }); 532 | 533 | test('#getAccessToken should return the access_token from the localStorage', function (assert) { 534 | assert.expect(2); 535 | let token = { access_token: 'abcd', foo: 'bar' }; 536 | window.localStorage.removeItem(service.tokenKeyName()); 537 | assert.notOk(service.getAccessToken()); 538 | 539 | service.saveToken(token); 540 | assert.deepEqual(service.getAccessToken(), token.access_token); 541 | }); 542 | 543 | test('#accessTokenIsExpired', function (assert) { 544 | assert.expect(3); 545 | let expiredToken = { access_token: 'abcd', foo: 'bar', expires_in: 3600 }; 546 | let validToken = { access_token: 'abcd', foo: 'bar', expires_in: 3600 }; 547 | window.localStorage.removeItem(service.tokenKeyName()); 548 | let stub = sinon.stub(service, 'now'); 549 | stub.onCall(0).returns(4200); 550 | stub.onCall(1).returns(1); 551 | // no tokens 552 | assert.ok(service.accessTokenIsExpired()); 553 | 554 | service.saveToken(expiredToken); 555 | assert.ok(service.accessTokenIsExpired()); 556 | 557 | service.saveToken(validToken); 558 | assert.notOk(service.accessTokenIsExpired()); 559 | stub.reset(); 560 | }); 561 | 562 | test('#expiresIn', function (assert) { 563 | let stub = sinon.stub(service, 'now').callsFake(() => 1000); 564 | 565 | assert.equal(service.expiresIn(3600), 4600); 566 | stub.reset(); 567 | }); 568 | 569 | test('#removeToken', function (assert) { 570 | assert.expect(2); 571 | window.localStorage.removeItem(service.tokenKeyName()); 572 | let token = { access_token: 'abcd', foo: 'bar' }; 573 | service.saveToken(token); 574 | assert.equal( 575 | window.localStorage.getItem(service.tokenKeyName()), 576 | JSON.stringify(token) 577 | ); 578 | service.removeToken(); 579 | assert.equal( 580 | window.localStorage.getItem(service.tokenKeyName()), 581 | undefined 582 | ); 583 | }); 584 | }); 585 | --------------------------------------------------------------------------------