From 0d15d6a54c89dceef4ae59579a085c1988a93737 Mon Sep 17 00:00:00 2001 From: Manfred Steyer Date: Mon, 23 Mar 2020 12:15:28 +0100 Subject: [PATCH] chore: Add prettier and execute it --- .prettierignore | 4 + .prettierrc | 3 + README.md | 97 +- angular.json | 26 +- package.json | 7 +- projects/angular-oauth2-oidc-jwks/README.md | 2 +- .../angular-oauth2-oidc-jwks/karma.conf.js | 7 +- .../angular-oauth2-oidc-jwks/ng-package.json | 6 +- .../angular-oauth2-oidc-jwks/package.json | 2 +- .../src/lib/jwks-validation-handler.ts | 7 +- .../tsconfig.lib.json | 10 +- .../tsconfig.spec.json | 14 +- projects/angular-oauth2-oidc-jwks/tslint.json | 14 +- projects/lib/karma.conf.js | 2 +- projects/lib/ng-package.json | 2 +- projects/lib/src/.vscode/settings.json | 56 +- projects/lib/src/angular-oauth-oidc.module.ts | 7 +- projects/lib/src/auth.config.ts | 10 +- projects/lib/src/base64-helper.ts | 2 +- projects/lib/src/factories.ts | 8 +- .../interceptors/default-oauth.interceptor.ts | 59 +- projects/lib/src/oauth-service.ts | 4365 +++++++++-------- .../lib/src/token-validation/hash-handler.ts | 103 +- .../jwks-validation-handler.ts | 4 +- .../token-validation/validation-handler.ts | 9 +- projects/lib/src/types.ts | 5 +- projects/lib/src/url-helper.service.ts | 13 +- projects/lib/tsconfig.lib.json | 10 +- projects/lib/tsconfig.lib.prod.json | 2 +- projects/lib/tsconfig.spec.json | 14 +- projects/lib/tslint.json | 20 +- .../quickstart-demo/e2e/protractor.conf.js | 12 +- .../quickstart-demo/e2e/src/app.e2e-spec.ts | 13 +- projects/quickstart-demo/e2e/tsconfig.json | 6 +- projects/quickstart-demo/karma.conf.js | 2 +- .../src/app/app.component.html | 4 +- .../src/app/app.component.spec.ts | 8 +- .../quickstart-demo/src/app/app.component.ts | 8 +- .../quickstart-demo/src/app/app.module.ts | 12 +- .../quickstart-demo/src/app/auth.config.ts | 2 +- projects/quickstart-demo/src/index.html | 22 +- projects/quickstart-demo/src/main.ts | 3 +- projects/quickstart-demo/src/polyfills.ts | 3 +- projects/quickstart-demo/tsconfig.app.json | 9 +- projects/quickstart-demo/tsconfig.spec.json | 15 +- projects/quickstart-demo/tslint.json | 14 +- projects/sample/karma.conf.js | 4 +- projects/sample/src/app/app.component.html | 28 +- projects/sample/src/app/app.component.spec.ts | 4 +- projects/sample/src/app/app.component.ts | 8 +- projects/sample/src/app/app.module.ts | 5 +- projects/sample/src/app/app.routes.ts | 5 +- .../sample/src/app/auth-code-flow.config.ts | 25 +- projects/sample/src/app/auth.config.ts | 2 +- .../alt-flight-card.component.html | 29 +- .../alt-flight-card/flight-list.ts | 21 +- .../flight-booking.component.html | 13 +- .../flight-card/flight-card.component.html | 32 +- .../flight-edit/flight-edit.component.ts | 38 +- .../flight-search-reactive.component.css | 16 +- .../flight-search-reactive.component.html | 97 +- .../flight-search/flight-search.component.css | 16 +- .../flight-search.component.html | 139 +- .../flight-search/flight-search.component.ts | 7 +- .../passenger-search.component.ts | 9 +- .../flight-booking/services/flight.service.ts | 18 +- .../flight-history.component.ts | 14 +- .../sample/src/app/home/home.component.html | 160 +- .../sample/src/app/home/home.component.ts | 43 +- .../password-flow-login.component.html | 64 +- .../src/app/shared/date/date.component.ts | 6 +- projects/sample/src/flags.ts | 1 - projects/sample/src/index.html | 24 +- projects/sample/src/polyfills.ts | 1 - projects/sample/src/silent-refresh.html | 39 +- projects/sample/tsconfig.app.json | 5 +- projects/sample/tsconfig.spec.json | 15 +- projects/sample/tslint.json | 40 +- tsconfig.json | 22 +- tsconfig.npm.json | 5 +- tslint.json | 53 +- 81 files changed, 2993 insertions(+), 3038 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..d0b804da --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Add files here to ignore them from prettier formatting + +/dist +/coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..544138be --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/README.md b/README.md index 16bd1ac2..62b6a000 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Support for OAuth 2 and OpenId Connect (OIDC) in Angular. Already prepared for t ## Breaking Change in Version 9 -With regards to tree shaking, beginning with version 9, the ``JwksValidationHandler`` has been moved to a library of its own. If you need it for implementing **implicit flow**, please install it using npm: +With regards to tree shaking, beginning with version 9, the `JwksValidationHandler` has been moved to a library of its own. If you need it for implementing **implicit flow**, please install it using npm: ``` npm i angular-oauth2-oidc-jwks --save @@ -38,7 +38,6 @@ import { JwksValidationHandler } from 'angular-oauth2-oidc'; Please note, that this dependency is not needed for the **code flow**, which is nowadays the **recommended** flow for single page applications. This also results in smaller bundle sizes. - ## Tested Environment Successfully tested with **Angular 9** and its Router, PathLocationStrategy as well as HashLocationStrategy and CommonJS-Bundling via webpack. At server side we've used IdentityServer (.NET / .NET Core) and Redhat's Keycloak (Java). @@ -66,14 +65,14 @@ Successfully tested with **Angular 9** and its Router, PathLocationStrategy as w - The issues contain some ideas for PRs and enhancements (see labels) - If you want to contribute to the docs, you can do so in the `docs-src` folder. Make sure you update `summary.json` as well. Then generate the docs with the following commands: - ``` sh + ```sh npm install -g @compodoc/compodoc npm run docs ``` ## Features -- Logging in via Code Flow + PKCE +- Logging in via Code Flow + PKCE - Hence, you are safe for the upcoming OAuth 2.1 - Logging in via Implicit Flow (where a user is redirected to Identity Provider) - "Logging in" via Password Flow (where a user enters their password into the client) @@ -90,17 +89,18 @@ Successfully tested with **Angular 9** and its Router, PathLocationStrategy as w You can use the OIDC-Sample-Server used in our examples. It assumes, that your Web-App runs on http://localhost:4200 -Username/Password: - - max/geheim - - bob/bob - - alice/alice +Username/Password: + +- max/geheim +- bob/bob +- alice/alice -*clientIds:* +_clientIds:_ - spa (Code Flow + PKCE) - implicit (implicit flow) -*redirectUris:* +_redirectUris:_ - localhost:[4200-4202] - localhost:[4200-4202]/index.html @@ -138,59 +138,58 @@ export class AppModule { } ``` -# Logging in +# Logging in Since Version 8, this library supports code flow and [PKCE](https://tools.ietf.org/html/rfc7636) to align with the current draft of the [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13) document. This is also the foundation of the upcoming OAuth 2.1. - To configure your solution for code flow + PKCE you have to set the `responseType` to `code`: - ```TypeScript - import { AuthConfig } from 'angular-oauth2-oidc'; +```TypeScript + import { AuthConfig } from 'angular-oauth2-oidc'; - export const authCodeFlowConfig: AuthConfig = { - // Url of the Identity Provider - issuer: 'https://demo.identityserver.io', + export const authCodeFlowConfig: AuthConfig = { + // Url of the Identity Provider + issuer: 'https://demo.identityserver.io', - // URL of the SPA to redirect the user to after login - redirectUri: window.location.origin + '/index.html', + // URL of the SPA to redirect the user to after login + redirectUri: window.location.origin + '/index.html', - // The SPA's id. The SPA is registerd with this id at the auth-server - // clientId: 'server.code', - clientId: 'spa', + // The SPA's id. The SPA is registerd with this id at the auth-server + // clientId: 'server.code', + clientId: 'spa', - // Just needed if your auth server demands a secret. In general, this - // is a sign that the auth server is not configured with SPAs in mind - // and it might not enforce further best practices vital for security - // such applications. - // dummyClientSecret: 'secret', + // Just needed if your auth server demands a secret. In general, this + // is a sign that the auth server is not configured with SPAs in mind + // and it might not enforce further best practices vital for security + // such applications. + // dummyClientSecret: 'secret', - responseType: 'code', + responseType: 'code', - // set the scope for the permissions the client should request - // The first four are defined by OIDC. - // Important: Request offline_access to get a refresh token - // The api scope is a usecase specific one - scope: 'openid profile email offline_access api', + // set the scope for the permissions the client should request + // The first four are defined by OIDC. + // Important: Request offline_access to get a refresh token + // The api scope is a usecase specific one + scope: 'openid profile email offline_access api', - showDebugInformation: true, + showDebugInformation: true, - // Not recommented: - // disablePKCI: true, - }; - ``` + // Not recommented: + // disablePKCI: true, + }; +``` After this, you can initialize the code flow using: - ```TypeScript - this.oauthService.initCodeFlow(); - ``` +```TypeScript +this.oauthService.initCodeFlow(); +``` -There is also a convenience method `initLoginFlow` which initializes either the code flow or the implicit flow depending on your configuration. +There is also a convenience method `initLoginFlow` which initializes either the code flow or the implicit flow depending on your configuration. - ```TypeScript - this.oauthService.initLoginFlow(); - ``` +```TypeScript +this.oauthService.initLoginFlow(); +``` Also -- as shown in the readme -- you have to execute the following code when bootstrapping to make the library to fetch the token: @@ -199,17 +198,15 @@ this.oauthService.configure(authCodeFlowConfig); this.oauthService.loadDiscoveryDocumentAndTryLogin(); ``` - ### Skipping the Login Form -If you don't want to display a login form that tells the user that they are redirected to the identity server, you can use the convenience function ``this.oauthService.loadDiscoveryDocumentAndLogin();`` instead of ``this.oauthService.loadDiscoveryDocumentAndTryLogin();`` when setting up the library. +If you don't want to display a login form that tells the user that they are redirected to the identity server, you can use the convenience function `this.oauthService.loadDiscoveryDocumentAndLogin();` instead of `this.oauthService.loadDiscoveryDocumentAndTryLogin();` when setting up the library. This directly redirects the user to the identity server if there are no valid tokens. Ensure you have your `issuer` set to your discovery document endpoint! - ### Calling a Web API with an Access Token -You can automate this task by switching ``sendAccessToken`` on and by setting ``allowedUrls`` to an array with prefixes for the respective URLs. Use lower case for the prefixes. +You can automate this task by switching `sendAccessToken` on and by setting `allowedUrls` to an array with prefixes for the respective URLs. Use lower case for the prefixes. ```TypeScript OAuthModule.forRoot({ @@ -228,7 +225,7 @@ See docs: https://manfredsteyer.github.io/angular-oauth2-oidc/docs/additional-do ## Routing -If you use the ``PathLocationStrategy`` (which is on by default) and have a general catch-all-route (``path: '**'``) you should be fine. Otherwise look up the section ``Routing with the HashStrategy`` in the [documentation](https://manfredsteyer.github.io/angular-oauth2-oidc/docs/). +If you use the `PathLocationStrategy` (which is on by default) and have a general catch-all-route (`path: '**'`) you should be fine. Otherwise look up the section `Routing with the HashStrategy` in the [documentation](https://manfredsteyer.github.io/angular-oauth2-oidc/docs/). ## Implicit Flow diff --git a/angular.json b/angular.json index c6885672..3d31073e 100644 --- a/angular.json +++ b/angular.json @@ -29,9 +29,7 @@ "projects/lib/tsconfig.lib.json", "projects/lib/tsconfig.spec.json" ], - "exclude": [ - "**/node_modules/**" - ] + "exclude": ["**/node_modules/**"] } } } @@ -131,9 +129,7 @@ "projects/sample/tsconfig.app.json", "projects/sample/tsconfig.spec.json" ], - "exclude": [ - "**/node_modules/**" - ] + "exclude": ["**/node_modules/**"] } } } @@ -159,9 +155,7 @@ "projects/quickstart-demo/src/favicon.ico", "projects/quickstart-demo/src/assets" ], - "styles": [ - "projects/quickstart-demo/src/styles.css" - ], + "styles": ["projects/quickstart-demo/src/styles.css"], "scripts": [] }, "configurations": { @@ -219,9 +213,7 @@ "projects/quickstart-demo/src/favicon.ico", "projects/quickstart-demo/src/assets" ], - "styles": [ - "projects/quickstart-demo/src/styles.css" - ], + "styles": ["projects/quickstart-demo/src/styles.css"], "scripts": [] } }, @@ -233,9 +225,7 @@ "projects/quickstart-demo/tsconfig.spec.json", "projects/quickstart-demo/e2e/tsconfig.json" ], - "exclude": [ - "**/node_modules/**" - ] + "exclude": ["**/node_modules/**"] } }, "e2e": { @@ -281,9 +271,7 @@ "projects/angular-oauth2-oidc-jwks/tsconfig.lib.json", "projects/angular-oauth2-oidc-jwks/tsconfig.spec.json" ], - "exclude": [ - "**/node_modules/**" - ] + "exclude": ["**/node_modules/**"] } } } @@ -297,4 +285,4 @@ "cli": { "analytics": false } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 8e1df8b8..92d4896c 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "scripts": { "ng": "ng", "start": "ng serve --project sample -o", - "build": "ng build --prod --project lib && npm run copy:readme && npm run docs", - "build:jwks": "ng build angular-oauth2-oidc-jwks --ts-config tsconfig.npm.json", + "build": "npm run prettier && ng build --prod --project lib && npm run copy:readme && npm run docs", + "build:jwks": "npm run prettier && ng build angular-oauth2-oidc-jwks --ts-config tsconfig.npm.json", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", "tsc": "tsc", + "prettier": "prettier --write projects/**", "docs": "npm run docs:build -- --disableCoverage --disablePrivate --disableInternal --includes docs-src", "docs:build": "compodoc -p projects/lib/tsconfig.lib.json -n angular-oauth2-oidc -d docs --hideGenerator", "docs:serve": "npm run docs:build -- -s", @@ -64,7 +65,7 @@ "karma-jasmine": "~3.1.0", "karma-jasmine-html-reporter": "^1.5.2", "ng-packagr": "^9.0.0", - "prettier": "1.19.1", + "prettier": "^1.19.1", "protractor": "~5.4.3", "ts-node": "~8.6.2", "tslint": "~5.18.0", diff --git a/projects/angular-oauth2-oidc-jwks/README.md b/projects/angular-oauth2-oidc-jwks/README.md index 5e7d9c29..59e81ab3 100644 --- a/projects/angular-oauth2-oidc-jwks/README.md +++ b/projects/angular-oauth2-oidc-jwks/README.md @@ -1,3 +1,3 @@ # angular-oauth2-oidc-jwks -``JwksValidationHandler`` for ``angular-oauth2-odic``. Only needed for implicit flow. \ No newline at end of file +`JwksValidationHandler` for `angular-oauth2-odic`. Only needed for implicit flow. diff --git a/projects/angular-oauth2-oidc-jwks/karma.conf.js b/projects/angular-oauth2-oidc-jwks/karma.conf.js index 660f2949..f7f4af20 100644 --- a/projects/angular-oauth2-oidc-jwks/karma.conf.js +++ b/projects/angular-oauth2-oidc-jwks/karma.conf.js @@ -1,7 +1,7 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html -module.exports = function (config) { +module.exports = function(config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], @@ -16,7 +16,10 @@ module.exports = function (config) { clearContext: false // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../../coverage/angular-oauth2-oidc-jwks'), + dir: require('path').join( + __dirname, + '../../coverage/angular-oauth2-oidc-jwks' + ), reports: ['html', 'lcovonly'], fixWebpackSourcePaths: true }, diff --git a/projects/angular-oauth2-oidc-jwks/ng-package.json b/projects/angular-oauth2-oidc-jwks/ng-package.json index ff2d04f0..57bde527 100644 --- a/projects/angular-oauth2-oidc-jwks/ng-package.json +++ b/projects/angular-oauth2-oidc-jwks/ng-package.json @@ -4,7 +4,5 @@ "lib": { "entryFile": "src/public-api.ts" }, - "whitelistedNonPeerDependencies": [ - "jsrsasign" - ] -} \ No newline at end of file + "whitelistedNonPeerDependencies": ["jsrsasign"] +} diff --git a/projects/angular-oauth2-oidc-jwks/package.json b/projects/angular-oauth2-oidc-jwks/package.json index 50f8ed9d..40f531b2 100644 --- a/projects/angular-oauth2-oidc-jwks/package.json +++ b/projects/angular-oauth2-oidc-jwks/package.json @@ -4,4 +4,4 @@ "dependencies": { "jsrsasign": "^8.0.12" } -} \ No newline at end of file +} diff --git a/projects/angular-oauth2-oidc-jwks/src/lib/jwks-validation-handler.ts b/projects/angular-oauth2-oidc-jwks/src/lib/jwks-validation-handler.ts index 00ebc29b..7325296f 100644 --- a/projects/angular-oauth2-oidc-jwks/src/lib/jwks-validation-handler.ts +++ b/projects/angular-oauth2-oidc-jwks/src/lib/jwks-validation-handler.ts @@ -1,5 +1,8 @@ import * as rs from 'jsrsasign'; -import { AbstractValidationHandler, ValidationParams } from 'angular-oauth2-oidc'; +import { + AbstractValidationHandler, + ValidationParams +} from 'angular-oauth2-oidc'; /** * Validates the signature of an id_token against one @@ -147,4 +150,4 @@ export class JwksValidationHandler extends AbstractValidationHandler { } return result; } -} \ No newline at end of file +} diff --git a/projects/angular-oauth2-oidc-jwks/tsconfig.lib.json b/projects/angular-oauth2-oidc-jwks/tsconfig.lib.json index bd23948e..2972099b 100644 --- a/projects/angular-oauth2-oidc-jwks/tsconfig.lib.json +++ b/projects/angular-oauth2-oidc-jwks/tsconfig.lib.json @@ -6,10 +6,7 @@ "declaration": true, "inlineSources": true, "types": [], - "lib": [ - "dom", - "es2018" - ] + "lib": ["dom", "es2018"] }, "angularCompilerOptions": { "annotateForClosureCompiler": true, @@ -19,8 +16,5 @@ "strictInjectionParameters": true, "enableResourceInlining": true }, - "exclude": [ - "src/test.ts", - "**/*.spec.ts" - ] + "exclude": ["src/test.ts", "**/*.spec.ts"] } diff --git a/projects/angular-oauth2-oidc-jwks/tsconfig.spec.json b/projects/angular-oauth2-oidc-jwks/tsconfig.spec.json index 16da33db..ec3528a8 100644 --- a/projects/angular-oauth2-oidc-jwks/tsconfig.spec.json +++ b/projects/angular-oauth2-oidc-jwks/tsconfig.spec.json @@ -2,16 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine", - "node" - ] + "types": ["jasmine", "node"] }, - "files": [ - "src/test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] + "files": ["src/test.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] } diff --git a/projects/angular-oauth2-oidc-jwks/tslint.json b/projects/angular-oauth2-oidc-jwks/tslint.json index 124133f8..205aedaa 100644 --- a/projects/angular-oauth2-oidc-jwks/tslint.json +++ b/projects/angular-oauth2-oidc-jwks/tslint.json @@ -1,17 +1,7 @@ { "extends": "../../tslint.json", "rules": { - "directive-selector": [ - true, - "attribute", - "lib", - "camelCase" - ], - "component-selector": [ - true, - "element", - "lib", - "kebab-case" - ] + "directive-selector": [true, "attribute", "lib", "camelCase"], + "component-selector": [true, "element", "lib", "kebab-case"] } } diff --git a/projects/lib/karma.conf.js b/projects/lib/karma.conf.js index 4c5f8d03..1a4dd5cf 100644 --- a/projects/lib/karma.conf.js +++ b/projects/lib/karma.conf.js @@ -1,7 +1,7 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html -module.exports = function (config) { +module.exports = function(config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], diff --git a/projects/lib/ng-package.json b/projects/lib/ng-package.json index 31eda8e1..862b3dbc 100644 --- a/projects/lib/ng-package.json +++ b/projects/lib/ng-package.json @@ -5,5 +5,5 @@ "lib": { "languageLevel": ["dom", "es2017"], "entryFile": "src/public_api.ts" - } + } } diff --git a/projects/lib/src/.vscode/settings.json b/projects/lib/src/.vscode/settings.json index fd388736..96c8d038 100644 --- a/projects/lib/src/.vscode/settings.json +++ b/projects/lib/src/.vscode/settings.json @@ -1,29 +1,29 @@ { - "cSpell.enabledLanguageIds": [ - "asciidoc", - "c", - "cpp", - "csharp", - "css", - "go", - "handlebars", - "html", - "jade", - "javascript", - "javascriptreact", - "json", - "latex", - "less", - "markdown", - "php", - "plaintext", - "pub", - "python", - "restructuredtext", - "rust", - "scss", - "text", - "typescriptreact", - "yml" - ] -} \ No newline at end of file + "cSpell.enabledLanguageIds": [ + "asciidoc", + "c", + "cpp", + "csharp", + "css", + "go", + "handlebars", + "html", + "jade", + "javascript", + "javascriptreact", + "json", + "latex", + "less", + "markdown", + "php", + "plaintext", + "pub", + "python", + "restructuredtext", + "rust", + "scss", + "text", + "typescriptreact", + "yml" + ] +} diff --git a/projects/lib/src/angular-oauth-oidc.module.ts b/projects/lib/src/angular-oauth-oidc.module.ts index da45fda8..0804510b 100644 --- a/projects/lib/src/angular-oauth-oidc.module.ts +++ b/projects/lib/src/angular-oauth-oidc.module.ts @@ -15,7 +15,10 @@ import { DefaultOAuthInterceptor } from './interceptors/default-oauth.intercepto import { ValidationHandler } from './token-validation/validation-handler'; import { NullValidationHandler } from './token-validation/null-validation-handler'; import { createDefaultLogger, createDefaultStorage } from './factories'; -import { HashHandler, DefaultHashHandler } from './token-validation/hash-handler'; +import { + HashHandler, + DefaultHashHandler +} from './token-validation/hash-handler'; @NgModule({ imports: [CommonModule], @@ -34,7 +37,7 @@ export class OAuthModule { UrlHelperService, { provide: OAuthLogger, useFactory: createDefaultLogger }, { provide: OAuthStorage, useFactory: createDefaultStorage }, - { provide: ValidationHandler, useClass: validationHandlerClass}, + { provide: ValidationHandler, useClass: validationHandlerClass }, { provide: HashHandler, useClass: DefaultHashHandler }, { provide: OAuthResourceServerErrorHandler, diff --git a/projects/lib/src/auth.config.ts b/projects/lib/src/auth.config.ts index 94406326..747b9601 100644 --- a/projects/lib/src/auth.config.ts +++ b/projects/lib/src/auth.config.ts @@ -82,7 +82,7 @@ export class AuthConfig { * the verbosity of the console needs to be explicitly set * to include Debug level messages. */ - public showDebugInformation? = false; + public showDebugInformation? = false; /** * The redirect uri used when doing silent refresh. @@ -228,7 +228,7 @@ export class AuthConfig { /** * The interceptors waits this time span if there is no token - */ + */ public waitForTokenInMsec? = 0; /** @@ -257,9 +257,7 @@ export class AuthConfig { * allowing a way for implementations to specify their own method of routing to new * urls. */ - public openUri?: ((uri: string) => void) = uri => { + public openUri?: (uri: string) => void = uri => { location.href = uri; - } - - + }; } diff --git a/projects/lib/src/base64-helper.ts b/projects/lib/src/base64-helper.ts index a0140d48..6568d3dc 100644 --- a/projects/lib/src/base64-helper.ts +++ b/projects/lib/src/base64-helper.ts @@ -18,4 +18,4 @@ export function base64UrlEncode(str): string { .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); -} \ No newline at end of file +} diff --git a/projects/lib/src/factories.ts b/projects/lib/src/factories.ts index b640f793..36d1a6ef 100644 --- a/projects/lib/src/factories.ts +++ b/projects/lib/src/factories.ts @@ -1,9 +1,11 @@ import { MemoryStorage } from './types'; export function createDefaultLogger() { - return console; + return console; } export function createDefaultStorage() { - return typeof sessionStorage !== 'undefined' ? sessionStorage : new MemoryStorage(); -} \ No newline at end of file + return typeof sessionStorage !== 'undefined' + ? sessionStorage + : new MemoryStorage(); +} diff --git a/projects/lib/src/interceptors/default-oauth.interceptor.ts b/projects/lib/src/interceptors/default-oauth.interceptor.ts index 4ce5c582..0d1c245e 100644 --- a/projects/lib/src/interceptors/default-oauth.interceptor.ts +++ b/projects/lib/src/interceptors/default-oauth.interceptor.ts @@ -4,10 +4,17 @@ import { HttpEvent, HttpHandler, HttpInterceptor, - HttpRequest, + HttpRequest } from '@angular/common/http'; import { Observable, of, merge } from 'rxjs'; -import { catchError, filter, map, take, mergeMap, timeout } from 'rxjs/operators'; +import { + catchError, + filter, + map, + take, + mergeMap, + timeout +} from 'rxjs/operators'; import { OAuthResourceServerErrorHandler } from './resource-server-error-handler'; import { OAuthModuleConfig } from '../oauth-module.config'; import { OAuthStorage } from '../types'; @@ -15,34 +22,38 @@ import { OAuthService } from '../oauth-service'; @Injectable() export class DefaultOAuthInterceptor implements HttpInterceptor { + constructor( + private authStorage: OAuthStorage, + private oAuthService: OAuthService, + private errorHandler: OAuthResourceServerErrorHandler, + @Optional() private moduleConfig: OAuthModuleConfig + ) {} - constructor( - private authStorage: OAuthStorage, - private oAuthService: OAuthService, - private errorHandler: OAuthResourceServerErrorHandler, - @Optional() private moduleConfig: OAuthModuleConfig - ) { } - - private checkUrl(url: string): boolean { - if (this.moduleConfig.resourceServer.customUrlValidation) { - return this.moduleConfig.resourceServer.customUrlValidation(url); - } - - if (this.moduleConfig.resourceServer.allowedUrls) { - return !!this.moduleConfig.resourceServer.allowedUrls.find(u => url.startsWith(u)); - } + private checkUrl(url: string): boolean { + if (this.moduleConfig.resourceServer.customUrlValidation) { + return this.moduleConfig.resourceServer.customUrlValidation(url); + } - return true; + if (this.moduleConfig.resourceServer.allowedUrls) { + return !!this.moduleConfig.resourceServer.allowedUrls.find(u => + url.startsWith(u) + ); } + return true; + } + public intercept( req: HttpRequest, next: HttpHandler ): Observable> { const url = req.url.toLowerCase(); - - if (!this.moduleConfig || !this.moduleConfig.resourceServer || !this.checkUrl(url)) { + if ( + !this.moduleConfig || + !this.moduleConfig.resourceServer || + !this.checkUrl(url) + ) { return next.handle(req); } @@ -56,14 +67,14 @@ export class DefaultOAuthInterceptor implements HttpInterceptor { return merge( of(this.oAuthService.getAccessToken()).pipe( - filter(token => token ? true : false), + filter(token => (token ? true : false)) ), this.oAuthService.events.pipe( filter(e => e.type === 'token_received'), timeout(this.oAuthService.waitForTokenInMsec || 0), catchError(_ => of(null)), // timeout is not an error - map(_ => this.oAuthService.getAccessToken()), - ), + map(_ => this.oAuthService.getAccessToken()) + ) ).pipe( take(1), mergeMap(token => { @@ -76,7 +87,7 @@ export class DefaultOAuthInterceptor implements HttpInterceptor { return next .handle(req) .pipe(catchError(err => this.errorHandler.handleError(err))); - }), + }) ); } } diff --git a/projects/lib/src/oauth-service.ts b/projects/lib/src/oauth-service.ts index 3119d627..922b2122 100644 --- a/projects/lib/src/oauth-service.ts +++ b/projects/lib/src/oauth-service.ts @@ -1,28 +1,36 @@ import { Injectable, NgZone, Optional, OnDestroy, Inject } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable, Subject, Subscription, of, race, from } from 'rxjs'; -import { filter, delay, first, tap, map, switchMap, debounceTime } from 'rxjs/operators'; +import { + filter, + delay, + first, + tap, + map, + switchMap, + debounceTime +} from 'rxjs/operators'; import { DOCUMENT } from '@angular/common'; import { - ValidationHandler, - ValidationParams + ValidationHandler, + ValidationParams } from './token-validation/validation-handler'; import { UrlHelperService } from './url-helper.service'; import { - OAuthEvent, - OAuthInfoEvent, - OAuthErrorEvent, - OAuthSuccessEvent + OAuthEvent, + OAuthInfoEvent, + OAuthErrorEvent, + OAuthSuccessEvent } from './events'; import { - OAuthLogger, - OAuthStorage, - LoginOptions, - ParsedIdToken, - OidcDiscoveryDoc, - TokenResponse, - UserInfo + OAuthLogger, + OAuthStorage, + LoginOptions, + ParsedIdToken, + OidcDiscoveryDoc, + TokenResponse, + UserInfo } from './types'; import { b64DecodeUnicode, base64UrlEncode } from './base64-helper'; import { AuthConfig } from './auth.config'; @@ -36,2392 +44,2489 @@ import { HashHandler } from './token-validation/hash-handler'; */ @Injectable() export class OAuthService extends AuthConfig implements OnDestroy { - // Extending AuthConfig ist just for LEGACY reasons - // to not break existing code. - - /** - * The ValidationHandler used to validate received - * id_tokens. - */ - public tokenValidationHandler: ValidationHandler; - - /** - * @internal - * Deprecated: use property events instead - */ - public discoveryDocumentLoaded = false; - - /** - * @internal - * Deprecated: use property events instead - */ - public discoveryDocumentLoaded$: Observable; - - /** - * Informs about events, like token_received or token_expires. - * See the string enum EventType for a full list of event types. - */ - public events: Observable; - - /** - * The received (passed around) state, when logging - * in with implicit flow. - */ - public state?= ''; - - protected eventsSubject: Subject = new Subject(); - protected discoveryDocumentLoadedSubject: Subject = new Subject(); - protected silentRefreshPostMessageEventListener: EventListener; - protected grantTypesSupported: Array = []; - protected _storage: OAuthStorage; - protected accessTokenTimeoutSubscription: Subscription; - protected idTokenTimeoutSubscription: Subscription; - protected tokenReceivedSubscription: Subscription; - protected sessionCheckEventListener: EventListener; - protected jwksUri: string; - protected sessionCheckTimer: any; - protected silentRefreshSubject: string; - protected inImplicitFlow = false; - - - protected saveNoncesInLocalStorage = false; - - constructor( - protected ngZone: NgZone, - protected http: HttpClient, - @Optional() storage: OAuthStorage, - @Optional() tokenValidationHandler: ValidationHandler, - @Optional() protected config: AuthConfig, - protected urlHelper: UrlHelperService, - protected logger: OAuthLogger, - @Optional() protected crypto: HashHandler, - @Inject(DOCUMENT) private document: Document, + // Extending AuthConfig ist just for LEGACY reasons + // to not break existing code. + + /** + * The ValidationHandler used to validate received + * id_tokens. + */ + public tokenValidationHandler: ValidationHandler; + + /** + * @internal + * Deprecated: use property events instead + */ + public discoveryDocumentLoaded = false; + + /** + * @internal + * Deprecated: use property events instead + */ + public discoveryDocumentLoaded$: Observable; + + /** + * Informs about events, like token_received or token_expires. + * See the string enum EventType for a full list of event types. + */ + public events: Observable; + + /** + * The received (passed around) state, when logging + * in with implicit flow. + */ + public state? = ''; + + protected eventsSubject: Subject = new Subject(); + protected discoveryDocumentLoadedSubject: Subject< + OidcDiscoveryDoc + > = new Subject(); + protected silentRefreshPostMessageEventListener: EventListener; + protected grantTypesSupported: Array = []; + protected _storage: OAuthStorage; + protected accessTokenTimeoutSubscription: Subscription; + protected idTokenTimeoutSubscription: Subscription; + protected tokenReceivedSubscription: Subscription; + protected sessionCheckEventListener: EventListener; + protected jwksUri: string; + protected sessionCheckTimer: any; + protected silentRefreshSubject: string; + protected inImplicitFlow = false; + + protected saveNoncesInLocalStorage = false; + + constructor( + protected ngZone: NgZone, + protected http: HttpClient, + @Optional() storage: OAuthStorage, + @Optional() tokenValidationHandler: ValidationHandler, + @Optional() protected config: AuthConfig, + protected urlHelper: UrlHelperService, + protected logger: OAuthLogger, + @Optional() protected crypto: HashHandler, + @Inject(DOCUMENT) private document: Document + ) { + super(); + + this.debug('angular-oauth2-oidc v8-beta'); + + this.discoveryDocumentLoaded$ = this.discoveryDocumentLoadedSubject.asObservable(); + this.events = this.eventsSubject.asObservable(); + + if (tokenValidationHandler) { + this.tokenValidationHandler = tokenValidationHandler; + } + + if (config) { + this.configure(config); + } + + try { + if (storage) { + this.setStorage(storage); + } else if (typeof sessionStorage !== 'undefined') { + this.setStorage(sessionStorage); + } + } catch (e) { + console.error( + 'No OAuthStorage provided and cannot access default (sessionStorage).' + + 'Consider providing a custom OAuthStorage implementation in your module.', + e + ); + } + + // in IE, sessionStorage does not always survive a redirect + if ( + typeof window !== 'undefined' && + typeof window['localStorage'] !== 'undefined' ) { - super(); + const ua = window?.navigator?.userAgent; + const msie = ua?.includes('MSIE ') || ua?.includes('Trident'); - this.debug('angular-oauth2-oidc v8-beta'); - - this.discoveryDocumentLoaded$ = this.discoveryDocumentLoadedSubject.asObservable(); - this.events = this.eventsSubject.asObservable(); + if (msie) { + this.saveNoncesInLocalStorage = true; + } + } - if (tokenValidationHandler) { - this.tokenValidationHandler = tokenValidationHandler; + this.setupRefreshTimer(); + } + + /** + * Use this method to configure the service + * @param config the configuration + */ + public configure(config: AuthConfig): void { + // For the sake of downward compatibility with + // original configuration API + Object.assign(this, new AuthConfig(), config); + + this.config = Object.assign({} as AuthConfig, new AuthConfig(), config); + + if (this.sessionChecksEnabled) { + this.setupSessionCheck(); + } + + this.configChanged(); + } + + protected configChanged(): void { + this.setupRefreshTimer(); + } + + public restartSessionChecksIfStillLoggedIn(): void { + if (this.hasValidIdToken()) { + this.initSessionCheck(); + } + } + + protected restartRefreshTimerIfStillLoggedIn(): void { + this.setupExpirationTimers(); + } + + protected setupSessionCheck(): void { + this.events.pipe(filter(e => e.type === 'token_received')).subscribe(e => { + this.initSessionCheck(); + }); + } + + /** + * Will setup up silent refreshing for when the token is + * about to expire. When the user is logged out via this.logOut method, the + * silent refreshing will pause and not refresh the tokens until the user is + * logged back in via receiving a new token. + * @param params Additional parameter to pass + * @param listenTo Setup automatic refresh of a specific token type + */ + public setupAutomaticSilentRefresh( + params: object = {}, + listenTo?: 'access_token' | 'id_token' | 'any', + noPrompt = true + ): void { + let shouldRunSilentRefresh = true; + this.events + .pipe( + tap(e => { + if (e.type === 'token_received') { + shouldRunSilentRefresh = true; + } else if (e.type === 'logout') { + shouldRunSilentRefresh = false; + } + }), + filter(e => e.type === 'token_expires'), + debounceTime(1000) + ) + .subscribe(e => { + const event = e as OAuthInfoEvent; + if ( + (listenTo == null || listenTo === 'any' || event.info === listenTo) && + shouldRunSilentRefresh + ) { + // this.silentRefresh(params, noPrompt).catch(_ => { + this.refreshInternal(params, noPrompt).catch(_ => { + this.debug('Automatic silent refresh did not work'); + }); } + }); - if (config) { - this.configure(config); + this.restartRefreshTimerIfStillLoggedIn(); + } + + protected refreshInternal( + params, + noPrompt + ): Promise { + if (!this.useSilentRefresh && this.responseType === 'code') { + return this.refreshToken(); + } else { + return this.silentRefresh(params, noPrompt); + } + } + + /** + * Convenience method that first calls `loadDiscoveryDocument(...)` and + * directly chains using the `then(...)` part of the promise to call + * the `tryLogin(...)` method. + * + * @param options LoginOptions to pass through to `tryLogin(...)` + */ + public loadDiscoveryDocumentAndTryLogin( + options: LoginOptions = null + ): Promise { + return this.loadDiscoveryDocument().then(doc => { + return this.tryLogin(options); + }); + } + + /** + * Convenience method that first calls `loadDiscoveryDocumentAndTryLogin(...)` + * and if then chains to `initLoginFlow()`, but only if there is no valid + * IdToken or no valid AccessToken. + * + * @param options LoginOptions to pass through to `tryLogin(...)` + */ + public loadDiscoveryDocumentAndLogin( + options: LoginOptions & { state?: string } = null + ): Promise { + if (!options) { + options = { state: '' }; + } + return this.loadDiscoveryDocumentAndTryLogin(options).then(_ => { + if (!this.hasValidIdToken() || !this.hasValidAccessToken()) { + if (this.responseType === 'code') { + this.initCodeFlow(); + } else { + this.initImplicitFlow(); } + return false; + } else { + return true; + } + }); + } - try { - if (storage) { - this.setStorage(storage); - } else if (typeof sessionStorage !== 'undefined') { - this.setStorage(sessionStorage); - } - } catch (e) { - - console.error( - 'No OAuthStorage provided and cannot access default (sessionStorage).' - + 'Consider providing a custom OAuthStorage implementation in your module.', - e - ); - } + protected debug(...args): void { + if (this.showDebugInformation) { + this.logger.debug.apply(this.logger, args); + } + } - // in IE, sessionStorage does not always survive a redirect - if (typeof window !== 'undefined' && - typeof window['localStorage'] !== 'undefined') { - const ua = window?.navigator?.userAgent; - const msie = ua?.includes('MSIE ') || ua?.includes('Trident'); + protected validateUrlFromDiscoveryDocument(url: string): string[] { + const errors: string[] = []; + const httpsCheck = this.validateUrlForHttps(url); + const issuerCheck = this.validateUrlAgainstIssuer(url); - if (msie) { - this.saveNoncesInLocalStorage = true; - } - } + if (!httpsCheck) { + errors.push( + 'https for all urls required. Also for urls received by discovery.' + ); + } - this.setupRefreshTimer(); + if (!issuerCheck) { + errors.push( + 'Every url in discovery document has to start with the issuer url.' + + 'Also see property strictDiscoveryDocumentValidation.' + ); } - /** - * Use this method to configure the service - * @param config the configuration - */ - public configure(config: AuthConfig): void { - // For the sake of downward compatibility with - // original configuration API - Object.assign(this, new AuthConfig(), config); + return errors; + } - this.config = Object.assign({} as AuthConfig, new AuthConfig(), config); + protected validateUrlForHttps(url: string): boolean { + if (!url) { + return true; + } - if (this.sessionChecksEnabled) { - this.setupSessionCheck(); - } + const lcUrl = url.toLowerCase(); - this.configChanged(); + if (this.requireHttps === false) { + return true; } - protected configChanged(): void { - this.setupRefreshTimer(); + if ( + (lcUrl.match(/^http:\/\/localhost($|[:\/])/) || + lcUrl.match(/^http:\/\/localhost($|[:\/])/)) && + this.requireHttps === 'remoteOnly' + ) { + return true; } - public restartSessionChecksIfStillLoggedIn(): void { - if (this.hasValidIdToken()) { - this.initSessionCheck(); - } - } + return lcUrl.startsWith('https://'); + } - protected restartRefreshTimerIfStillLoggedIn(): void { - this.setupExpirationTimers(); + protected assertUrlNotNullAndCorrectProtocol( + url: string | undefined, + description: string + ) { + if (!url) { + throw new Error(`'${description}' should not be null`); + } + if (!this.validateUrlForHttps(url)) { + throw new Error( + `'${description}' must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).` + ); } + } - protected setupSessionCheck(): void { - this.events.pipe(filter(e => e.type === 'token_received')).subscribe(e => { - this.initSessionCheck(); - }); + protected validateUrlAgainstIssuer(url: string) { + if (!this.strictDiscoveryDocumentValidation) { + return true; + } + if (!url) { + return true; } + return url.toLowerCase().startsWith(this.issuer.toLowerCase()); + } - /** - * Will setup up silent refreshing for when the token is - * about to expire. When the user is logged out via this.logOut method, the - * silent refreshing will pause and not refresh the tokens until the user is - * logged back in via receiving a new token. - * @param params Additional parameter to pass - * @param listenTo Setup automatic refresh of a specific token type - */ - public setupAutomaticSilentRefresh(params: object = {}, listenTo?: 'access_token' | 'id_token' | 'any', noPrompt = true): void { - let shouldRunSilentRefresh = true; - this.events.pipe( - tap((e) => { - if (e.type === 'token_received') { - shouldRunSilentRefresh = true; - } else if (e.type === 'logout') { - shouldRunSilentRefresh = false; - } - }), - filter(e => e.type === 'token_expires'), - debounceTime(1000), - ).subscribe(e => { - const event = e as OAuthInfoEvent; - if ((listenTo == null || listenTo === 'any' || event.info === listenTo) && shouldRunSilentRefresh) { - // this.silentRefresh(params, noPrompt).catch(_ => { - this.refreshInternal(params, noPrompt).catch(_ => { - this.debug('Automatic silent refresh did not work'); - }); - } - }); + protected setupRefreshTimer(): void { + if (typeof window === 'undefined') { + this.debug('timer not supported on this plattform'); + return; + } - this.restartRefreshTimerIfStillLoggedIn(); + if (this.hasValidIdToken() || this.hasValidAccessToken()) { + this.clearAccessTokenTimer(); + this.clearIdTokenTimer(); + this.setupExpirationTimers(); } - protected refreshInternal(params, noPrompt): Promise { + if (this.tokenReceivedSubscription) + this.tokenReceivedSubscription.unsubscribe(); - if (!this.useSilentRefresh && this.responseType === 'code') { - return this.refreshToken(); - } else { - return this.silentRefresh(params, noPrompt); - } - } + this.tokenReceivedSubscription = this.events + .pipe(filter(e => e.type === 'token_received')) + .subscribe(_ => { + this.clearAccessTokenTimer(); + this.clearIdTokenTimer(); + this.setupExpirationTimers(); + }); + } - /** - * Convenience method that first calls `loadDiscoveryDocument(...)` and - * directly chains using the `then(...)` part of the promise to call - * the `tryLogin(...)` method. - * - * @param options LoginOptions to pass through to `tryLogin(...)` - */ - public loadDiscoveryDocumentAndTryLogin(options: LoginOptions = null): Promise { - return this.loadDiscoveryDocument().then(doc => { - return this.tryLogin(options); - }); + protected setupExpirationTimers(): void { + if (this.hasValidAccessToken()) { + this.setupAccessTokenTimer(); } - /** - * Convenience method that first calls `loadDiscoveryDocumentAndTryLogin(...)` - * and if then chains to `initLoginFlow()`, but only if there is no valid - * IdToken or no valid AccessToken. - * - * @param options LoginOptions to pass through to `tryLogin(...)` - */ - public loadDiscoveryDocumentAndLogin(options: LoginOptions & { state?: string } = null): Promise { - if (!options) { - options = { state: '' }; - } - return this.loadDiscoveryDocumentAndTryLogin(options).then(_ => { - if (!this.hasValidIdToken() || !this.hasValidAccessToken()) { - if (this.responseType === 'code') { - this.initCodeFlow(); - } else { - this.initImplicitFlow(); - } - return false; - } else { - return true; - } - }); + if (this.hasValidIdToken()) { + this.setupIdTokenTimer(); } + } - protected debug(...args): void { - if (this.showDebugInformation) { - this.logger.debug.apply(this.logger, args); - } - } + protected setupAccessTokenTimer(): void { + const expiration = this.getAccessTokenExpiration(); + const storedAt = this.getAccessTokenStoredAt(); + const timeout = this.calcTimeout(storedAt, expiration); - protected validateUrlFromDiscoveryDocument(url: string): string[] { - const errors: string[] = []; - const httpsCheck = this.validateUrlForHttps(url); - const issuerCheck = this.validateUrlAgainstIssuer(url); + this.ngZone.runOutsideAngular(() => { + this.accessTokenTimeoutSubscription = of( + new OAuthInfoEvent('token_expires', 'access_token') + ) + .pipe(delay(timeout)) + .subscribe(e => { + this.ngZone.run(() => { + this.eventsSubject.next(e); + }); + }); + }); + } + + protected setupIdTokenTimer(): void { + const expiration = this.getIdTokenExpiration(); + const storedAt = this.getIdTokenStoredAt(); + const timeout = this.calcTimeout(storedAt, expiration); + + this.ngZone.runOutsideAngular(() => { + this.idTokenTimeoutSubscription = of( + new OAuthInfoEvent('token_expires', 'id_token') + ) + .pipe(delay(timeout)) + .subscribe(e => { + this.ngZone.run(() => { + this.eventsSubject.next(e); + }); + }); + }); + } + + protected clearAccessTokenTimer(): void { + if (this.accessTokenTimeoutSubscription) { + this.accessTokenTimeoutSubscription.unsubscribe(); + } + } + + protected clearIdTokenTimer(): void { + if (this.idTokenTimeoutSubscription) { + this.idTokenTimeoutSubscription.unsubscribe(); + } + } + + protected calcTimeout(storedAt: number, expiration: number): number { + const now = Date.now(); + const delta = + (expiration - storedAt) * this.timeoutFactor - (now - storedAt); + return Math.max(0, delta); + } + + /** + * DEPRECATED. Use a provider for OAuthStorage instead: + * + * { provide: OAuthStorage, useFactory: oAuthStorageFactory } + * export function oAuthStorageFactory(): OAuthStorage { return localStorage; } + * Sets a custom storage used to store the received + * tokens on client side. By default, the browser's + * sessionStorage is used. + * @ignore + * + * @param storage + */ + public setStorage(storage: OAuthStorage): void { + this._storage = storage; + this.configChanged(); + } + + /** + * Loads the discovery document to configure most + * properties of this service. The url of the discovery + * document is infered from the issuer's url according + * to the OpenId Connect spec. To use another url you + * can pass it to to optional parameter fullUrl. + * + * @param fullUrl + */ + public loadDiscoveryDocument( + fullUrl: string = null + ): Promise { + return new Promise((resolve, reject) => { + if (!fullUrl) { + fullUrl = this.issuer || ''; + if (!fullUrl.endsWith('/')) { + fullUrl += '/'; + } + fullUrl += '.well-known/openid-configuration'; + } - if (!httpsCheck) { - errors.push( - 'https for all urls required. Also for urls received by discovery.' - ); - } + if (!this.validateUrlForHttps(fullUrl)) { + reject( + "issuer must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)." + ); + return; + } - if (!issuerCheck) { - errors.push( - 'Every url in discovery document has to start with the issuer url.' + - 'Also see property strictDiscoveryDocumentValidation.' + this.http.get(fullUrl).subscribe( + doc => { + if (!this.validateDiscoveryDocument(doc)) { + this.eventsSubject.next( + new OAuthErrorEvent('discovery_document_validation_error', null) ); - } + reject('discovery_document_validation_error'); + return; + } - return errors; - } + this.loginUrl = doc.authorization_endpoint; + this.logoutUrl = doc.end_session_endpoint || this.logoutUrl; + this.grantTypesSupported = doc.grant_types_supported; + this.issuer = doc.issuer; + this.tokenEndpoint = doc.token_endpoint; + this.userinfoEndpoint = + doc.userinfo_endpoint || this.userinfoEndpoint; + this.jwksUri = doc.jwks_uri; + this.sessionCheckIFrameUrl = + doc.check_session_iframe || this.sessionCheckIFrameUrl; + + this.discoveryDocumentLoaded = true; + this.discoveryDocumentLoadedSubject.next(doc); + + if (this.sessionChecksEnabled) { + this.restartSessionChecksIfStillLoggedIn(); + } - protected validateUrlForHttps(url: string): boolean { - if (!url) { - return true; - } + this.loadJwks() + .then(jwks => { + const result: object = { + discoveryDocument: doc, + jwks: jwks + }; + + const event = new OAuthSuccessEvent( + 'discovery_document_loaded', + result + ); + this.eventsSubject.next(event); + resolve(event); + return; + }) + .catch(err => { + this.eventsSubject.next( + new OAuthErrorEvent('discovery_document_load_error', err) + ); + reject(err); + return; + }); + }, + err => { + this.logger.error('error loading discovery document', err); + this.eventsSubject.next( + new OAuthErrorEvent('discovery_document_load_error', err) + ); + reject(err); + } + ); + }); + } + + protected loadJwks(): Promise { + return new Promise((resolve, reject) => { + if (this.jwksUri) { + this.http.get(this.jwksUri).subscribe( + jwks => { + this.jwks = jwks; + this.eventsSubject.next( + new OAuthSuccessEvent('discovery_document_loaded') + ); + resolve(jwks); + }, + err => { + this.logger.error('error loading jwks', err); + this.eventsSubject.next( + new OAuthErrorEvent('jwks_load_error', err) + ); + reject(err); + } + ); + } else { + resolve(null); + } + }); + } + + protected validateDiscoveryDocument(doc: OidcDiscoveryDoc): boolean { + let errors: string[]; + + if (!this.skipIssuerCheck && doc.issuer !== this.issuer) { + this.logger.error( + 'invalid issuer in discovery document', + 'expected: ' + this.issuer, + 'current: ' + doc.issuer + ); + return false; + } + + errors = this.validateUrlFromDiscoveryDocument(doc.authorization_endpoint); + if (errors.length > 0) { + this.logger.error( + 'error validating authorization_endpoint in discovery document', + errors + ); + return false; + } + + errors = this.validateUrlFromDiscoveryDocument(doc.end_session_endpoint); + if (errors.length > 0) { + this.logger.error( + 'error validating end_session_endpoint in discovery document', + errors + ); + return false; + } + + errors = this.validateUrlFromDiscoveryDocument(doc.token_endpoint); + if (errors.length > 0) { + this.logger.error( + 'error validating token_endpoint in discovery document', + errors + ); + } + + errors = this.validateUrlFromDiscoveryDocument(doc.userinfo_endpoint); + if (errors.length > 0) { + this.logger.error( + 'error validating userinfo_endpoint in discovery document', + errors + ); + return false; + } + + errors = this.validateUrlFromDiscoveryDocument(doc.jwks_uri); + if (errors.length > 0) { + this.logger.error( + 'error validating jwks_uri in discovery document', + errors + ); + return false; + } + + if (this.sessionChecksEnabled && !doc.check_session_iframe) { + this.logger.warn( + 'sessionChecksEnabled is activated but discovery document' + + ' does not contain a check_session_iframe field' + ); + } + + return true; + } + + /** + * Uses password flow to exchange userName and password for an + * access_token. After receiving the access_token, this method + * uses it to query the userinfo endpoint in order to get information + * about the user in question. + * + * When using this, make sure that the property oidc is set to false. + * Otherwise stricter validations take place that make this operation + * fail. + * + * @param userName + * @param password + * @param headers Optional additional http-headers. + */ + public fetchTokenUsingPasswordFlowAndLoadUserProfile( + userName: string, + password: string, + headers: HttpHeaders = new HttpHeaders() + ): Promise { + return this.fetchTokenUsingPasswordFlow( + userName, + password, + headers + ).then(() => this.loadUserProfile()); + } + + /** + * Loads the user profile by accessing the user info endpoint defined by OpenId Connect. + * + * When using this with OAuth2 password flow, make sure that the property oidc is set to false. + * Otherwise stricter validations take place that make this operation fail. + */ + public loadUserProfile(): Promise { + if (!this.hasValidAccessToken()) { + throw new Error('Can not load User Profile without access_token'); + } + if (!this.validateUrlForHttps(this.userinfoEndpoint)) { + throw new Error( + "userinfoEndpoint must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)." + ); + } + + return new Promise((resolve, reject) => { + const headers = new HttpHeaders().set( + 'Authorization', + 'Bearer ' + this.getAccessToken() + ); + + this.http + .get(this.userinfoEndpoint, { headers }) + .subscribe( + info => { + this.debug('userinfo received', info); + + const existingClaims = this.getIdentityClaims() || {}; + + if (!this.skipSubjectCheck) { + if ( + this.oidc && + (!existingClaims['sub'] || info.sub !== existingClaims['sub']) + ) { + const err = + 'if property oidc is true, the received user-id (sub) has to be the user-id ' + + 'of the user that has logged in with oidc.\n' + + 'if you are not using oidc but just oauth2 password flow set oidc to false'; + + reject(err); + return; + } + } - const lcUrl = url.toLowerCase(); + info = Object.assign({}, existingClaims, info); - if (this.requireHttps === false) { - return true; - } + this._storage.setItem('id_token_claims_obj', JSON.stringify(info)); + this.eventsSubject.next( + new OAuthSuccessEvent('user_profile_loaded') + ); + resolve(info); + }, + err => { + this.logger.error('error loading user info', err); + this.eventsSubject.next( + new OAuthErrorEvent('user_profile_load_error', err) + ); + reject(err); + } + ); + }); + } + + /** + * Uses password flow to exchange userName and password for an access_token. + * @param userName + * @param password + * @param headers Optional additional http-headers. + */ + public fetchTokenUsingPasswordFlow( + userName: string, + password: string, + headers: HttpHeaders = new HttpHeaders() + ): Promise { + this.assertUrlNotNullAndCorrectProtocol( + this.tokenEndpoint, + 'tokenEndpoint' + ); + + return new Promise((resolve, reject) => { + /** + * A `HttpParameterCodec` that uses `encodeURIComponent` and `decodeURIComponent` to + * serialize and parse URL parameter keys and values. + * + * @stable + */ + let params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() }) + .set('grant_type', 'password') + .set('scope', this.scope) + .set('username', userName) + .set('password', password); + + if (this.useHttpBasicAuth) { + const header = btoa(`${this.clientId}:${this.dummyClientSecret}`); + headers = headers.set('Authorization', 'Basic ' + header); + } - if ( - (lcUrl.match(/^http:\/\/localhost($|[:\/])/) || - lcUrl.match(/^http:\/\/localhost($|[:\/])/)) && - this.requireHttps === 'remoteOnly' - ) { - return true; - } + if (!this.useHttpBasicAuth) { + params = params.set('client_id', this.clientId); + } - return lcUrl.startsWith('https://'); - } + if (!this.useHttpBasicAuth && this.dummyClientSecret) { + params = params.set('client_secret', this.dummyClientSecret); + } - protected assertUrlNotNullAndCorrectProtocol(url: string | undefined, description: string) { - if (!url) { - throw new Error(`'${description}' should not be null`); - } - if (!this.validateUrlForHttps(url)) { - throw new Error(`'${description}' must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).`); + if (this.customQueryParams) { + for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { + params = params.set(key, this.customQueryParams[key]); } - } + } - protected validateUrlAgainstIssuer(url: string) { - if (!this.strictDiscoveryDocumentValidation) { - return true; - } - if (!url) { - return true; - } - return url.toLowerCase().startsWith(this.issuer.toLowerCase()); - } + headers = headers.set( + 'Content-Type', + 'application/x-www-form-urlencoded' + ); - protected setupRefreshTimer(): void { - if (typeof window === 'undefined') { - this.debug('timer not supported on this plattform'); - return; - } + this.http + .post(this.tokenEndpoint, params, { headers }) + .subscribe( + tokenResponse => { + this.debug('tokenResponse', tokenResponse); + this.storeAccessTokenResponse( + tokenResponse.access_token, + tokenResponse.refresh_token, + tokenResponse.expires_in, + tokenResponse.scope, + this.extractRecognizedCustomParameters(tokenResponse) + ); - if (this.hasValidIdToken() || this.hasValidAccessToken()) { - this.clearAccessTokenTimer(); - this.clearIdTokenTimer(); - this.setupExpirationTimers(); - } + this.eventsSubject.next(new OAuthSuccessEvent('token_received')); + resolve(tokenResponse); + }, + err => { + this.logger.error('Error performing password flow', err); + this.eventsSubject.next(new OAuthErrorEvent('token_error', err)); + reject(err); + } + ); + }); + } + + /** + * Refreshes the token using a refresh_token. + * This does not work for implicit flow, b/c + * there is no refresh_token in this flow. + * A solution for this is provided by the + * method silentRefresh. + */ + public refreshToken(): Promise { + this.assertUrlNotNullAndCorrectProtocol( + this.tokenEndpoint, + 'tokenEndpoint' + ); + + return new Promise((resolve, reject) => { + let params = new HttpParams() + .set('grant_type', 'refresh_token') + .set('scope', this.scope) + .set('refresh_token', this._storage.getItem('refresh_token')); + + let headers = new HttpHeaders().set( + 'Content-Type', + 'application/x-www-form-urlencoded' + ); + + if (this.useHttpBasicAuth) { + const header = btoa(`${this.clientId}:${this.dummyClientSecret}`); + headers = headers.set('Authorization', 'Basic ' + header); + } - if (this.tokenReceivedSubscription) - this.tokenReceivedSubscription.unsubscribe(); + if (!this.useHttpBasicAuth) { + params = params.set('client_id', this.clientId); + } - this.tokenReceivedSubscription = this.events.pipe(filter(e => e.type === 'token_received')).subscribe(_ => { - this.clearAccessTokenTimer(); - this.clearIdTokenTimer(); - this.setupExpirationTimers(); - }); - } + if (!this.useHttpBasicAuth && this.dummyClientSecret) { + params = params.set('client_secret', this.dummyClientSecret); + } - protected setupExpirationTimers(): void { - if (this.hasValidAccessToken()) { - this.setupAccessTokenTimer(); + if (this.customQueryParams) { + for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { + params = params.set(key, this.customQueryParams[key]); } + } + + this.http + .post(this.tokenEndpoint, params, { headers }) + .pipe( + switchMap(tokenResponse => { + if (tokenResponse.id_token) { + return from( + this.processIdToken( + tokenResponse.id_token, + tokenResponse.access_token, + true + ) + ).pipe( + tap(result => this.storeIdToken(result)), + map(_ => tokenResponse) + ); + } else { + return of(tokenResponse); + } + }) + ) + .subscribe( + tokenResponse => { + this.debug('refresh tokenResponse', tokenResponse); + this.storeAccessTokenResponse( + tokenResponse.access_token, + tokenResponse.refresh_token, + tokenResponse.expires_in, + tokenResponse.scope, + this.extractRecognizedCustomParameters(tokenResponse) + ); + this.eventsSubject.next(new OAuthSuccessEvent('token_received')); + this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); + resolve(tokenResponse); + }, + err => { + this.logger.error('Error refreshing token', err); + this.eventsSubject.next( + new OAuthErrorEvent('token_refresh_error', err) + ); + reject(err); + } + ); + }); + } - if (this.hasValidIdToken()) { - this.setupIdTokenTimer(); - } + protected removeSilentRefreshEventListener(): void { + if (this.silentRefreshPostMessageEventListener) { + window.removeEventListener( + 'message', + this.silentRefreshPostMessageEventListener + ); + this.silentRefreshPostMessageEventListener = null; } + } - protected setupAccessTokenTimer(): void { + protected setupSilentRefreshEventListener(): void { + this.removeSilentRefreshEventListener(); - const expiration = this.getAccessTokenExpiration(); - const storedAt = this.getAccessTokenStoredAt(); - const timeout = this.calcTimeout(storedAt, expiration); + this.silentRefreshPostMessageEventListener = (e: MessageEvent) => { + const message = this.processMessageEventMessage(e); - this.ngZone.runOutsideAngular(() => { - this.accessTokenTimeoutSubscription = of( - new OAuthInfoEvent('token_expires', 'access_token') - ) - .pipe(delay(timeout)) - .subscribe(e => { - this.ngZone.run(() => { - this.eventsSubject.next(e); - }); - }); - }); - } + this.tryLogin({ + customHashFragment: message, + preventClearHashAfterLogin: true, + customRedirectUri: this.silentRefreshRedirectUri || this.redirectUri + }).catch(err => this.debug('tryLogin during silent refresh failed', err)); + }; - protected setupIdTokenTimer(): void { + window.addEventListener( + 'message', + this.silentRefreshPostMessageEventListener + ); + } - const expiration = this.getIdTokenExpiration(); - const storedAt = this.getIdTokenStoredAt(); - const timeout = this.calcTimeout(storedAt, expiration); + /** + * Performs a silent refresh for implicit flow. + * Use this method to get new tokens when/before + * the existing tokens expire. + */ + public silentRefresh( + params: object = {}, + noPrompt = true + ): Promise { + const claims: object = this.getIdentityClaims() || {}; - this.ngZone.runOutsideAngular(() => { - this.idTokenTimeoutSubscription = of( - new OAuthInfoEvent('token_expires', 'id_token') - ) - .pipe(delay(timeout)) - .subscribe(e => { - this.ngZone.run(() => { - this.eventsSubject.next(e); - }); - }); - }); + if (this.useIdTokenHintForSilentRefresh && this.hasValidIdToken()) { + params['id_token_hint'] = this.getIdToken(); } - protected clearAccessTokenTimer(): void { - if (this.accessTokenTimeoutSubscription) { - this.accessTokenTimeoutSubscription.unsubscribe(); - } + if (!this.validateUrlForHttps(this.loginUrl)) { + throw new Error( + "loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)." + ); } - protected clearIdTokenTimer(): void { - if (this.idTokenTimeoutSubscription) { - this.idTokenTimeoutSubscription.unsubscribe(); - } + if (typeof document === 'undefined') { + throw new Error('silent refresh is not supported on this platform'); } - protected calcTimeout(storedAt: number, expiration: number): number { - const now = Date.now(); - const delta = (expiration - storedAt) * this.timeoutFactor - (now - storedAt); - return Math.max(0, delta); + const existingIframe = document.getElementById( + this.silentRefreshIFrameName + ); + + if (existingIframe) { + document.body.removeChild(existingIframe); } - /** - * DEPRECATED. Use a provider for OAuthStorage instead: - * - * { provide: OAuthStorage, useFactory: oAuthStorageFactory } - * export function oAuthStorageFactory(): OAuthStorage { return localStorage; } - * Sets a custom storage used to store the received - * tokens on client side. By default, the browser's - * sessionStorage is used. - * @ignore - * - * @param storage - */ - public setStorage(storage: OAuthStorage): void { - this._storage = storage; - this.configChanged(); - } - - /** - * Loads the discovery document to configure most - * properties of this service. The url of the discovery - * document is infered from the issuer's url according - * to the OpenId Connect spec. To use another url you - * can pass it to to optional parameter fullUrl. - * - * @param fullUrl - */ - public loadDiscoveryDocument(fullUrl: string = null): Promise { - return new Promise((resolve, reject) => { - if (!fullUrl) { - fullUrl = this.issuer || ''; - if (!fullUrl.endsWith('/')) { - fullUrl += '/'; - } - fullUrl += '.well-known/openid-configuration'; - } + this.silentRefreshSubject = claims['sub']; - if (!this.validateUrlForHttps(fullUrl)) { - reject('issuer must use HTTPS (with TLS), or config value for property \'requireHttps\' must be set to \'false\' and allow HTTP (without TLS).'); - return; - } + const iframe = document.createElement('iframe'); + iframe.id = this.silentRefreshIFrameName; - this.http.get(fullUrl).subscribe( - doc => { - if (!this.validateDiscoveryDocument(doc)) { - this.eventsSubject.next( - new OAuthErrorEvent('discovery_document_validation_error', null) - ); - reject('discovery_document_validation_error'); - return; - } - - this.loginUrl = doc.authorization_endpoint; - this.logoutUrl = doc.end_session_endpoint || this.logoutUrl; - this.grantTypesSupported = doc.grant_types_supported; - this.issuer = doc.issuer; - this.tokenEndpoint = doc.token_endpoint; - this.userinfoEndpoint = doc.userinfo_endpoint || this.userinfoEndpoint; - this.jwksUri = doc.jwks_uri; - this.sessionCheckIFrameUrl = doc.check_session_iframe || this.sessionCheckIFrameUrl; - - this.discoveryDocumentLoaded = true; - this.discoveryDocumentLoadedSubject.next(doc); - - if (this.sessionChecksEnabled) { - this.restartSessionChecksIfStillLoggedIn(); - } - - this.loadJwks() - .then(jwks => { - const result: object = { - discoveryDocument: doc, - jwks: jwks - }; - - const event = new OAuthSuccessEvent( - 'discovery_document_loaded', - result - ); - this.eventsSubject.next(event); - resolve(event); - return; - }) - .catch(err => { - this.eventsSubject.next( - new OAuthErrorEvent('discovery_document_load_error', err) - ); - reject(err); - return; - }); - }, - err => { - this.logger.error('error loading discovery document', err); - this.eventsSubject.next( - new OAuthErrorEvent('discovery_document_load_error', err) - ); - reject(err); - } - ); - }); - } + this.setupSilentRefreshEventListener(); + + const redirectUri = this.silentRefreshRedirectUri || this.redirectUri; + this.createLoginUrl(null, null, redirectUri, noPrompt, params).then(url => { + iframe.setAttribute('src', url); - protected loadJwks(): Promise { - return new Promise((resolve, reject) => { - if (this.jwksUri) { - this.http.get(this.jwksUri).subscribe( - jwks => { - this.jwks = jwks; - this.eventsSubject.next( - new OAuthSuccessEvent('discovery_document_loaded') - ); - resolve(jwks); - }, - err => { - this.logger.error('error loading jwks', err); - this.eventsSubject.next( - new OAuthErrorEvent('jwks_load_error', err) - ); - reject(err); - } - ); + if (!this.silentRefreshShowIFrame) { + iframe.style['display'] = 'none'; + } + document.body.appendChild(iframe); + }); + + const errors = this.events.pipe( + filter(e => e instanceof OAuthErrorEvent), + first() + ); + const success = this.events.pipe( + filter(e => e.type === 'token_received'), + first() + ); + const timeout = of( + new OAuthErrorEvent('silent_refresh_timeout', null) + ).pipe(delay(this.silentRefreshTimeout)); + + return race([errors, success, timeout]) + .pipe( + map(e => { + if (e instanceof OAuthErrorEvent) { + if (e.type === 'silent_refresh_timeout') { + this.eventsSubject.next(e); } else { - resolve(null); + e = new OAuthErrorEvent('silent_refresh_error', e); + this.eventsSubject.next(e); } - }); - } - - protected validateDiscoveryDocument(doc: OidcDiscoveryDoc): boolean { - let errors: string[]; - - if (!this.skipIssuerCheck && doc.issuer !== this.issuer) { - this.logger.error( - 'invalid issuer in discovery document', - 'expected: ' + this.issuer, - 'current: ' + doc.issuer - ); - return false; + throw e; + } else if (e.type === 'token_received') { + e = new OAuthSuccessEvent('silently_refreshed'); + this.eventsSubject.next(e); + } + return e; + }) + ) + .toPromise(); + } + + /** + * This method exists for backwards compatibility. + * {@link OAuthService#initLoginFlowInPopup} handles both code + * and implicit flows. + */ + public initImplicitFlowInPopup(options?: { + height?: number; + width?: number; + }) { + return this.initLoginFlowInPopup(options); + } + + public initLoginFlowInPopup(options?: { height?: number; width?: number }) { + options = options || {}; + return this.createLoginUrl( + null, + null, + this.silentRefreshRedirectUri, + false, + { + display: 'popup' + } + ).then(url => { + return new Promise((resolve, reject) => { + /** + * Error handling section + */ + const checkForPopupClosedInterval = 500; + let windowRef = window.open( + url, + '_blank', + this.calculatePopupFeatures(options) + ); + let checkForPopupClosedTimer: any; + const checkForPopupClosed = () => { + if (!windowRef || windowRef.closed) { + cleanup(); + reject(new OAuthErrorEvent('popup_closed', {})); + } + }; + if (!windowRef) { + reject(new OAuthErrorEvent('popup_blocked', {})); + } else { + checkForPopupClosedTimer = window.setInterval( + checkForPopupClosed, + checkForPopupClosedInterval + ); } - errors = this.validateUrlFromDiscoveryDocument(doc.authorization_endpoint); - if (errors.length > 0) { - this.logger.error( - 'error validating authorization_endpoint in discovery document', - errors - ); - return false; - } + const cleanup = () => { + window.clearInterval(checkForPopupClosedTimer); + window.removeEventListener('message', listener); + if (windowRef !== null) { + windowRef.close(); + } + windowRef = null; + }; - errors = this.validateUrlFromDiscoveryDocument(doc.end_session_endpoint); - if (errors.length > 0) { - this.logger.error( - 'error validating end_session_endpoint in discovery document', - errors - ); - return false; - } + const listener = (e: MessageEvent) => { + const message = this.processMessageEventMessage(e); - errors = this.validateUrlFromDiscoveryDocument(doc.token_endpoint); - if (errors.length > 0) { - this.logger.error( - 'error validating token_endpoint in discovery document', - errors + if (message && message !== null) { + this.tryLogin({ + customHashFragment: message, + preventClearHashAfterLogin: true, + customRedirectUri: this.silentRefreshRedirectUri + }).then( + () => { + cleanup(); + resolve(); + }, + err => { + cleanup(); + reject(err); + } ); - } + } else { + console.log('false event firing'); + } + }; - errors = this.validateUrlFromDiscoveryDocument(doc.userinfo_endpoint); - if (errors.length > 0) { - this.logger.error( - 'error validating userinfo_endpoint in discovery document', - errors - ); - return false; - } + window.addEventListener('message', listener); + }); + }); + } - errors = this.validateUrlFromDiscoveryDocument(doc.jwks_uri); - if (errors.length > 0) { - this.logger.error('error validating jwks_uri in discovery document', errors); - return false; - } + protected calculatePopupFeatures(options: { + height?: number; + width?: number; + }): string { + // Specify an static height and width and calculate centered position - if (this.sessionChecksEnabled && !doc.check_session_iframe) { - this.logger.warn( - 'sessionChecksEnabled is activated but discovery document' + - ' does not contain a check_session_iframe field' - ); - } + const height = options.height || 470; + const width = options.width || 500; + const left = window.screenLeft + (window.outerWidth - width) / 2; + const top = window.screenTop + (window.outerHeight - height) / 2; + return `location=no,toolbar=no,width=${width},height=${height},top=${top},left=${left}`; + } - return true; - } + protected processMessageEventMessage(e: MessageEvent): string { + let expectedPrefix = '#'; - /** - * Uses password flow to exchange userName and password for an - * access_token. After receiving the access_token, this method - * uses it to query the userinfo endpoint in order to get information - * about the user in question. - * - * When using this, make sure that the property oidc is set to false. - * Otherwise stricter validations take place that make this operation - * fail. - * - * @param userName - * @param password - * @param headers Optional additional http-headers. - */ - public fetchTokenUsingPasswordFlowAndLoadUserProfile( - userName: string, - password: string, - headers: HttpHeaders = new HttpHeaders() - ): Promise { - return this.fetchTokenUsingPasswordFlow(userName, password, headers).then( - () => this.loadUserProfile() - ); + if (this.silentRefreshMessagePrefix) { + expectedPrefix += this.silentRefreshMessagePrefix; } - /** - * Loads the user profile by accessing the user info endpoint defined by OpenId Connect. - * - * When using this with OAuth2 password flow, make sure that the property oidc is set to false. - * Otherwise stricter validations take place that make this operation fail. - */ - public loadUserProfile(): Promise { - if (!this.hasValidAccessToken()) { - throw new Error('Can not load User Profile without access_token'); - } - if (!this.validateUrlForHttps(this.userinfoEndpoint)) { - throw new Error('userinfoEndpoint must use HTTPS (with TLS), or config value for property \'requireHttps\' must be set to \'false\' and allow HTTP (without TLS).'); - } + if (!e || !e.data || typeof e.data !== 'string') { + return; + } - return new Promise((resolve, reject) => { - const headers = new HttpHeaders().set( - 'Authorization', - 'Bearer ' + this.getAccessToken() - ); + const prefixedMessage: string = e.data; - this.http.get(this.userinfoEndpoint, { headers }).subscribe( - info => { - this.debug('userinfo received', info); - - const existingClaims = this.getIdentityClaims() || {}; - - if (!this.skipSubjectCheck) { - if ( - this.oidc && - (!existingClaims['sub'] || info.sub !== existingClaims['sub']) - ) { - const err = - 'if property oidc is true, the received user-id (sub) has to be the user-id ' + - 'of the user that has logged in with oidc.\n' + - 'if you are not using oidc but just oauth2 password flow set oidc to false'; - - reject(err); - return; - } - } - - info = Object.assign({}, existingClaims, info); - - this._storage.setItem('id_token_claims_obj', JSON.stringify(info)); - this.eventsSubject.next(new OAuthSuccessEvent('user_profile_loaded')); - resolve(info); - }, - err => { - this.logger.error('error loading user info', err); - this.eventsSubject.next( - new OAuthErrorEvent('user_profile_load_error', err) - ); - reject(err); - } - ); - }); + if (!prefixedMessage.startsWith(expectedPrefix)) { + return; } - /** - * Uses password flow to exchange userName and password for an access_token. - * @param userName - * @param password - * @param headers Optional additional http-headers. - */ - public fetchTokenUsingPasswordFlow( - userName: string, - password: string, - headers: HttpHeaders = new HttpHeaders() - - ): Promise { - this.assertUrlNotNullAndCorrectProtocol(this.tokenEndpoint, 'tokenEndpoint'); - - return new Promise((resolve, reject) => { - /** - * A `HttpParameterCodec` that uses `encodeURIComponent` and `decodeURIComponent` to - * serialize and parse URL parameter keys and values. - * - * @stable - */ - let params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() }) - .set('grant_type', 'password') - .set('scope', this.scope) - .set('username', userName) - .set('password', password); - - if (this.useHttpBasicAuth) { - const header = btoa(`${this.clientId}:${this.dummyClientSecret}`); - headers = headers.set( - 'Authorization', - 'Basic ' + header); - } + return '#' + prefixedMessage.substr(expectedPrefix.length); + } - if (!this.useHttpBasicAuth) { - params = params.set('client_id', this.clientId); - } + protected canPerformSessionCheck(): boolean { + if (!this.sessionChecksEnabled) { + return false; + } + if (!this.sessionCheckIFrameUrl) { + console.warn( + 'sessionChecksEnabled is activated but there is no sessionCheckIFrameUrl' + ); + return false; + } + const sessionState = this.getSessionState(); + if (!sessionState) { + console.warn( + 'sessionChecksEnabled is activated but there is no session_state' + ); + return false; + } + if (typeof document === 'undefined') { + return false; + } - if (!this.useHttpBasicAuth && this.dummyClientSecret) { - params = params.set('client_secret', this.dummyClientSecret); - } + return true; + } - if (this.customQueryParams) { - for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { - params = params.set(key, this.customQueryParams[key]); - } - } + protected setupSessionCheckEventListener(): void { + this.removeSessionCheckEventListener(); - headers = headers.set( - 'Content-Type', - 'application/x-www-form-urlencoded' - ); + this.sessionCheckEventListener = (e: MessageEvent) => { + const origin = e.origin.toLowerCase(); + const issuer = this.issuer.toLowerCase(); - this.http - .post(this.tokenEndpoint, params, { headers }) - .subscribe( - tokenResponse => { - this.debug('tokenResponse', tokenResponse); - this.storeAccessTokenResponse( - tokenResponse.access_token, - tokenResponse.refresh_token, - tokenResponse.expires_in, - tokenResponse.scope, - this.extractRecognizedCustomParameters(tokenResponse) - ); - - this.eventsSubject.next(new OAuthSuccessEvent('token_received')); - resolve(tokenResponse); - }, - err => { - this.logger.error('Error performing password flow', err); - this.eventsSubject.next(new OAuthErrorEvent('token_error', err)); - reject(err); - } - ); - }); - } + this.debug('sessionCheckEventListener'); - /** - * Refreshes the token using a refresh_token. - * This does not work for implicit flow, b/c - * there is no refresh_token in this flow. - * A solution for this is provided by the - * method silentRefresh. - */ - public refreshToken(): Promise { - this.assertUrlNotNullAndCorrectProtocol(this.tokenEndpoint, 'tokenEndpoint'); - - return new Promise((resolve, reject) => { - let params = new HttpParams() - .set('grant_type', 'refresh_token') - .set('scope', this.scope) - .set('refresh_token', this._storage.getItem('refresh_token')); - - let headers = new HttpHeaders().set( - 'Content-Type', - 'application/x-www-form-urlencoded' - ); + if (!issuer.startsWith(origin)) { + this.debug( + 'sessionCheckEventListener', + 'wrong origin', + origin, + 'expected', + issuer, + 'event', + e + ); - if (this.useHttpBasicAuth) { - const header = btoa(`${this.clientId}:${this.dummyClientSecret}`); - headers = headers.set( - 'Authorization', - 'Basic ' + header); - } + return; + } - if (!this.useHttpBasicAuth) { - params = params.set('client_id', this.clientId); - } + // only run in Angular zone if it is 'changed' or 'error' + switch (e.data) { + case 'unchanged': + this.handleSessionUnchanged(); + break; + case 'changed': + this.ngZone.run(() => { + this.handleSessionChange(); + }); + break; + case 'error': + this.ngZone.run(() => { + this.handleSessionError(); + }); + break; + } - if (!this.useHttpBasicAuth && this.dummyClientSecret) { - params = params.set('client_secret', this.dummyClientSecret); - } + this.debug('got info from session check inframe', e); + }; + + // prevent Angular from refreshing the view on every message (runs in intervals) + this.ngZone.runOutsideAngular(() => { + window.addEventListener('message', this.sessionCheckEventListener); + }); + } + + protected handleSessionUnchanged(): void { + this.debug('session check', 'session unchanged'); + } + + protected handleSessionChange(): void { + this.eventsSubject.next(new OAuthInfoEvent('session_changed')); + this.stopSessionCheckTimer(); + + if (!this.useSilentRefresh && this.responseType === 'code') { + this.refreshToken() + .then(_ => { + this.debug('token refresh after session change worked'); + }) + .catch(_ => { + this.debug('token refresh did not work after session changed'); + this.eventsSubject.next(new OAuthInfoEvent('session_terminated')); + this.logOut(true); + }); + } else if (this.silentRefreshRedirectUri) { + this.silentRefresh().catch(_ => + this.debug('silent refresh failed after session changed') + ); + this.waitForSilentRefreshAfterSessionChange(); + } else { + this.eventsSubject.next(new OAuthInfoEvent('session_terminated')); + this.logOut(true); + } + } + + protected waitForSilentRefreshAfterSessionChange(): void { + this.events + .pipe( + filter( + (e: OAuthEvent) => + e.type === 'silently_refreshed' || + e.type === 'silent_refresh_timeout' || + e.type === 'silent_refresh_error' + ), + first() + ) + .subscribe(e => { + if (e.type !== 'silently_refreshed') { + this.debug('silent refresh did not work after session changed'); + this.eventsSubject.next(new OAuthInfoEvent('session_terminated')); + this.logOut(true); + } + }); + } - if (this.customQueryParams) { - for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { - params = params.set(key, this.customQueryParams[key]); - } - } + protected handleSessionError(): void { + this.stopSessionCheckTimer(); + this.eventsSubject.next(new OAuthInfoEvent('session_error')); + } - this.http - .post(this.tokenEndpoint, params, { headers }) - .pipe(switchMap(tokenResponse => { - if (tokenResponse.id_token) { - return from(this.processIdToken(tokenResponse.id_token, tokenResponse.access_token, true)) - .pipe( - tap(result => this.storeIdToken(result)), - map(_ => tokenResponse) - ); - } else { - return of(tokenResponse); - } - })) - .subscribe( - tokenResponse => { - this.debug('refresh tokenResponse', tokenResponse); - this.storeAccessTokenResponse( - tokenResponse.access_token, - tokenResponse.refresh_token, - tokenResponse.expires_in, - tokenResponse.scope, - this.extractRecognizedCustomParameters(tokenResponse) - ); - - this.eventsSubject.next(new OAuthSuccessEvent('token_received')); - this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); - resolve(tokenResponse); - }, - err => { - this.logger.error('Error refreshing token', err); - this.eventsSubject.next( - new OAuthErrorEvent('token_refresh_error', err) - ); - reject(err); - } - ); - }); + protected removeSessionCheckEventListener(): void { + if (this.sessionCheckEventListener) { + window.removeEventListener('message', this.sessionCheckEventListener); + this.sessionCheckEventListener = null; } + } - protected removeSilentRefreshEventListener(): void { - if (this.silentRefreshPostMessageEventListener) { - window.removeEventListener( - 'message', - this.silentRefreshPostMessageEventListener - ); - this.silentRefreshPostMessageEventListener = null; - } + protected initSessionCheck(): void { + if (!this.canPerformSessionCheck()) { + return; } - protected setupSilentRefreshEventListener(): void { - this.removeSilentRefreshEventListener(); - - this.silentRefreshPostMessageEventListener = (e: MessageEvent) => { - const message = this.processMessageEventMessage(e); + const existingIframe = document.getElementById(this.sessionCheckIFrameName); + if (existingIframe) { + document.body.removeChild(existingIframe); + } - this.tryLogin({ - customHashFragment: message, - preventClearHashAfterLogin: true, - customRedirectUri: this.silentRefreshRedirectUri || this.redirectUri - }).catch(err => this.debug('tryLogin during silent refresh failed', err)); - }; + const iframe = document.createElement('iframe'); + iframe.id = this.sessionCheckIFrameName; - window.addEventListener( - 'message', - this.silentRefreshPostMessageEventListener - ); - } + this.setupSessionCheckEventListener(); - /** - * Performs a silent refresh for implicit flow. - * Use this method to get new tokens when/before - * the existing tokens expire. - */ - public silentRefresh(params: object = {}, noPrompt = true): Promise { - const claims: object = this.getIdentityClaims() || {}; + const url = this.sessionCheckIFrameUrl; + iframe.setAttribute('src', url); + iframe.style.display = 'none'; + document.body.appendChild(iframe); - if (this.useIdTokenHintForSilentRefresh && this.hasValidIdToken()) { - params['id_token_hint'] = this.getIdToken(); - } + this.startSessionCheckTimer(); + } - if (!this.validateUrlForHttps(this.loginUrl)) { - throw new Error('loginUrl must use HTTPS (with TLS), or config value for property \'requireHttps\' must be set to \'false\' and allow HTTP (without TLS).'); - } + protected startSessionCheckTimer(): void { + this.stopSessionCheckTimer(); + this.ngZone.runOutsideAngular(() => { + this.sessionCheckTimer = setInterval( + this.checkSession.bind(this), + this.sessionCheckIntervall + ); + }); + } - if (typeof document === 'undefined') { - throw new Error('silent refresh is not supported on this platform'); - } + protected stopSessionCheckTimer(): void { + if (this.sessionCheckTimer) { + clearInterval(this.sessionCheckTimer); + this.sessionCheckTimer = null; + } + } - const existingIframe = document.getElementById( - this.silentRefreshIFrameName - ); + public checkSession(): void { + const iframe: any = document.getElementById(this.sessionCheckIFrameName); - if (existingIframe) { - document.body.removeChild(existingIframe); - } + if (!iframe) { + this.logger.warn( + 'checkSession did not find iframe', + this.sessionCheckIFrameName + ); + } - this.silentRefreshSubject = claims['sub']; + const sessionState = this.getSessionState(); - const iframe = document.createElement('iframe'); - iframe.id = this.silentRefreshIFrameName; + if (!sessionState) { + this.stopSessionCheckTimer(); + } - this.setupSilentRefreshEventListener(); + const message = this.clientId + ' ' + sessionState; + iframe.contentWindow.postMessage(message, this.issuer); + } - const redirectUri = this.silentRefreshRedirectUri || this.redirectUri; - this.createLoginUrl(null, null, redirectUri, noPrompt, params).then(url => { - iframe.setAttribute('src', url); + protected async createLoginUrl( + state = '', + loginHint = '', + customRedirectUri = '', + noPrompt = false, + params: object = {} + ): Promise { + const that = this; - if (!this.silentRefreshShowIFrame) { - iframe.style['display'] = 'none'; - } - document.body.appendChild(iframe); - }); + let redirectUri: string; - const errors = this.events.pipe( - filter(e => e instanceof OAuthErrorEvent), - first() - ); - const success = this.events.pipe( - filter(e => e.type === 'token_received'), - first() - ); - const timeout = of( - new OAuthErrorEvent('silent_refresh_timeout', null) - ).pipe(delay(this.silentRefreshTimeout)); - - return race([errors, success, timeout]) - .pipe( - map(e => { - if (e instanceof OAuthErrorEvent) { - if (e.type === 'silent_refresh_timeout') { - this.eventsSubject.next(e); - } else { - e = new OAuthErrorEvent('silent_refresh_error', e); - this.eventsSubject.next(e); - } - throw e; - } else if (e.type === 'token_received') { - e = new OAuthSuccessEvent('silently_refreshed'); - this.eventsSubject.next(e); - } - return e; - }) - ) - .toPromise(); + if (customRedirectUri) { + redirectUri = customRedirectUri; + } else { + redirectUri = this.redirectUri; } - /** - * This method exists for backwards compatibility. - * {@link OAuthService#initLoginFlowInPopup} handles both code - * and implicit flows. - */ - public initImplicitFlowInPopup(options?: { height?: number, width?: number }) { - return this.initLoginFlowInPopup(options); - } - - public initLoginFlowInPopup(options?: { height?: number, width?: number }) { - options = options || {}; - return this.createLoginUrl(null, null, this.silentRefreshRedirectUri, false, { - display: 'popup' - }).then(url => { - return new Promise((resolve, reject) => { - /** - * Error handling section - */ - const checkForPopupClosedInterval = 500; - let windowRef = window.open(url, '_blank', this.calculatePopupFeatures(options)); - let checkForPopupClosedTimer: any; - const checkForPopupClosed = () => { - if (!windowRef || windowRef.closed) { - cleanup(); - reject(new OAuthErrorEvent('popup_closed', {})); - } - }; - if (!windowRef) { - reject(new OAuthErrorEvent('popup_blocked', {})); - } else { - checkForPopupClosedTimer = window.setInterval(checkForPopupClosed, checkForPopupClosedInterval); - } - - const cleanup = () => { - window.clearInterval(checkForPopupClosedTimer); - window.removeEventListener('message', listener); - if (windowRef !== null) { - windowRef.close(); - } - windowRef = null; - }; - - const listener = (e: MessageEvent) => { - const message = this.processMessageEventMessage(e); - - if (message && message !== null) { - this.tryLogin({ - customHashFragment: message, - preventClearHashAfterLogin: true, - customRedirectUri: this.silentRefreshRedirectUri, - }).then(() => { - cleanup(); - resolve(); - }, err => { - cleanup(); - reject(err); - }); - } else { - console.log('false event firing'); - } - - }; - - window.addEventListener('message', listener); - }); - }); + const nonce = await this.createAndSaveNonce(); + + if (state) { + state = + nonce + this.config.nonceStateSeparator + encodeURIComponent(state); + } else { + state = nonce; } - protected calculatePopupFeatures(options: { height?: number, width?: number }): string { - // Specify an static height and width and calculate centered position + if (!this.requestAccessToken && !this.oidc) { + throw new Error('Either requestAccessToken or oidc or both must be true'); + } - const height = options.height || 470; - const width = options.width || 500; - const left = window.screenLeft + ((window.outerWidth - width) / 2); - const top = window.screenTop + ((window.outerHeight - height) / 2); - return `location=no,toolbar=no,width=${width},height=${height},top=${top},left=${left}`; + if (this.config.responseType) { + this.responseType = this.config.responseType; + } else { + if (this.oidc && this.requestAccessToken) { + this.responseType = 'id_token token'; + } else if (this.oidc && !this.requestAccessToken) { + this.responseType = 'id_token'; + } else { + this.responseType = 'token'; + } } - protected processMessageEventMessage(e: MessageEvent): string { - let expectedPrefix = '#'; + const seperationChar = that.loginUrl.indexOf('?') > -1 ? '&' : '?'; + + let scope = that.scope; + + if (this.oidc && !scope.match(/(^|\s)openid($|\s)/)) { + scope = 'openid ' + scope; + } + + let url = + that.loginUrl + + seperationChar + + 'response_type=' + + encodeURIComponent(that.responseType) + + '&client_id=' + + encodeURIComponent(that.clientId) + + '&state=' + + encodeURIComponent(state) + + '&redirect_uri=' + + encodeURIComponent(redirectUri) + + '&scope=' + + encodeURIComponent(scope); + + if (this.responseType === 'code' && !this.disablePKCE) { + const [ + challenge, + verifier + ] = await this.createChallangeVerifierPairForPKCE(); + + if ( + this.saveNoncesInLocalStorage && + typeof window['localStorage'] !== 'undefined' + ) { + localStorage.setItem('PKCI_verifier', verifier); + } else { + this._storage.setItem('PKCI_verifier', verifier); + } - if (this.silentRefreshMessagePrefix) { - expectedPrefix += this.silentRefreshMessagePrefix; - } + url += '&code_challenge=' + challenge; + url += '&code_challenge_method=S256'; + } - if (!e || !e.data || typeof e.data !== 'string') { - return; - } + if (loginHint) { + url += '&login_hint=' + encodeURIComponent(loginHint); + } - const prefixedMessage: string = e.data; + if (that.resource) { + url += '&resource=' + encodeURIComponent(that.resource); + } - if (!prefixedMessage.startsWith(expectedPrefix)) { - return; - } + if (that.oidc) { + url += '&nonce=' + encodeURIComponent(nonce); + } - return '#' + prefixedMessage.substr(expectedPrefix.length); + if (noPrompt) { + url += '&prompt=none'; } - protected canPerformSessionCheck(): boolean { - if (!this.sessionChecksEnabled) { - return false; - } - if (!this.sessionCheckIFrameUrl) { - console.warn( - 'sessionChecksEnabled is activated but there is no sessionCheckIFrameUrl' - ); - return false; - } - const sessionState = this.getSessionState(); - if (!sessionState) { - console.warn( - 'sessionChecksEnabled is activated but there is no session_state' - ); - return false; - } - if (typeof document === 'undefined') { - return false; - } + for (const key of Object.keys(params)) { + url += + '&' + encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); + } - return true; + if (this.customQueryParams) { + for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { + url += + '&' + key + '=' + encodeURIComponent(this.customQueryParams[key]); + } } - protected setupSessionCheckEventListener(): void { - this.removeSessionCheckEventListener(); + return url; + } - this.sessionCheckEventListener = (e: MessageEvent) => { - const origin = e.origin.toLowerCase(); - const issuer = this.issuer.toLowerCase(); - - this.debug('sessionCheckEventListener'); - - if (!issuer.startsWith(origin)) { - this.debug( - 'sessionCheckEventListener', - 'wrong origin', - origin, - 'expected', - issuer, - 'event', - e - ); - - return; - } - - // only run in Angular zone if it is 'changed' or 'error' - switch (e.data) { - case 'unchanged': - this.handleSessionUnchanged(); - break; - case 'changed': - this.ngZone.run(() => { - this.handleSessionChange(); - }); - break; - case 'error': - this.ngZone.run(() => { - this.handleSessionError(); - }); - break; - } - - this.debug('got info from session check inframe', e); - }; - - // prevent Angular from refreshing the view on every message (runs in intervals) - this.ngZone.runOutsideAngular(() => { - window.addEventListener('message', this.sessionCheckEventListener); - }); - } - - protected handleSessionUnchanged(): void { - this.debug('session check', 'session unchanged'); + initImplicitFlowInternal( + additionalState = '', + params: string | object = '' + ): void { + if (this.inImplicitFlow) { + return; } - protected handleSessionChange(): void { - this.eventsSubject.next(new OAuthInfoEvent('session_changed')); - this.stopSessionCheckTimer(); + this.inImplicitFlow = true; - if (!this.useSilentRefresh && this.responseType === 'code') { - this.refreshToken() - .then(_ => { - this.debug('token refresh after session change worked'); - }) - .catch(_ => { - this.debug('token refresh did not work after session changed'); - this.eventsSubject.next(new OAuthInfoEvent('session_terminated')); - this.logOut(true); - }); - } else if (this.silentRefreshRedirectUri) { - this.silentRefresh().catch(_ => - this.debug('silent refresh failed after session changed') - ); - this.waitForSilentRefreshAfterSessionChange(); - } else { - this.eventsSubject.next(new OAuthInfoEvent('session_terminated')); - this.logOut(true); - } + if (!this.validateUrlForHttps(this.loginUrl)) { + throw new Error( + "loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)." + ); } - protected waitForSilentRefreshAfterSessionChange(): void { - this.events - .pipe( - filter( - (e: OAuthEvent) => - e.type === 'silently_refreshed' || - e.type === 'silent_refresh_timeout' || - e.type === 'silent_refresh_error' - ), - first() - ) - .subscribe(e => { - if (e.type !== 'silently_refreshed') { - this.debug('silent refresh did not work after session changed'); - this.eventsSubject.next(new OAuthInfoEvent('session_terminated')); - this.logOut(true); - } - }); - } + let addParams: object = {}; + let loginHint: string = null; - protected handleSessionError(): void { - this.stopSessionCheckTimer(); - this.eventsSubject.next(new OAuthInfoEvent('session_error')); + if (typeof params === 'string') { + loginHint = params; + } else if (typeof params === 'object') { + addParams = params; } - protected removeSessionCheckEventListener(): void { - if (this.sessionCheckEventListener) { - window.removeEventListener('message', this.sessionCheckEventListener); - this.sessionCheckEventListener = null; - } + this.createLoginUrl(additionalState, loginHint, null, false, addParams) + .then(this.config.openUri) + .catch(error => { + console.error('Error in initImplicitFlow', error); + this.inImplicitFlow = false; + }); + } + + /** + * Starts the implicit flow and redirects to user to + * the auth servers' login url. + * + * @param additionalState Optional state that is passed around. + * You'll find this state in the property `state` after `tryLogin` logged in the user. + * @param params Hash with additional parameter. If it is a string, it is used for the + * parameter loginHint (for the sake of compatibility with former versions) + */ + public initImplicitFlow( + additionalState = '', + params: string | object = '' + ): void { + if (this.loginUrl !== '') { + this.initImplicitFlowInternal(additionalState, params); + } else { + this.events + .pipe(filter(e => e.type === 'discovery_document_loaded')) + .subscribe(_ => this.initImplicitFlowInternal(additionalState, params)); + } + } + + /** + * Reset current implicit flow + * + * @description This method allows resetting the current implict flow in order to be initialized again. + */ + public resetImplicitFlow(): void { + this.inImplicitFlow = false; + } + + protected callOnTokenReceivedIfExists(options: LoginOptions): void { + const that = this; + if (options.onTokenReceived) { + const tokenParams = { + idClaims: that.getIdentityClaims(), + idToken: that.getIdToken(), + accessToken: that.getAccessToken(), + state: that.state + }; + options.onTokenReceived(tokenParams); + } + } + + protected storeAccessTokenResponse( + accessToken: string, + refreshToken: string, + expiresIn: number, + grantedScopes: String, + customParameters?: Map + ): void { + this._storage.setItem('access_token', accessToken); + if (grantedScopes && !Array.isArray(grantedScopes)) { + this._storage.setItem( + 'granted_scopes', + JSON.stringify(grantedScopes.split('+')) + ); + } else if (grantedScopes && Array.isArray(grantedScopes)) { + this._storage.setItem('granted_scopes', JSON.stringify(grantedScopes)); + } + + this._storage.setItem('access_token_stored_at', '' + Date.now()); + if (expiresIn) { + const expiresInMilliSeconds = expiresIn * 1000; + const now = new Date(); + const expiresAt = now.getTime() + expiresInMilliSeconds; + this._storage.setItem('expires_at', '' + expiresAt); + } + + if (refreshToken) { + this._storage.setItem('refresh_token', refreshToken); + } + if (customParameters) { + customParameters.forEach((value: string, key: string) => { + this._storage.setItem(key, value); + }); } + } - protected initSessionCheck(): void { - if (!this.canPerformSessionCheck()) { - return; - } - - const existingIframe = document.getElementById(this.sessionCheckIFrameName); - if (existingIframe) { - document.body.removeChild(existingIframe); - } - - const iframe = document.createElement('iframe'); - iframe.id = this.sessionCheckIFrameName; - - this.setupSessionCheckEventListener(); - - const url = this.sessionCheckIFrameUrl; - iframe.setAttribute('src', url); - iframe.style.display = 'none'; - document.body.appendChild(iframe); - - this.startSessionCheckTimer(); + /** + * Delegates to tryLoginImplicitFlow for the sake of competability + * @param options Optional options. + */ + public tryLogin(options: LoginOptions = null): Promise { + if (this.config.responseType === 'code') { + return this.tryLoginCodeFlow(options).then(_ => true); + } else { + return this.tryLoginImplicitFlow(options); } + } - protected startSessionCheckTimer(): void { - this.stopSessionCheckTimer(); - this.ngZone.runOutsideAngular(() => { - this.sessionCheckTimer = setInterval( - this.checkSession.bind(this), - this.sessionCheckIntervall - ); - }); + private parseQueryString(queryString: string): object { + if (!queryString || queryString.length === 0) { + return {}; } - protected stopSessionCheckTimer(): void { - if (this.sessionCheckTimer) { - clearInterval(this.sessionCheckTimer); - this.sessionCheckTimer = null; - } + if (queryString.charAt(0) === '?') { + queryString = queryString.substr(1); } - public checkSession(): void { - const iframe: any = document.getElementById(this.sessionCheckIFrameName); + return this.urlHelper.parseQueryString(queryString); + } - if (!iframe) { - this.logger.warn( - 'checkSession did not find iframe', - this.sessionCheckIFrameName - ); - } - - const sessionState = this.getSessionState(); + public tryLoginCodeFlow(options: LoginOptions = null): Promise { + options = options || {}; - if (!sessionState) { - this.stopSessionCheckTimer(); - } + const querySource = options.customHashFragment + ? options.customHashFragment.substring(1) + : window.location.search; - const message = this.clientId + ' ' + sessionState; - iframe.contentWindow.postMessage(message, this.issuer); - } + const parts = this.getCodePartsFromUrl(querySource); - protected async createLoginUrl( - state = '', - loginHint = '', - customRedirectUri = '', - noPrompt = false, - params: object = {} - ): Promise { - const that = this; + const code = parts['code']; + const state = parts['state']; - let redirectUri: string; + const sessionState = parts['session_state']; - if (customRedirectUri) { - redirectUri = customRedirectUri; - } else { - redirectUri = this.redirectUri; - } - - const nonce = await this.createAndSaveNonce(); - - if (state) { - state = nonce + this.config.nonceStateSeparator + encodeURIComponent(state); - } else { - state = nonce; - } - - if (!this.requestAccessToken && !this.oidc) { - throw new Error( - 'Either requestAccessToken or oidc or both must be true' - ); - } - - if (this.config.responseType) { - this.responseType = this.config.responseType; - } else { - if (this.oidc && this.requestAccessToken) { - this.responseType = 'id_token token'; - } else if (this.oidc && !this.requestAccessToken) { - this.responseType = 'id_token'; - } else { - this.responseType = 'token'; - } - } - - const seperationChar = that.loginUrl.indexOf('?') > -1 ? '&' : '?'; - - let scope = that.scope; - - if (this.oidc && !scope.match(/(^|\s)openid($|\s)/)) { - scope = 'openid ' + scope; - } - - let url = - that.loginUrl + - seperationChar + - 'response_type=' + - encodeURIComponent(that.responseType) + - '&client_id=' + - encodeURIComponent(that.clientId) + - '&state=' + - encodeURIComponent(state) + - '&redirect_uri=' + - encodeURIComponent(redirectUri) + - '&scope=' + - encodeURIComponent(scope); - - if (this.responseType === 'code' && !this.disablePKCE) { - const [challenge, verifier] = await this.createChallangeVerifierPairForPKCE(); - - if (this.saveNoncesInLocalStorage && typeof window['localStorage'] !== 'undefined') { - localStorage.setItem('PKCI_verifier', verifier); - } else { - this._storage.setItem('PKCI_verifier', verifier); - } - - url += '&code_challenge=' + challenge; - url += '&code_challenge_method=S256'; - } - - if (loginHint) { - url += '&login_hint=' + encodeURIComponent(loginHint); - } - - if (that.resource) { - url += '&resource=' + encodeURIComponent(that.resource); - } - - if (that.oidc) { - url += '&nonce=' + encodeURIComponent(nonce); - } - - if (noPrompt) { - url += '&prompt=none'; - } - - for (const key of Object.keys(params)) { - url += - '&' + encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); - } - - if (this.customQueryParams) { - for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { - url += - '&' + key + '=' + encodeURIComponent(this.customQueryParams[key]); - } - } - - return url; + if (!options.preventClearHashAfterLogin) { + const href = location.href + .replace(/[&\?]code=[^&\$]*/, '') + .replace(/[&\?]scope=[^&\$]*/, '') + .replace(/[&\?]state=[^&\$]*/, '') + .replace(/[&\?]session_state=[^&\$]*/, ''); + history.replaceState(null, window.name, href); } - initImplicitFlowInternal( - additionalState = '', - params: string | object = '' - ): void { - if (this.inImplicitFlow) { - return; - } - - this.inImplicitFlow = true; + let [nonceInState, userState] = this.parseState(state); + this.state = userState; - if (!this.validateUrlForHttps(this.loginUrl)) { - throw new Error( - 'loginUrl must use HTTPS (with TLS), or config value for property \'requireHttps\' must be set to \'false\' and allow HTTP (without TLS).' - ); - } - - let addParams: object = {}; - let loginHint: string = null; - - if (typeof params === 'string') { - loginHint = params; - } else if (typeof params === 'object') { - addParams = params; - } - - this.createLoginUrl(additionalState, loginHint, null, false, addParams) - .then(this.config.openUri) - .catch(error => { - console.error('Error in initImplicitFlow', error); - this.inImplicitFlow = false; - }); - } - - /** - * Starts the implicit flow and redirects to user to - * the auth servers' login url. - * - * @param additionalState Optional state that is passed around. - * You'll find this state in the property `state` after `tryLogin` logged in the user. - * @param params Hash with additional parameter. If it is a string, it is used for the - * parameter loginHint (for the sake of compatibility with former versions) - */ - public initImplicitFlow( - additionalState = '', - params: string | object = '' - ): void { - if (this.loginUrl !== '') { - this.initImplicitFlowInternal(additionalState, params); - } else { - this.events - .pipe(filter(e => e.type === 'discovery_document_loaded')) - .subscribe(_ => this.initImplicitFlowInternal(additionalState, params)); - } + if (parts['error']) { + this.debug('error trying to login'); + this.handleLoginError({}, parts); + const err = new OAuthErrorEvent('code_error', {}, parts); + this.eventsSubject.next(err); + return Promise.reject(err); } - /** - * Reset current implicit flow - * - * @description This method allows resetting the current implict flow in order to be initialized again. - */ - public resetImplicitFlow(): void { - this.inImplicitFlow = false; + if (!nonceInState) { + return Promise.resolve(); } - protected callOnTokenReceivedIfExists(options: LoginOptions): void { - const that = this; - if (options.onTokenReceived) { - const tokenParams = { - idClaims: that.getIdentityClaims(), - idToken: that.getIdToken(), - accessToken: that.getAccessToken(), - state: that.state - }; - options.onTokenReceived(tokenParams); - } + const success = this.validateNonce(nonceInState); + if (!success) { + const event = new OAuthErrorEvent('invalid_nonce_in_state', null); + this.eventsSubject.next(event); + return Promise.reject(event); } - protected storeAccessTokenResponse( - accessToken: string, - refreshToken: string, - expiresIn: number, - grantedScopes: String, - customParameters?: Map - ): void { - this._storage.setItem('access_token', accessToken); - if (grantedScopes && !Array.isArray(grantedScopes)) { - this._storage.setItem('granted_scopes', JSON.stringify(grantedScopes.split('+'))); - } else if (grantedScopes && Array.isArray(grantedScopes)) { - this._storage.setItem('granted_scopes', JSON.stringify(grantedScopes)); - } - - this._storage.setItem('access_token_stored_at', '' + Date.now()); - if (expiresIn) { - const expiresInMilliSeconds = expiresIn * 1000; - const now = new Date(); - const expiresAt = now.getTime() + expiresInMilliSeconds; - this._storage.setItem('expires_at', '' + expiresAt); - } + this.storeSessionState(sessionState); - if (refreshToken) { - this._storage.setItem('refresh_token', refreshToken); - } - if (customParameters) { - customParameters.forEach((value : string, key: string) => { - this._storage.setItem(key, value); - }); - } + if (code) { + return this.getTokenFromCode(code, options).then(_ => null); + } else { + return Promise.resolve(); } + } - /** - * Delegates to tryLoginImplicitFlow for the sake of competability - * @param options Optional options. - */ - public tryLogin(options: LoginOptions = null): Promise { - if (this.config.responseType === 'code') { - return this.tryLoginCodeFlow(options).then(_ => true); - } else { - return this.tryLoginImplicitFlow(options); - } + /** + * Retrieve the returned auth code from the redirect uri that has been called. + * If required also check hash, as we could use hash location strategy. + */ + private getCodePartsFromUrl(queryString: string): object { + if (!queryString || queryString.length === 0) { + return this.urlHelper.getHashFragmentParams(); } - private parseQueryString(queryString: string): object { - if (!queryString || queryString.length === 0) { - return {}; - } - - if (queryString.charAt(0) === '?') { - queryString = queryString.substr(1); - } - - return this.urlHelper.parseQueryString(queryString); + // normalize query string + if (queryString.charAt(0) === '?') { + queryString = queryString.substr(1); } - public tryLoginCodeFlow(options: LoginOptions = null): Promise { - options = options || {}; - - const querySource = options.customHashFragment ? - options.customHashFragment.substring(1) : - window.location.search; - - const parts = this.getCodePartsFromUrl(querySource); + return this.urlHelper.parseQueryString(queryString); + } - const code = parts['code']; - const state = parts['state']; + /** + * Get token using an intermediate code. Works for the Authorization Code flow. + */ + private getTokenFromCode( + code: string, + options: LoginOptions + ): Promise { + let params = new HttpParams() + .set('grant_type', 'authorization_code') + .set('code', code) + .set('redirect_uri', options.customRedirectUri || this.redirectUri); - const sessionState = parts['session_state']; - - if (!options.preventClearHashAfterLogin) { - const href = location.href - .replace(/[&\?]code=[^&\$]*/, '') - .replace(/[&\?]scope=[^&\$]*/, '') - .replace(/[&\?]state=[^&\$]*/, '') - .replace(/[&\?]session_state=[^&\$]*/, ''); - - history.replaceState(null, window.name, href); - } - - let [nonceInState, userState] = this.parseState(state); - this.state = userState; - - if (parts['error']) { - this.debug('error trying to login'); - this.handleLoginError({}, parts); - const err = new OAuthErrorEvent('code_error', {}, parts); - this.eventsSubject.next(err); - return Promise.reject(err); - } + if (!this.disablePKCE) { + let pkciVerifier; - if (!nonceInState) { - return Promise.resolve(); - } - - const success = this.validateNonce(nonceInState); - if (!success) { - const event = new OAuthErrorEvent('invalid_nonce_in_state', null); - this.eventsSubject.next(event); - return Promise.reject(event); - } - - this.storeSessionState(sessionState); + if ( + this.saveNoncesInLocalStorage && + typeof window['localStorage'] !== 'undefined' + ) { + pkciVerifier = localStorage.getItem('PKCI_verifier'); + } else { + pkciVerifier = this._storage.getItem('PKCI_verifier'); + } - if (code) { - return this.getTokenFromCode(code, options).then(_ => null); - } else { - return Promise.resolve(); - } + if (!pkciVerifier) { + console.warn('No PKCI verifier found in oauth storage!'); + } else { + params = params.set('code_verifier', pkciVerifier); + } } - /** - * Retrieve the returned auth code from the redirect uri that has been called. - * If required also check hash, as we could use hash location strategy. - */ - private getCodePartsFromUrl(queryString: string): object { - if (!queryString || queryString.length === 0) { - return this.urlHelper.getHashFragmentParams(); - } + return this.fetchAndProcessToken(params); + } - // normalize query string - if (queryString.charAt(0) === '?') { - queryString = queryString.substr(1); - } + private fetchAndProcessToken(params: HttpParams): Promise { + this.assertUrlNotNullAndCorrectProtocol( + this.tokenEndpoint, + 'tokenEndpoint' + ); + let headers = new HttpHeaders().set( + 'Content-Type', + 'application/x-www-form-urlencoded' + ); - return this.urlHelper.parseQueryString(queryString); + if (this.useHttpBasicAuth) { + const header = btoa(`${this.clientId}:${this.dummyClientSecret}`); + headers = headers.set('Authorization', 'Basic ' + header); } - /** - * Get token using an intermediate code. Works for the Authorization Code flow. - */ - private getTokenFromCode(code: string, options: LoginOptions): Promise { - let params = new HttpParams() - .set('grant_type', 'authorization_code') - .set('code', code) - .set('redirect_uri', options.customRedirectUri || this.redirectUri); - - if (!this.disablePKCE) { - let pkciVerifier; - - if (this.saveNoncesInLocalStorage && - typeof window['localStorage'] !== 'undefined') { - pkciVerifier = localStorage.getItem('PKCI_verifier'); - } else { - pkciVerifier = this._storage.getItem('PKCI_verifier'); - } - - if (!pkciVerifier) { - console.warn('No PKCI verifier found in oauth storage!'); - } else { - params = params.set('code_verifier', pkciVerifier); - } - } - - return this.fetchAndProcessToken(params); + if (!this.useHttpBasicAuth) { + params = params.set('client_id', this.clientId); } - private fetchAndProcessToken(params: HttpParams): Promise { - - this.assertUrlNotNullAndCorrectProtocol(this.tokenEndpoint, 'tokenEndpoint'); - let headers = new HttpHeaders() - .set('Content-Type', 'application/x-www-form-urlencoded'); - - if (this.useHttpBasicAuth) { - const header = btoa(`${this.clientId}:${this.dummyClientSecret}`); - headers = headers.set( - 'Authorization', - 'Basic ' + header); - } - - if (!this.useHttpBasicAuth) { - params = params.set('client_id', this.clientId); - } - - if (!this.useHttpBasicAuth && this.dummyClientSecret) { - params = params.set('client_secret', this.dummyClientSecret); - } - - return new Promise((resolve, reject) => { - - if (this.customQueryParams) { - for (let key of Object.getOwnPropertyNames(this.customQueryParams)) { - params = params.set(key, this.customQueryParams[key]); - } - } - - this.http.post(this.tokenEndpoint, params, { headers }).subscribe( - (tokenResponse) => { - this.debug('refresh tokenResponse', tokenResponse); - this.storeAccessTokenResponse( - tokenResponse.access_token, - tokenResponse.refresh_token, - tokenResponse.expires_in, - tokenResponse.scope, - this.extractRecognizedCustomParameters(tokenResponse)); - - if (this.oidc && tokenResponse.id_token) { - this.processIdToken(tokenResponse.id_token, tokenResponse.access_token). - then(result => { - this.storeIdToken(result); - - this.eventsSubject.next(new OAuthSuccessEvent('token_received')); - this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); - - resolve(tokenResponse); - }) - .catch(reason => { - this.eventsSubject.next(new OAuthErrorEvent('token_validation_error', reason)); - console.error('Error validating tokens'); - console.error(reason); - - reject(reason); - }); - } else { - this.eventsSubject.next(new OAuthSuccessEvent('token_received')); - this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); - - resolve(tokenResponse); - } - }, - (err) => { - console.error('Error getting token', err); - this.eventsSubject.next(new OAuthErrorEvent('token_refresh_error', err)); - reject(err); - } - ); - }); + if (!this.useHttpBasicAuth && this.dummyClientSecret) { + params = params.set('client_secret', this.dummyClientSecret); } - /** - * Checks whether there are tokens in the hash fragment - * as a result of the implicit flow. These tokens are - * parsed, validated and used to sign the user in to the - * current client. - * - * @param options Optional options. - */ - public tryLoginImplicitFlow(options: LoginOptions = null): Promise { - options = options || {}; - - let parts: object; - - if (options.customHashFragment) { - parts = this.urlHelper.getHashFragmentParams(options.customHashFragment); - } else { - parts = this.urlHelper.getHashFragmentParams(); - } - - this.debug('parsed url', parts); - - const state = parts['state']; - - let [nonceInState, userState] = this.parseState(state); - this.state = userState; - - if (parts['error']) { - this.debug('error trying to login'); - this.handleLoginError(options, parts); - const err = new OAuthErrorEvent('token_error', {}, parts); - this.eventsSubject.next(err); - return Promise.reject(err); + return new Promise((resolve, reject) => { + if (this.customQueryParams) { + for (let key of Object.getOwnPropertyNames(this.customQueryParams)) { + params = params.set(key, this.customQueryParams[key]); } + } - const accessToken = parts['access_token']; - const idToken = parts['id_token']; - const sessionState = parts['session_state']; - const grantedScopes = parts['scope']; - - if (!this.requestAccessToken && !this.oidc) { - return Promise.reject( - 'Either requestAccessToken or oidc (or both) must be true.' + this.http + .post(this.tokenEndpoint, params, { headers }) + .subscribe( + tokenResponse => { + this.debug('refresh tokenResponse', tokenResponse); + this.storeAccessTokenResponse( + tokenResponse.access_token, + tokenResponse.refresh_token, + tokenResponse.expires_in, + tokenResponse.scope, + this.extractRecognizedCustomParameters(tokenResponse) ); - } - - if (this.requestAccessToken && !accessToken) { - return Promise.resolve(false); - } - if (this.requestAccessToken && !options.disableOAuth2StateCheck && !state) { - return Promise.resolve(false); - } - if (this.oidc && !idToken) { - return Promise.resolve(false); - } - if (this.sessionChecksEnabled && !sessionState) { - this.logger.warn( - 'session checks (Session Status Change Notification) ' + - 'were activated in the configuration but the id_token ' + - 'does not contain a session_state claim' - ); - } + if (this.oidc && tokenResponse.id_token) { + this.processIdToken( + tokenResponse.id_token, + tokenResponse.access_token + ) + .then(result => { + this.storeIdToken(result); + + this.eventsSubject.next( + new OAuthSuccessEvent('token_received') + ); + this.eventsSubject.next( + new OAuthSuccessEvent('token_refreshed') + ); + + resolve(tokenResponse); + }) + .catch(reason => { + this.eventsSubject.next( + new OAuthErrorEvent('token_validation_error', reason) + ); + console.error('Error validating tokens'); + console.error(reason); - if (this.requestAccessToken && !options.disableOAuth2StateCheck) { - const success = this.validateNonce(nonceInState); + reject(reason); + }); + } else { + this.eventsSubject.next(new OAuthSuccessEvent('token_received')); + this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); - if (!success) { - const event = new OAuthErrorEvent('invalid_nonce_in_state', null); - this.eventsSubject.next(event); - return Promise.reject(event); + resolve(tokenResponse); } - } - - if (this.requestAccessToken) { - this.storeAccessTokenResponse( - accessToken, - null, - parts['expires_in'] || this.fallbackAccessTokenExpirationTimeInSec, - grantedScopes + }, + err => { + console.error('Error getting token', err); + this.eventsSubject.next( + new OAuthErrorEvent('token_refresh_error', err) ); - } + reject(err); + } + ); + }); + } - if (!this.oidc) { - this.eventsSubject.next(new OAuthSuccessEvent('token_received')); - if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) { - location.hash = ''; - } + /** + * Checks whether there are tokens in the hash fragment + * as a result of the implicit flow. These tokens are + * parsed, validated and used to sign the user in to the + * current client. + * + * @param options Optional options. + */ + public tryLoginImplicitFlow(options: LoginOptions = null): Promise { + options = options || {}; - this.callOnTokenReceivedIfExists(options); - return Promise.resolve(true); + let parts: object; - } - - return this.processIdToken(idToken, accessToken) - .then(result => { - if (options.validationHandler) { - return options - .validationHandler({ - accessToken: accessToken, - idClaims: result.idTokenClaims, - idToken: result.idToken, - state: state - }) - .then(_ => result); - } - return result; - }) - .then(result => { - this.storeIdToken(result); - this.storeSessionState(sessionState); - if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) { - location.hash = ''; - } - this.eventsSubject.next(new OAuthSuccessEvent('token_received')); - this.callOnTokenReceivedIfExists(options); - this.inImplicitFlow = false; - return true; - }) - .catch(reason => { - this.eventsSubject.next( - new OAuthErrorEvent('token_validation_error', reason) - ); - this.logger.error('Error validating tokens'); - this.logger.error(reason); - return Promise.reject(reason); - }); + if (options.customHashFragment) { + parts = this.urlHelper.getHashFragmentParams(options.customHashFragment); + } else { + parts = this.urlHelper.getHashFragmentParams(); } - private parseState(state: string): [string, string] { - let nonce = state; - let userState = ''; + this.debug('parsed url', parts); - if (state) { - const idx = state.indexOf(this.config.nonceStateSeparator); - if (idx > -1) { - nonce = state.substr(0, idx); - userState = state.substr(idx + this.config.nonceStateSeparator.length); - } - } - return [nonce, userState]; - } - - protected validateNonce( - nonceInState: string - ): boolean { + const state = parts['state']; - let savedNonce; + let [nonceInState, userState] = this.parseState(state); + this.state = userState; - if (this.saveNoncesInLocalStorage && - typeof window['localStorage'] !== 'undefined') { - savedNonce = localStorage.getItem('nonce'); - } else { - savedNonce = this._storage.getItem('nonce'); - } + if (parts['error']) { + this.debug('error trying to login'); + this.handleLoginError(options, parts); + const err = new OAuthErrorEvent('token_error', {}, parts); + this.eventsSubject.next(err); + return Promise.reject(err); + } - if (savedNonce !== nonceInState) { + const accessToken = parts['access_token']; + const idToken = parts['id_token']; + const sessionState = parts['session_state']; + const grantedScopes = parts['scope']; - const err = 'Validating access_token failed, wrong state/nonce.'; - console.error(err, savedNonce, nonceInState); - return false; - } - return true; + if (!this.requestAccessToken && !this.oidc) { + return Promise.reject( + 'Either requestAccessToken or oidc (or both) must be true.' + ); } - protected storeIdToken(idToken: ParsedIdToken): void { - this._storage.setItem('id_token', idToken.idToken); - this._storage.setItem('id_token_claims_obj', idToken.idTokenClaimsJson); - this._storage.setItem('id_token_expires_at', '' + idToken.idTokenExpiresAt); - this._storage.setItem('id_token_stored_at', '' + Date.now()); + if (this.requestAccessToken && !accessToken) { + return Promise.resolve(false); } - - protected storeSessionState(sessionState: string): void { - this._storage.setItem('session_state', sessionState); + if (this.requestAccessToken && !options.disableOAuth2StateCheck && !state) { + return Promise.resolve(false); } - - protected getSessionState(): string { - return this._storage.getItem('session_state'); + if (this.oidc && !idToken) { + return Promise.resolve(false); } - protected handleLoginError(options: LoginOptions, parts: object): void { - if (options.onLoginError) { - options.onLoginError(parts); - } - if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) { - location.hash = ''; - } + if (this.sessionChecksEnabled && !sessionState) { + this.logger.warn( + 'session checks (Session Status Change Notification) ' + + 'were activated in the configuration but the id_token ' + + 'does not contain a session_state claim' + ); } - /** - * @ignore - */ - public processIdToken( - idToken: string, - accessToken: string, - skipNonceCheck = false - ): Promise { - const tokenParts = idToken.split('.'); - const headerBase64 = this.padBase64(tokenParts[0]); - const headerJson = b64DecodeUnicode(headerBase64); - const header = JSON.parse(headerJson); - const claimsBase64 = this.padBase64(tokenParts[1]); - const claimsJson = b64DecodeUnicode(claimsBase64); - const claims = JSON.parse(claimsJson); - - let savedNonce; - if (this.saveNoncesInLocalStorage && - typeof window['localStorage'] !== 'undefined') { - savedNonce = localStorage.getItem('nonce'); - } else { - savedNonce = this._storage.getItem('nonce'); - } - - if (Array.isArray(claims.aud)) { - if (claims.aud.every(v => v !== this.clientId)) { - const err = 'Wrong audience: ' + claims.aud.join(','); - this.logger.warn(err); - return Promise.reject(err); - } - } else { - if (claims.aud !== this.clientId) { - const err = 'Wrong audience: ' + claims.aud; - this.logger.warn(err); - return Promise.reject(err); - } - } + if (this.requestAccessToken && !options.disableOAuth2StateCheck) { + const success = this.validateNonce(nonceInState); - if (!claims.sub) { - const err = 'No sub claim in id_token'; - this.logger.warn(err); - return Promise.reject(err); - } - - /* For now, we only check whether the sub against - * silentRefreshSubject when sessionChecksEnabled is on - * We will reconsider in a later version to do this - * in every other case too. - */ - if ( - this.sessionChecksEnabled && - this.silentRefreshSubject && - this.silentRefreshSubject !== claims['sub'] - ) { - const err = - 'After refreshing, we got an id_token for another user (sub). ' + - `Expected sub: ${this.silentRefreshSubject}, received sub: ${ - claims['sub'] - }`; - - this.logger.warn(err); - return Promise.reject(err); - } - - if (!claims.iat) { - const err = 'No iat claim in id_token'; - this.logger.warn(err); - return Promise.reject(err); - } - - if (!this.skipIssuerCheck && claims.iss !== this.issuer) { - const err = 'Wrong issuer: ' + claims.iss; - this.logger.warn(err); - return Promise.reject(err); - } - - if (!skipNonceCheck && claims.nonce !== savedNonce) { - const err = 'Wrong nonce: ' + claims.nonce; - this.logger.warn(err); - return Promise.reject(err); - } - // at_hash is not applicable to authorization code flow - // addressing https://github.com/manfredsteyer/angular-oauth2-oidc/issues/661 - // i.e. Based on spec the at_hash check is only true for implicit code flow on Ping Federate - // https://www.pingidentity.com/developer/en/resources/openid-connect-developers-guide.html - if (this.hasOwnProperty('responseType') && this.responseType === 'code') { - this.disableAtHashCheck = true; - } - if ( - !this.disableAtHashCheck && - this.requestAccessToken && - !claims['at_hash'] - ) { - const err = 'An at_hash is needed!'; - this.logger.warn(err); - return Promise.reject(err); - } - - const now = Date.now(); - const issuedAtMSec = claims.iat * 1000; - const expiresAtMSec = claims.exp * 1000; - const clockSkewInMSec = (this.clockSkewInSec || 600) * 1000; - - if ( - issuedAtMSec - clockSkewInMSec >= now || - expiresAtMSec + clockSkewInMSec <= now - ) { - const err = 'Token has expired'; - console.error(err); - console.error({ - now: now, - issuedAtMSec: issuedAtMSec, - expiresAtMSec: expiresAtMSec - }); - return Promise.reject(err); - } - - const validationParams: ValidationParams = { - accessToken: accessToken, - idToken: idToken, - jwks: this.jwks, - idTokenClaims: claims, - idTokenHeader: header, - loadKeys: () => this.loadJwks() - }; - - if (this.disableAtHashCheck) { - return this.checkSignature(validationParams).then(_ => { - const result: ParsedIdToken = { - idToken: idToken, - idTokenClaims: claims, - idTokenClaimsJson: claimsJson, - idTokenHeader: header, - idTokenHeaderJson: headerJson, - idTokenExpiresAt: expiresAtMSec - }; - return result; - }); - } - - return this.checkAtHash(validationParams) - .then(atHashValid => { - if ( - !this.disableAtHashCheck && - this.requestAccessToken && - !atHashValid - ) { - const err = 'Wrong at_hash'; - this.logger.warn(err); - return Promise.reject(err); - } - - return this.checkSignature(validationParams).then(_ => { - const atHashCheckEnabled = !this.disableAtHashCheck; - const result: ParsedIdToken = { - idToken: idToken, - idTokenClaims: claims, - idTokenClaimsJson: claimsJson, - idTokenHeader: header, - idTokenHeaderJson: headerJson, - idTokenExpiresAt: expiresAtMSec - }; - if (atHashCheckEnabled) { - return this.checkAtHash(validationParams).then(atHashValid => { - if (this.requestAccessToken && !atHashValid) { - const err = 'Wrong at_hash'; - this.logger.warn(err); - return Promise.reject(err); - } else { - return result; - } - }); - } else { - return result; - } - }); - }); + if (!success) { + const event = new OAuthErrorEvent('invalid_nonce_in_state', null); + this.eventsSubject.next(event); + return Promise.reject(event); + } } - /** - * Returns the received claims about the user. - */ - public getIdentityClaims(): object { - const claims = this._storage.getItem('id_token_claims_obj'); - if (!claims) { - return null; - } - return JSON.parse(claims); + if (this.requestAccessToken) { + this.storeAccessTokenResponse( + accessToken, + null, + parts['expires_in'] || this.fallbackAccessTokenExpirationTimeInSec, + grantedScopes + ); } - /** - * Returns the granted scopes from the server. - */ - public getGrantedScopes(): object { - const scopes = this._storage.getItem('granted_scopes'); - if (!scopes) { - return null; - } - return JSON.parse(scopes); - } + if (!this.oidc) { + this.eventsSubject.next(new OAuthSuccessEvent('token_received')); + if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) { + location.hash = ''; + } - /** - * Returns the current id_token. - */ - public getIdToken(): string { - return this._storage - ? this._storage.getItem('id_token') - : null; + this.callOnTokenReceivedIfExists(options); + return Promise.resolve(true); } - protected padBase64(base64data): string { - while (base64data.length % 4 !== 0) { - base64data += '='; + return this.processIdToken(idToken, accessToken) + .then(result => { + if (options.validationHandler) { + return options + .validationHandler({ + accessToken: accessToken, + idClaims: result.idTokenClaims, + idToken: result.idToken, + state: state + }) + .then(_ => result); } - return base64data; - } + return result; + }) + .then(result => { + this.storeIdToken(result); + this.storeSessionState(sessionState); + if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) { + location.hash = ''; + } + this.eventsSubject.next(new OAuthSuccessEvent('token_received')); + this.callOnTokenReceivedIfExists(options); + this.inImplicitFlow = false; + return true; + }) + .catch(reason => { + this.eventsSubject.next( + new OAuthErrorEvent('token_validation_error', reason) + ); + this.logger.error('Error validating tokens'); + this.logger.error(reason); + return Promise.reject(reason); + }); + } - /** - * Returns the current access_token. - */ - public getAccessToken(): string { - return this._storage - ? this._storage.getItem('access_token') - : null; - } + private parseState(state: string): [string, string] { + let nonce = state; + let userState = ''; - public getRefreshToken(): string { - return this._storage - ? this._storage.getItem('refresh_token') - : null; + if (state) { + const idx = state.indexOf(this.config.nonceStateSeparator); + if (idx > -1) { + nonce = state.substr(0, idx); + userState = state.substr(idx + this.config.nonceStateSeparator.length); + } } + return [nonce, userState]; + } - /** - * Returns the expiration date of the access_token - * as milliseconds since 1970. - */ - public getAccessTokenExpiration(): number { - if (!this._storage.getItem('expires_at')) { - return null; - } - return parseInt(this._storage.getItem('expires_at'), 10); - } + protected validateNonce(nonceInState: string): boolean { + let savedNonce; - protected getAccessTokenStoredAt(): number { - return parseInt(this._storage.getItem('access_token_stored_at'), 10); + if ( + this.saveNoncesInLocalStorage && + typeof window['localStorage'] !== 'undefined' + ) { + savedNonce = localStorage.getItem('nonce'); + } else { + savedNonce = this._storage.getItem('nonce'); + } + + if (savedNonce !== nonceInState) { + const err = 'Validating access_token failed, wrong state/nonce.'; + console.error(err, savedNonce, nonceInState); + return false; + } + return true; + } + + protected storeIdToken(idToken: ParsedIdToken): void { + this._storage.setItem('id_token', idToken.idToken); + this._storage.setItem('id_token_claims_obj', idToken.idTokenClaimsJson); + this._storage.setItem('id_token_expires_at', '' + idToken.idTokenExpiresAt); + this._storage.setItem('id_token_stored_at', '' + Date.now()); + } + + protected storeSessionState(sessionState: string): void { + this._storage.setItem('session_state', sessionState); + } + + protected getSessionState(): string { + return this._storage.getItem('session_state'); + } + + protected handleLoginError(options: LoginOptions, parts: object): void { + if (options.onLoginError) { + options.onLoginError(parts); + } + if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) { + location.hash = ''; + } + } + + /** + * @ignore + */ + public processIdToken( + idToken: string, + accessToken: string, + skipNonceCheck = false + ): Promise { + const tokenParts = idToken.split('.'); + const headerBase64 = this.padBase64(tokenParts[0]); + const headerJson = b64DecodeUnicode(headerBase64); + const header = JSON.parse(headerJson); + const claimsBase64 = this.padBase64(tokenParts[1]); + const claimsJson = b64DecodeUnicode(claimsBase64); + const claims = JSON.parse(claimsJson); + + let savedNonce; + if ( + this.saveNoncesInLocalStorage && + typeof window['localStorage'] !== 'undefined' + ) { + savedNonce = localStorage.getItem('nonce'); + } else { + savedNonce = this._storage.getItem('nonce'); } - protected getIdTokenStoredAt(): number { - return parseInt(this._storage.getItem('id_token_stored_at'), 10); + if (Array.isArray(claims.aud)) { + if (claims.aud.every(v => v !== this.clientId)) { + const err = 'Wrong audience: ' + claims.aud.join(','); + this.logger.warn(err); + return Promise.reject(err); + } + } else { + if (claims.aud !== this.clientId) { + const err = 'Wrong audience: ' + claims.aud; + this.logger.warn(err); + return Promise.reject(err); + } } - /** - * Returns the expiration date of the id_token - * as milliseconds since 1970. - */ - public getIdTokenExpiration(): number { - if (!this._storage.getItem('id_token_expires_at')) { - return null; - } - - return parseInt(this._storage.getItem('id_token_expires_at'), 10); + if (!claims.sub) { + const err = 'No sub claim in id_token'; + this.logger.warn(err); + return Promise.reject(err); } - /** - * Checkes, whether there is a valid access_token. + /* For now, we only check whether the sub against + * silentRefreshSubject when sessionChecksEnabled is on + * We will reconsider in a later version to do this + * in every other case too. */ - public hasValidAccessToken(): boolean { - if (this.getAccessToken()) { - const expiresAt = this._storage.getItem('expires_at'); - const now = new Date(); - if (expiresAt && parseInt(expiresAt, 10) < now.getTime()) { - return false; - } - - return true; - } + if ( + this.sessionChecksEnabled && + this.silentRefreshSubject && + this.silentRefreshSubject !== claims['sub'] + ) { + const err = + 'After refreshing, we got an id_token for another user (sub). ' + + `Expected sub: ${this.silentRefreshSubject}, received sub: ${claims['sub']}`; - return false; + this.logger.warn(err); + return Promise.reject(err); } - /** - * Checks whether there is a valid id_token. - */ - public hasValidIdToken(): boolean { - if (this.getIdToken()) { - const expiresAt = this._storage.getItem('id_token_expires_at'); - const now = new Date(); - if (expiresAt && parseInt(expiresAt, 10) < now.getTime()) { - return false; - } - - return true; - } - - return false; + if (!claims.iat) { + const err = 'No iat claim in id_token'; + this.logger.warn(err); + return Promise.reject(err); } - /** - * Retrieve a saved custom property of the TokenReponse object. Only if predefined in authconfig. - */ - public getCustomTokenResponseProperty(requestedProperty: string): any { - return this._storage && this.config.customTokenParameters - && (this.config.customTokenParameters.indexOf(requestedProperty) >= 0) - && this._storage.getItem(requestedProperty) !== null - ? JSON.parse(this._storage.getItem(requestedProperty)) : null; + if (!this.skipIssuerCheck && claims.iss !== this.issuer) { + const err = 'Wrong issuer: ' + claims.iss; + this.logger.warn(err); + return Promise.reject(err); } - /** - * Returns the auth-header that can be used - * to transmit the access_token to a service - */ - public authorizationHeader(): string { - return 'Bearer ' + this.getAccessToken(); + if (!skipNonceCheck && claims.nonce !== savedNonce) { + const err = 'Wrong nonce: ' + claims.nonce; + this.logger.warn(err); + return Promise.reject(err); } - - /** - * Removes all tokens and logs the user out. - * If a logout url is configured, the user is - * redirected to it with optional state parameter. - * @param noRedirectToLogoutUrl - * @param state - */ - public logOut(noRedirectToLogoutUrl = false, state = ''): void { - const id_token = this.getIdToken(); - this._storage.removeItem('access_token'); - this._storage.removeItem('id_token'); - this._storage.removeItem('refresh_token'); - - if (this.saveNoncesInLocalStorage) { - localStorage.removeItem('nonce'); - localStorage.removeItem('PKCI_verifier'); - } else { - this._storage.removeItem('nonce'); - this._storage.removeItem('PKCI_verifier'); - } - - this._storage.removeItem('expires_at'); - this._storage.removeItem('id_token_claims_obj'); - this._storage.removeItem('id_token_expires_at'); - this._storage.removeItem('id_token_stored_at'); - this._storage.removeItem('access_token_stored_at'); - this._storage.removeItem('granted_scopes'); - this._storage.removeItem('session_state'); - if (this.config.customTokenParameters) { - this.config.customTokenParameters.forEach(customParam => this._storage.removeItem(customParam)); - } - this.silentRefreshSubject = null; - - this.eventsSubject.next(new OAuthInfoEvent('logout')); - - if (!this.logoutUrl) { - return; - } - if (noRedirectToLogoutUrl) { - return; - } - - if (!id_token && !this.postLogoutRedirectUri) { - return; - } - - let logoutUrl: string; - - if (!this.validateUrlForHttps(this.logoutUrl)) { - throw new Error( - 'logoutUrl must use HTTPS (with TLS), or config value for property \'requireHttps\' must be set to \'false\' and allow HTTP (without TLS).' - ); - } - - // For backward compatibility - if (this.logoutUrl.indexOf('{{') > -1) { - logoutUrl = this.logoutUrl - .replace(/\{\{id_token\}\}/, id_token) - .replace(/\{\{client_id\}\}/, this.clientId); - } else { - - let params = new HttpParams(); - - if (id_token) { - params = params.set('id_token_hint', id_token); - } - - const postLogoutUrl = this.postLogoutRedirectUri || this.redirectUri; - if (postLogoutUrl) { - params = params.set('post_logout_redirect_uri', postLogoutUrl); - - if (state) { - params = params.set('state', state); - } - } - - logoutUrl = - this.logoutUrl + - (this.logoutUrl.indexOf('?') > -1 ? '&' : '?') + - params.toString(); - } - this.config.openUri(logoutUrl); + // at_hash is not applicable to authorization code flow + // addressing https://github.com/manfredsteyer/angular-oauth2-oidc/issues/661 + // i.e. Based on spec the at_hash check is only true for implicit code flow on Ping Federate + // https://www.pingidentity.com/developer/en/resources/openid-connect-developers-guide.html + if (this.hasOwnProperty('responseType') && this.responseType === 'code') { + this.disableAtHashCheck = true; } - - /** - * @ignore - */ - public createAndSaveNonce(): Promise { - const that = this; - return this.createNonce().then(function (nonce: any) { - // Use localStorage for nonce if possible - // localStorage is the only storage who survives a - // redirect in ALL browsers (also IE) - // Otherwiese we'd force teams who have to support - // IE into using localStorage for everything - if (that.saveNoncesInLocalStorage && - typeof window['localStorage'] !== 'undefined') { - localStorage.setItem('nonce', nonce); - } else { - that._storage.setItem('nonce', nonce); - } - return nonce; - }); + if ( + !this.disableAtHashCheck && + this.requestAccessToken && + !claims['at_hash'] + ) { + const err = 'An at_hash is needed!'; + this.logger.warn(err); + return Promise.reject(err); } - /** - * @ignore - */ - public ngOnDestroy(): void { - this.clearAccessTokenTimer(); - this.clearIdTokenTimer(); + const now = Date.now(); + const issuedAtMSec = claims.iat * 1000; + const expiresAtMSec = claims.exp * 1000; + const clockSkewInMSec = (this.clockSkewInSec || 600) * 1000; - this.removeSilentRefreshEventListener(); - const silentRefreshFrame = this.document.getElementById(this.silentRefreshIFrameName); - if (silentRefreshFrame) { - silentRefreshFrame.remove(); - } - - this.stopSessionCheckTimer(); - this.removeSessionCheckEventListener(); - const sessionCheckFrame = this.document.getElementById(this.sessionCheckIFrameName); - if (sessionCheckFrame) { - sessionCheckFrame.remove(); - } + if ( + issuedAtMSec - clockSkewInMSec >= now || + expiresAtMSec + clockSkewInMSec <= now + ) { + const err = 'Token has expired'; + console.error(err); + console.error({ + now: now, + issuedAtMSec: issuedAtMSec, + expiresAtMSec: expiresAtMSec + }); + return Promise.reject(err); + } + + const validationParams: ValidationParams = { + accessToken: accessToken, + idToken: idToken, + jwks: this.jwks, + idTokenClaims: claims, + idTokenHeader: header, + loadKeys: () => this.loadJwks() + }; + + if (this.disableAtHashCheck) { + return this.checkSignature(validationParams).then(_ => { + const result: ParsedIdToken = { + idToken: idToken, + idTokenClaims: claims, + idTokenClaimsJson: claimsJson, + idTokenHeader: header, + idTokenHeaderJson: headerJson, + idTokenExpiresAt: expiresAtMSec + }; + return result; + }); } - protected createNonce(): Promise { - return new Promise((resolve) => { - if (this.rngUrl) { - throw new Error( - 'createNonce with rng-web-api has not been implemented so far' - ); - } + return this.checkAtHash(validationParams).then(atHashValid => { + if (!this.disableAtHashCheck && this.requestAccessToken && !atHashValid) { + const err = 'Wrong at_hash'; + this.logger.warn(err); + return Promise.reject(err); + } - /* - * This alphabet is from: - * https://tools.ietf.org/html/rfc7636#section-4.1 - * - * [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" - */ - const unreserved = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; - let size = 45; - let id = ''; - - const crypto = typeof self === 'undefined' ? null : (self.crypto || self['msCrypto']); - if (crypto) { - let bytes = new Uint8Array(size); - crypto.getRandomValues(bytes); - - // Needed for IE - if (!bytes.map) { - (bytes as any).map = Array.prototype.map; - } - - bytes = bytes.map(x => unreserved.charCodeAt(x % unreserved.length)); - id = String.fromCharCode.apply(null, bytes); + return this.checkSignature(validationParams).then(_ => { + const atHashCheckEnabled = !this.disableAtHashCheck; + const result: ParsedIdToken = { + idToken: idToken, + idTokenClaims: claims, + idTokenClaimsJson: claimsJson, + idTokenHeader: header, + idTokenHeaderJson: headerJson, + idTokenExpiresAt: expiresAtMSec + }; + if (atHashCheckEnabled) { + return this.checkAtHash(validationParams).then(atHashValid => { + if (this.requestAccessToken && !atHashValid) { + const err = 'Wrong at_hash'; + this.logger.warn(err); + return Promise.reject(err); } else { - while (0 < size--) { - id += unreserved[Math.random() * unreserved.length | 0]; - } + return result; } - - resolve(base64UrlEncode(id)); - }); - } - - protected async checkAtHash(params: ValidationParams): Promise { - if (!this.tokenValidationHandler) { - this.logger.warn( - 'No tokenValidationHandler configured. Cannot check at_hash.' - ); - return true; + }); + } else { + return result; } - return this.tokenValidationHandler.validateAtHash(params); - } + }); + }); + } + + /** + * Returns the received claims about the user. + */ + public getIdentityClaims(): object { + const claims = this._storage.getItem('id_token_claims_obj'); + if (!claims) { + return null; + } + return JSON.parse(claims); + } + + /** + * Returns the granted scopes from the server. + */ + public getGrantedScopes(): object { + const scopes = this._storage.getItem('granted_scopes'); + if (!scopes) { + return null; + } + return JSON.parse(scopes); + } + + /** + * Returns the current id_token. + */ + public getIdToken(): string { + return this._storage ? this._storage.getItem('id_token') : null; + } + + protected padBase64(base64data): string { + while (base64data.length % 4 !== 0) { + base64data += '='; + } + return base64data; + } + + /** + * Returns the current access_token. + */ + public getAccessToken(): string { + return this._storage ? this._storage.getItem('access_token') : null; + } + + public getRefreshToken(): string { + return this._storage ? this._storage.getItem('refresh_token') : null; + } + + /** + * Returns the expiration date of the access_token + * as milliseconds since 1970. + */ + public getAccessTokenExpiration(): number { + if (!this._storage.getItem('expires_at')) { + return null; + } + return parseInt(this._storage.getItem('expires_at'), 10); + } + + protected getAccessTokenStoredAt(): number { + return parseInt(this._storage.getItem('access_token_stored_at'), 10); + } + + protected getIdTokenStoredAt(): number { + return parseInt(this._storage.getItem('id_token_stored_at'), 10); + } + + /** + * Returns the expiration date of the id_token + * as milliseconds since 1970. + */ + public getIdTokenExpiration(): number { + if (!this._storage.getItem('id_token_expires_at')) { + return null; + } + + return parseInt(this._storage.getItem('id_token_expires_at'), 10); + } + + /** + * Checkes, whether there is a valid access_token. + */ + public hasValidAccessToken(): boolean { + if (this.getAccessToken()) { + const expiresAt = this._storage.getItem('expires_at'); + const now = new Date(); + if (expiresAt && parseInt(expiresAt, 10) < now.getTime()) { + return false; + } - protected checkSignature(params: ValidationParams): Promise { - if (!this.tokenValidationHandler) { - this.logger.warn( - 'No tokenValidationHandler configured. Cannot check signature.' - ); - return Promise.resolve(null); - } - return this.tokenValidationHandler.validateSignature(params); + return true; } + return false; + } - /** - * Start the implicit flow or the code flow, - * depending on your configuration. - */ - public initLoginFlow( - additionalState = '', - params = {} - ): void { - if (this.responseType === 'code') { - return this.initCodeFlow(additionalState, params); - } else { - return this.initImplicitFlow(additionalState, params); - } - } + /** + * Checks whether there is a valid id_token. + */ + public hasValidIdToken(): boolean { + if (this.getIdToken()) { + const expiresAt = this._storage.getItem('id_token_expires_at'); + const now = new Date(); + if (expiresAt && parseInt(expiresAt, 10) < now.getTime()) { + return false; + } - /** - * Starts the authorization code flow and redirects to user to - * the auth servers login url. - */ - public initCodeFlow( - additionalState = '', - params = {} - ): void { - if (this.loginUrl !== '') { - this.initCodeFlowInternal(additionalState, params); - } else { - this.events.pipe(filter(e => e.type === 'discovery_document_loaded')) - .subscribe(_ => this.initCodeFlowInternal(additionalState, params)); - } - } + return true; + } + + return false; + } + + /** + * Retrieve a saved custom property of the TokenReponse object. Only if predefined in authconfig. + */ + public getCustomTokenResponseProperty(requestedProperty: string): any { + return this._storage && + this.config.customTokenParameters && + this.config.customTokenParameters.indexOf(requestedProperty) >= 0 && + this._storage.getItem(requestedProperty) !== null + ? JSON.parse(this._storage.getItem(requestedProperty)) + : null; + } + + /** + * Returns the auth-header that can be used + * to transmit the access_token to a service + */ + public authorizationHeader(): string { + return 'Bearer ' + this.getAccessToken(); + } + + /** + * Removes all tokens and logs the user out. + * If a logout url is configured, the user is + * redirected to it with optional state parameter. + * @param noRedirectToLogoutUrl + * @param state + */ + public logOut(noRedirectToLogoutUrl = false, state = ''): void { + const id_token = this.getIdToken(); + this._storage.removeItem('access_token'); + this._storage.removeItem('id_token'); + this._storage.removeItem('refresh_token'); + + if (this.saveNoncesInLocalStorage) { + localStorage.removeItem('nonce'); + localStorage.removeItem('PKCI_verifier'); + } else { + this._storage.removeItem('nonce'); + this._storage.removeItem('PKCI_verifier'); + } + + this._storage.removeItem('expires_at'); + this._storage.removeItem('id_token_claims_obj'); + this._storage.removeItem('id_token_expires_at'); + this._storage.removeItem('id_token_stored_at'); + this._storage.removeItem('access_token_stored_at'); + this._storage.removeItem('granted_scopes'); + this._storage.removeItem('session_state'); + if (this.config.customTokenParameters) { + this.config.customTokenParameters.forEach(customParam => + this._storage.removeItem(customParam) + ); + } + this.silentRefreshSubject = null; + + this.eventsSubject.next(new OAuthInfoEvent('logout')); + + if (!this.logoutUrl) { + return; + } + if (noRedirectToLogoutUrl) { + return; + } + + if (!id_token && !this.postLogoutRedirectUri) { + return; + } + + let logoutUrl: string; + + if (!this.validateUrlForHttps(this.logoutUrl)) { + throw new Error( + "logoutUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)." + ); + } + + // For backward compatibility + if (this.logoutUrl.indexOf('{{') > -1) { + logoutUrl = this.logoutUrl + .replace(/\{\{id_token\}\}/, id_token) + .replace(/\{\{client_id\}\}/, this.clientId); + } else { + let params = new HttpParams(); + + if (id_token) { + params = params.set('id_token_hint', id_token); + } - private initCodeFlowInternal( - additionalState = '', - params = {} - ): void { + const postLogoutUrl = this.postLogoutRedirectUri || this.redirectUri; + if (postLogoutUrl) { + params = params.set('post_logout_redirect_uri', postLogoutUrl); - if (!this.validateUrlForHttps(this.loginUrl)) { - throw new Error('loginUrl must use HTTPS (with TLS), or config value for property \'requireHttps\' must be set to \'false\' and allow HTTP (without TLS).'); + if (state) { + params = params.set('state', state); } + } - this.createLoginUrl(additionalState, '', null, false, params) - .then(this.config.openUri) - .catch(error => { - console.error('Error in initAuthorizationCodeFlow'); - console.error(error); - }); - } - - protected async createChallangeVerifierPairForPKCE(): Promise<[string, string]> { + logoutUrl = + this.logoutUrl + + (this.logoutUrl.indexOf('?') > -1 ? '&' : '?') + + params.toString(); + } + this.config.openUri(logoutUrl); + } + + /** + * @ignore + */ + public createAndSaveNonce(): Promise { + const that = this; + return this.createNonce().then(function(nonce: any) { + // Use localStorage for nonce if possible + // localStorage is the only storage who survives a + // redirect in ALL browsers (also IE) + // Otherwiese we'd force teams who have to support + // IE into using localStorage for everything + if ( + that.saveNoncesInLocalStorage && + typeof window['localStorage'] !== 'undefined' + ) { + localStorage.setItem('nonce', nonce); + } else { + that._storage.setItem('nonce', nonce); + } + return nonce; + }); + } + + /** + * @ignore + */ + public ngOnDestroy(): void { + this.clearAccessTokenTimer(); + this.clearIdTokenTimer(); + + this.removeSilentRefreshEventListener(); + const silentRefreshFrame = this.document.getElementById( + this.silentRefreshIFrameName + ); + if (silentRefreshFrame) { + silentRefreshFrame.remove(); + } + + this.stopSessionCheckTimer(); + this.removeSessionCheckEventListener(); + const sessionCheckFrame = this.document.getElementById( + this.sessionCheckIFrameName + ); + if (sessionCheckFrame) { + sessionCheckFrame.remove(); + } + } + + protected createNonce(): Promise { + return new Promise(resolve => { + if (this.rngUrl) { + throw new Error( + 'createNonce with rng-web-api has not been implemented so far' + ); + } - if (!this.crypto) { - throw new Error('PKCE support for code flow needs a CryptoHander. Did you import the OAuthModule using forRoot() ?'); + /* + * This alphabet is from: + * https://tools.ietf.org/html/rfc7636#section-4.1 + * + * [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" + */ + const unreserved = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + let size = 45; + let id = ''; + + const crypto = + typeof self === 'undefined' ? null : self.crypto || self['msCrypto']; + if (crypto) { + let bytes = new Uint8Array(size); + crypto.getRandomValues(bytes); + + // Needed for IE + if (!bytes.map) { + (bytes as any).map = Array.prototype.map; + } + + bytes = bytes.map(x => unreserved.charCodeAt(x % unreserved.length)); + id = String.fromCharCode.apply(null, bytes); + } else { + while (0 < size--) { + id += unreserved[(Math.random() * unreserved.length) | 0]; } + } - const verifier = await this.createNonce(); - const challengeRaw = await this.crypto.calcHash(verifier, 'sha-256'); - const challenge = base64UrlEncode(challengeRaw); + resolve(base64UrlEncode(id)); + }); + } + + protected async checkAtHash(params: ValidationParams): Promise { + if (!this.tokenValidationHandler) { + this.logger.warn( + 'No tokenValidationHandler configured. Cannot check at_hash.' + ); + return true; + } + return this.tokenValidationHandler.validateAtHash(params); + } + + protected checkSignature(params: ValidationParams): Promise { + if (!this.tokenValidationHandler) { + this.logger.warn( + 'No tokenValidationHandler configured. Cannot check signature.' + ); + return Promise.resolve(null); + } + return this.tokenValidationHandler.validateSignature(params); + } + + /** + * Start the implicit flow or the code flow, + * depending on your configuration. + */ + public initLoginFlow(additionalState = '', params = {}): void { + if (this.responseType === 'code') { + return this.initCodeFlow(additionalState, params); + } else { + return this.initImplicitFlow(additionalState, params); + } + } + + /** + * Starts the authorization code flow and redirects to user to + * the auth servers login url. + */ + public initCodeFlow(additionalState = '', params = {}): void { + if (this.loginUrl !== '') { + this.initCodeFlowInternal(additionalState, params); + } else { + this.events + .pipe(filter(e => e.type === 'discovery_document_loaded')) + .subscribe(_ => this.initCodeFlowInternal(additionalState, params)); + } + } + + private initCodeFlowInternal(additionalState = '', params = {}): void { + if (!this.validateUrlForHttps(this.loginUrl)) { + throw new Error( + "loginUrl must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)." + ); + } + + this.createLoginUrl(additionalState, '', null, false, params) + .then(this.config.openUri) + .catch(error => { + console.error('Error in initAuthorizationCodeFlow'); + console.error(error); + }); + } - return [challenge, verifier]; + protected async createChallangeVerifierPairForPKCE(): Promise< + [string, string] + > { + if (!this.crypto) { + throw new Error( + 'PKCE support for code flow needs a CryptoHander. Did you import the OAuthModule using forRoot() ?' + ); } - private extractRecognizedCustomParameters(tokenResponse: TokenResponse): Map { - let foundParameters: Map = new Map(); - if (!this.config.customTokenParameters) { - return foundParameters; - } - this.config.customTokenParameters.forEach((recognizedParameter: string) => { - if (tokenResponse[recognizedParameter]) { - foundParameters.set(recognizedParameter, JSON.stringify(tokenResponse[recognizedParameter])); - } - }); + const verifier = await this.createNonce(); + const challengeRaw = await this.crypto.calcHash(verifier, 'sha-256'); + const challenge = base64UrlEncode(challengeRaw); + + return [challenge, verifier]; + } + + private extractRecognizedCustomParameters( + tokenResponse: TokenResponse + ): Map { + let foundParameters: Map = new Map(); + if (!this.config.customTokenParameters) { return foundParameters; } + this.config.customTokenParameters.forEach((recognizedParameter: string) => { + if (tokenResponse[recognizedParameter]) { + foundParameters.set( + recognizedParameter, + JSON.stringify(tokenResponse[recognizedParameter]) + ); + } + }); + return foundParameters; + } } diff --git a/projects/lib/src/token-validation/hash-handler.ts b/projects/lib/src/token-validation/hash-handler.ts index 17d423f1..da82b1e0 100644 --- a/projects/lib/src/token-validation/hash-handler.ts +++ b/projects/lib/src/token-validation/hash-handler.ts @@ -2,65 +2,62 @@ import { Injectable } from '@angular/core'; import { sha256 } from 'js-sha256'; - /** * Abstraction for crypto algorithms -*/ + */ export abstract class HashHandler { - abstract calcHash(valueToHash: string, algorithm: string): Promise; + abstract calcHash(valueToHash: string, algorithm: string): Promise; } @Injectable() export class DefaultHashHandler implements HashHandler { - - async calcHash(valueToHash: string, algorithm: string): Promise { - // const encoder = new TextEncoder(); - // const hashArray = await window.crypto.subtle.digest(algorithm, data); - // const data = encoder.encode(valueToHash); - - const hashArray = sha256.array(valueToHash); - // const hashString = this.toHashString(hashArray); - const hashString = this.toHashString2(hashArray); - - return hashString; - } - - toHashString2(byteArray: number[]) { - let result = ''; - for (let e of byteArray) { - result += String.fromCharCode(e); - } - return result; + async calcHash(valueToHash: string, algorithm: string): Promise { + // const encoder = new TextEncoder(); + // const hashArray = await window.crypto.subtle.digest(algorithm, data); + // const data = encoder.encode(valueToHash); + + const hashArray = sha256.array(valueToHash); + // const hashString = this.toHashString(hashArray); + const hashString = this.toHashString2(hashArray); + + return hashString; + } + + toHashString2(byteArray: number[]) { + let result = ''; + for (let e of byteArray) { + result += String.fromCharCode(e); } - - toHashString(buffer: ArrayBuffer) { - const byteArray = new Uint8Array(buffer); - let result = ''; - for (let e of byteArray) { - result += String.fromCharCode(e); - } - return result; + return result; + } + + toHashString(buffer: ArrayBuffer) { + const byteArray = new Uint8Array(buffer); + let result = ''; + for (let e of byteArray) { + result += String.fromCharCode(e); } - - // hexString(buffer) { - // const byteArray = new Uint8Array(buffer); - // const hexCodes = [...byteArray].map(value => { - // const hexCode = value.toString(16); - // const paddedHexCode = hexCode.padStart(2, '0'); - // return paddedHexCode; - // }); - - // return hexCodes.join(''); - // } - - // toHashString(hexString: string) { - // let result = ''; - // for (let i = 0; i < hexString.length; i += 2) { - // let hexDigit = hexString.charAt(i) + hexString.charAt(i + 1); - // let num = parseInt(hexDigit, 16); - // result += String.fromCharCode(num); - // } - // return result; - // } - -} \ No newline at end of file + return result; + } + + // hexString(buffer) { + // const byteArray = new Uint8Array(buffer); + // const hexCodes = [...byteArray].map(value => { + // const hexCode = value.toString(16); + // const paddedHexCode = hexCode.padStart(2, '0'); + // return paddedHexCode; + // }); + + // return hexCodes.join(''); + // } + + // toHashString(hexString: string) { + // let result = ''; + // for (let i = 0; i < hexString.length; i += 2) { + // let hexDigit = hexString.charAt(i) + hexString.charAt(i + 1); + // let num = parseInt(hexDigit, 16); + // result += String.fromCharCode(num); + // } + // return result; + // } +} diff --git a/projects/lib/src/token-validation/jwks-validation-handler.ts b/projects/lib/src/token-validation/jwks-validation-handler.ts index 491d746a..0135c8a6 100644 --- a/projects/lib/src/token-validation/jwks-validation-handler.ts +++ b/projects/lib/src/token-validation/jwks-validation-handler.ts @@ -23,10 +23,8 @@ This also results in smaller bundle sizes. * to an library of its own, namely angular-oauth2-oidc-utils */ export class JwksValidationHandler extends NullValidationHandler { - constructor() { super(); console.error(err); } - -} \ No newline at end of file +} diff --git a/projects/lib/src/token-validation/validation-handler.ts b/projects/lib/src/token-validation/validation-handler.ts index fbf4369d..e98f23bd 100644 --- a/projects/lib/src/token-validation/validation-handler.ts +++ b/projects/lib/src/token-validation/validation-handler.ts @@ -24,7 +24,9 @@ export abstract class ValidationHandler { /** * Validates the at_hash in an id_token against the received access_token. */ - public abstract validateAtHash(validationParams: ValidationParams): Promise; + public abstract validateAtHash( + validationParams: ValidationParams + ): Promise; } /** @@ -83,5 +85,8 @@ export abstract class AbstractValidationHandler implements ValidationHandler { * @param valueToHash * @param algorithm */ - protected abstract calcHash(valueToHash: string, algorithm: string): Promise; + protected abstract calcHash( + valueToHash: string, + algorithm: string + ): Promise; } diff --git a/projects/lib/src/types.ts b/projects/lib/src/types.ts index ce9f8e59..249d9ead 100644 --- a/projects/lib/src/types.ts +++ b/projects/lib/src/types.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import { Injectable } from '@angular/core'; /** * Additional options that can be passed to tryLogin. @@ -106,7 +106,6 @@ export class MemoryStorage implements OAuthStorage { setItem(key: string, data: string): void { this.data.set(key, data); } - } /** @@ -138,7 +137,7 @@ export interface ParsedIdToken { */ export interface TokenResponse { access_token: string; - id_token: string; + id_token: string; token_type: string; expires_in: number; refresh_token: string; diff --git a/projects/lib/src/url-helper.service.ts b/projects/lib/src/url-helper.service.ts index 507aed50..4e338e9d 100644 --- a/projects/lib/src/url-helper.service.ts +++ b/projects/lib/src/url-helper.service.ts @@ -24,14 +24,7 @@ export class UrlHelperService { public parseQueryString(queryString: string): object { const data = {}; - let - pairs, - pair, - separatorIndex, - escapedKey, - escapedValue, - key, - value; + let pairs, pair, separatorIndex, escapedKey, escapedValue, key, value; if (queryString === null) { return data; @@ -54,7 +47,9 @@ export class UrlHelperService { key = decodeURIComponent(escapedKey); value = decodeURIComponent(escapedValue); - if (key.substr(0, 1) === '/') { key = key.substr(1); } + if (key.substr(0, 1) === '/') { + key = key.substr(1); + } data[key] = value; } diff --git a/projects/lib/tsconfig.lib.json b/projects/lib/tsconfig.lib.json index 4169056c..b7f9e869 100644 --- a/projects/lib/tsconfig.lib.json +++ b/projects/lib/tsconfig.lib.json @@ -12,13 +12,7 @@ "experimentalDecorators": true, "importHelpers": true, "types": [], - "lib": [ - "dom", - "es2015" - ] + "lib": ["dom", "es2015"] }, - "exclude": [ - "src/test.ts", - "**/*.spec.ts" - ] + "exclude": ["src/test.ts", "**/*.spec.ts"] } diff --git a/projects/lib/tsconfig.lib.prod.json b/projects/lib/tsconfig.lib.prod.json index 50f58136..15a1e1fa 100644 --- a/projects/lib/tsconfig.lib.prod.json +++ b/projects/lib/tsconfig.lib.prod.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.lib.json", "compilerOptions": { - "outDir": "../../out-tsc/lib-prod", + "outDir": "../../out-tsc/lib-prod" } } diff --git a/projects/lib/tsconfig.spec.json b/projects/lib/tsconfig.spec.json index 16da33db..ec3528a8 100644 --- a/projects/lib/tsconfig.spec.json +++ b/projects/lib/tsconfig.spec.json @@ -2,16 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine", - "node" - ] + "types": ["jasmine", "node"] }, - "files": [ - "src/test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] + "files": ["src/test.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] } diff --git a/projects/lib/tslint.json b/projects/lib/tslint.json index 73f120b7..205aedaa 100644 --- a/projects/lib/tslint.json +++ b/projects/lib/tslint.json @@ -1,17 +1,7 @@ { - "extends": "../../tslint.json", - "rules": { - "directive-selector": [ - true, - "attribute", - "lib", - "camelCase" - ], - "component-selector": [ - true, - "element", - "lib", - "kebab-case" - ] - } + "extends": "../../tslint.json", + "rules": { + "directive-selector": [true, "attribute", "lib", "camelCase"], + "component-selector": [true, "element", "lib", "kebab-case"] + } } diff --git a/projects/quickstart-demo/e2e/protractor.conf.js b/projects/quickstart-demo/e2e/protractor.conf.js index 73e4e680..f0620bbe 100644 --- a/projects/quickstart-demo/e2e/protractor.conf.js +++ b/projects/quickstart-demo/e2e/protractor.conf.js @@ -9,11 +9,9 @@ const { SpecReporter } = require('jasmine-spec-reporter'); */ exports.config = { allScriptsTimeout: 11000, - specs: [ - './src/**/*.e2e-spec.ts' - ], + specs: ['./src/**/*.e2e-spec.ts'], capabilities: { - 'browserName': 'chrome' + browserName: 'chrome' }, directConnect: true, baseUrl: 'http://localhost:4200/', @@ -27,6 +25,8 @@ exports.config = { require('ts-node').register({ project: require('path').join(__dirname, './tsconfig.json') }); - jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + jasmine + .getEnv() + .addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); } -}; \ No newline at end of file +}; diff --git a/projects/quickstart-demo/e2e/src/app.e2e-spec.ts b/projects/quickstart-demo/e2e/src/app.e2e-spec.ts index 48f4204c..009b116e 100644 --- a/projects/quickstart-demo/e2e/src/app.e2e-spec.ts +++ b/projects/quickstart-demo/e2e/src/app.e2e-spec.ts @@ -15,9 +15,14 @@ describe('workspace-project App', () => { afterEach(async () => { // Assert that there are no errors emitted from the browser - const logs = await browser.manage().logs().get(logging.Type.BROWSER); - expect(logs).not.toContain(jasmine.objectContaining({ - level: logging.Level.SEVERE, - } as logging.Entry)); + const logs = await browser + .manage() + .logs() + .get(logging.Type.BROWSER); + expect(logs).not.toContain( + jasmine.objectContaining({ + level: logging.Level.SEVERE + } as logging.Entry) + ); }); }); diff --git a/projects/quickstart-demo/e2e/tsconfig.json b/projects/quickstart-demo/e2e/tsconfig.json index bc240fbf..20135143 100644 --- a/projects/quickstart-demo/e2e/tsconfig.json +++ b/projects/quickstart-demo/e2e/tsconfig.json @@ -4,10 +4,6 @@ "outDir": "../../../out-tsc/e2e", "module": "commonjs", "target": "es5", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] + "types": ["jasmine", "jasminewd2", "node"] } } diff --git a/projects/quickstart-demo/karma.conf.js b/projects/quickstart-demo/karma.conf.js index 612419e4..da707d71 100644 --- a/projects/quickstart-demo/karma.conf.js +++ b/projects/quickstart-demo/karma.conf.js @@ -1,7 +1,7 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html -module.exports = function (config) { +module.exports = function(config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], diff --git a/projects/quickstart-demo/src/app/app.component.html b/projects/quickstart-demo/src/app/app.component.html index 30587c8d..d3cf440a 100644 --- a/projects/quickstart-demo/src/app/app.component.html +++ b/projects/quickstart-demo/src/app/app.component.html @@ -1,9 +1,7 @@
-

- Welcome to {{ title }}! -

+

Welcome to {{ title }}!

User

diff --git a/projects/quickstart-demo/src/app/app.component.spec.ts b/projects/quickstart-demo/src/app/app.component.spec.ts index b1d3d44c..2d4a3286 100644 --- a/projects/quickstart-demo/src/app/app.component.spec.ts +++ b/projects/quickstart-demo/src/app/app.component.spec.ts @@ -4,9 +4,7 @@ import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ - AppComponent - ], + declarations: [AppComponent] }).compileComponents(); })); @@ -26,6 +24,8 @@ describe('AppComponent', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; - expect(compiled.querySelector('h1').textContent).toContain('Welcome to quickstart-demo!'); + expect(compiled.querySelector('h1').textContent).toContain( + 'Welcome to quickstart-demo!' + ); }); }); diff --git a/projects/quickstart-demo/src/app/app.component.ts b/projects/quickstart-demo/src/app/app.component.ts index f8f7cda5..531fd34c 100644 --- a/projects/quickstart-demo/src/app/app.component.ts +++ b/projects/quickstart-demo/src/app/app.component.ts @@ -19,10 +19,9 @@ export class AppComponent { //this.oauthService.setupAutomaticSilentRefresh(); // Automatically load user profile - this.oauthService - .events - .pipe(filter(e => e.type === 'token_received')) - .subscribe(_ => this.oauthService.loadUserProfile()); + this.oauthService.events + .pipe(filter(e => e.type === 'token_received')) + .subscribe(_ => this.oauthService.loadUserProfile()); } get userName(): string { @@ -42,5 +41,4 @@ export class AppComponent { refresh() { this.oauthService.refreshToken(); } - } diff --git a/projects/quickstart-demo/src/app/app.module.ts b/projects/quickstart-demo/src/app/app.module.ts index 75954e99..c31b5799 100644 --- a/projects/quickstart-demo/src/app/app.module.ts +++ b/projects/quickstart-demo/src/app/app.module.ts @@ -6,17 +6,11 @@ import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ - imports: [ - BrowserModule, - OAuthModule.forRoot(), - HttpClientModule - ], - declarations: [ - AppComponent - ], + imports: [BrowserModule, OAuthModule.forRoot(), HttpClientModule], + declarations: [AppComponent], providers: [ // { provide: OAuthStorage, useValue: localStorage } ], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule {} diff --git a/projects/quickstart-demo/src/app/auth.config.ts b/projects/quickstart-demo/src/app/auth.config.ts index e8fb1fda..93312ff2 100644 --- a/projects/quickstart-demo/src/app/auth.config.ts +++ b/projects/quickstart-demo/src/app/auth.config.ts @@ -7,5 +7,5 @@ export const authCodeFlowConfig: AuthConfig = { responseType: 'code', scope: 'openid profile email offline_access api', showDebugInformation: true, - timeoutFactor: 0.01, + timeoutFactor: 0.01 }; diff --git a/projects/quickstart-demo/src/index.html b/projects/quickstart-demo/src/index.html index ba8595b4..bb93d4c5 100644 --- a/projects/quickstart-demo/src/index.html +++ b/projects/quickstart-demo/src/index.html @@ -1,14 +1,14 @@ - + - - - QuickstartDemo - + + + QuickstartDemo + - - - - - - + + + + + + diff --git a/projects/quickstart-demo/src/main.ts b/projects/quickstart-demo/src/main.ts index c7b673cf..fa4e0aef 100644 --- a/projects/quickstart-demo/src/main.ts +++ b/projects/quickstart-demo/src/main.ts @@ -8,5 +8,6 @@ if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule) +platformBrowserDynamic() + .bootstrapModule(AppModule) .catch(err => console.error(err)); diff --git a/projects/quickstart-demo/src/polyfills.ts b/projects/quickstart-demo/src/polyfills.ts index aa665d6b..2f258e56 100644 --- a/projects/quickstart-demo/src/polyfills.ts +++ b/projects/quickstart-demo/src/polyfills.ts @@ -55,8 +55,7 @@ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js/dist/zone'; // Included with Angular CLI. - +import 'zone.js/dist/zone'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS diff --git a/projects/quickstart-demo/tsconfig.app.json b/projects/quickstart-demo/tsconfig.app.json index 57fc3cbc..f10ee2ef 100644 --- a/projects/quickstart-demo/tsconfig.app.json +++ b/projects/quickstart-demo/tsconfig.app.json @@ -4,11 +4,6 @@ "outDir": "../../out-tsc/app", "types": [] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/test.ts", - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/test.ts", "src/**/*.spec.ts"] } diff --git a/projects/quickstart-demo/tsconfig.spec.json b/projects/quickstart-demo/tsconfig.spec.json index a8ce1d39..8eec07d4 100644 --- a/projects/quickstart-demo/tsconfig.spec.json +++ b/projects/quickstart-demo/tsconfig.spec.json @@ -2,17 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine", - "node" - ] + "types": ["jasmine", "node"] }, - "files": [ - "src/test.ts", - "src/polyfills.ts" - ], - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] + "files": ["src/test.ts", "src/polyfills.ts"], + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] } diff --git a/projects/quickstart-demo/tslint.json b/projects/quickstart-demo/tslint.json index 19e8161a..8006e74e 100644 --- a/projects/quickstart-demo/tslint.json +++ b/projects/quickstart-demo/tslint.json @@ -1,17 +1,7 @@ { "extends": "../../tslint.json", "rules": { - "directive-selector": [ - true, - "attribute", - "app", - "camelCase" - ], - "component-selector": [ - true, - "element", - "app", - "kebab-case" - ] + "directive-selector": [true, "attribute", "app", "camelCase"], + "component-selector": [true, "element", "app", "kebab-case"] } } diff --git a/projects/sample/karma.conf.js b/projects/sample/karma.conf.js index b2417fde..1a4dd5cf 100644 --- a/projects/sample/karma.conf.js +++ b/projects/sample/karma.conf.js @@ -1,7 +1,7 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html -module.exports = function (config) { +module.exports = function(config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], @@ -28,4 +28,4 @@ module.exports = function (config) { browsers: ['Chrome'], singleRun: false }); -}; \ No newline at end of file +}; diff --git a/projects/sample/src/app/app.component.html b/projects/sample/src/app/app.component.html index 83d37c99..d713365b 100644 --- a/projects/sample/src/app/app.component.html +++ b/projects/sample/src/app/app.component.html @@ -1,21 +1,21 @@
+
+ +
-
- -
- -
- -
- +
+ +
- \ No newline at end of file + diff --git a/projects/sample/src/app/app.component.spec.ts b/projects/sample/src/app/app.component.spec.ts index 06a39e2b..eca3de3e 100644 --- a/projects/sample/src/app/app.component.spec.ts +++ b/projects/sample/src/app/app.component.spec.ts @@ -6,9 +6,7 @@ import { AppModule } from './app.module'; describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - AppModule - ] + imports: [AppModule] }).compileComponents(); })); diff --git a/projects/sample/src/app/app.component.ts b/projects/sample/src/app/app.component.ts index 03fcb670..b7694944 100644 --- a/projects/sample/src/app/app.component.ts +++ b/projects/sample/src/app/app.component.ts @@ -3,7 +3,7 @@ import { authConfig } from './auth.config'; import { Component } from '@angular/core'; import { OAuthService, NullValidationHandler } from 'angular-oauth2-oidc'; import { Router } from '@angular/router'; -import { filter} from 'rxjs/operators'; +import { filter } from 'rxjs/operators'; import { authCodeFlowConfig } from './auth-code-flow.config'; import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks'; import { useHash } from '../flags'; @@ -15,7 +15,6 @@ import { useHash } from '../flags'; }) export class AppComponent { constructor(private router: Router, private oauthService: OAuthService) { - // Remember the selected configuration if (sessionStorage.getItem('flow') === 'code') { this.configureCodeFlow(); @@ -33,17 +32,14 @@ export class AppComponent { } private configureCodeFlow() { - this.oauthService.configure(authCodeFlowConfig); this.oauthService.loadDiscoveryDocumentAndTryLogin(); // Optional // this.oauthService.setupAutomaticSilentRefresh(); - } private configureImplicitFlow() { - this.oauthService.configure(authConfig); // this.oauthService.setStorage(localStorage); this.oauthService.tokenValidationHandler = new JwksValidationHandler(); @@ -71,7 +67,7 @@ export class AppComponent { }); } - // + // // Below you find further examples for configuration functions // diff --git a/projects/sample/src/app/app.module.ts b/projects/sample/src/app/app.module.ts index 6597b066..30fa412d 100644 --- a/projects/sample/src/app/app.module.ts +++ b/projects/sample/src/app/app.module.ts @@ -16,7 +16,6 @@ import { CustomPreloadingStrategy } from './shared/preload/custom-preloading.str import { LocationStrategy, HashLocationStrategy } from '@angular/common'; import { useHash } from '../flags'; - const ROUTING_OPTIONS: ExtraOptions = { // preloadingStrategy: CustomPreloadingStrategy, useHash: useHash, @@ -32,7 +31,7 @@ const ROUTING_OPTIONS: ExtraOptions = { HttpClientModule, SharedModule.forRoot(), OAuthModule.forRoot({ - resourceServer: { + resourceServer: { allowedUrls: ['http://www.angular.at/api'], sendAccessToken: true } @@ -42,7 +41,7 @@ const ROUTING_OPTIONS: ExtraOptions = { AppComponent, HomeComponent, FlightHistoryComponent, - PasswordFlowLoginComponent, + PasswordFlowLoginComponent ], providers: [ // (useHash) ? { provide: LocationStrategy, useClass: HashLocationStrategy } : [], diff --git a/projects/sample/src/app/app.routes.ts b/projects/sample/src/app/app.routes.ts index 90e6f1f8..027229e7 100644 --- a/projects/sample/src/app/app.routes.ts +++ b/projects/sample/src/app/app.routes.ts @@ -19,7 +19,10 @@ export let APP_ROUTES: Routes = [ }, { path: 'flight-booking', - loadChildren: () => import('./flight-booking/flight-booking.module').then(mod => mod.FlightBookingModule) + loadChildren: () => + import('./flight-booking/flight-booking.module').then( + mod => mod.FlightBookingModule + ) }, { path: 'history', diff --git a/projects/sample/src/app/auth-code-flow.config.ts b/projects/sample/src/app/auth-code-flow.config.ts index 707d3f0a..78ad262c 100644 --- a/projects/sample/src/app/auth-code-flow.config.ts +++ b/projects/sample/src/app/auth-code-flow.config.ts @@ -5,8 +5,9 @@ export const authCodeFlowConfig: AuthConfig = { issuer: 'https://idsvr4.azurewebsites.net', // URL of the SPA to redirect the user to after login - redirectUri: window.location.origin - + ((localStorage.getItem('useHashLocationStrategy') === 'true') + redirectUri: + window.location.origin + + (localStorage.getItem('useHashLocationStrategy') === 'true' ? '/#/index.html' : '/index.html'), @@ -26,19 +27,18 @@ export const authCodeFlowConfig: AuthConfig = { // The first four are defined by OIDC. // Important: Request offline_access to get a refresh token // The api scope is a usecase specific one - scope: (useSilentRefreshForCodeFlow) ? - 'openid profile email api' : - 'openid profile email offline_access api', + scope: useSilentRefreshForCodeFlow + ? 'openid profile email api' + : 'openid profile email offline_access api', - // ^^ Please note that offline_access is not needed for silent refresh - // At least when using idsvr, this even prevents silent refresh - // as idsvr ALWAYS prompts the user for consent when this scope is - // requested + // ^^ Please note that offline_access is not needed for silent refresh + // At least when using idsvr, this even prevents silent refresh + // as idsvr ALWAYS prompts the user for consent when this scope is + // requested // This is needed for silent refresh (refreshing tokens w/o a refresh_token) // **AND** for logging in with a popup - silentRefreshRedirectUri: - `${window.location.origin}/silent-refresh.html`, + silentRefreshRedirectUri: `${window.location.origin}/silent-refresh.html`, useSilentRefresh: useSilentRefreshForCodeFlow, @@ -46,7 +46,6 @@ export const authCodeFlowConfig: AuthConfig = { sessionChecksEnabled: true, - timeoutFactor: 0.01, + timeoutFactor: 0.01 // disablePKCI: true, - }; diff --git a/projects/sample/src/app/auth.config.ts b/projects/sample/src/app/auth.config.ts index 36a65afa..5b9dcdd3 100644 --- a/projects/sample/src/app/auth.config.ts +++ b/projects/sample/src/app/auth.config.ts @@ -28,7 +28,7 @@ export const authConfig: AuthConfig = { showDebugInformation: true, - sessionChecksEnabled: true, + sessionChecksEnabled: true // timeoutFactor: 0.01, }; diff --git a/projects/sample/src/app/flight-booking/alt-flight-card/alt-flight-card.component.html b/projects/sample/src/app/flight-booking/alt-flight-card/alt-flight-card.component.html index 2a3702c3..a2840394 100644 --- a/projects/sample/src/app/flight-booking/alt-flight-card/alt-flight-card.component.html +++ b/projects/sample/src/app/flight-booking/alt-flight-card/alt-flight-card.component.html @@ -1,16 +1,17 @@ -
+
+

{{ item.from }} - {{ item.to }}

+

Flugnr. #{{ item.id }}

+

Datum: {{ item.date | date: 'dd.MM.yyyy HH:mm' }}

-

{{item.from}} - {{item.to}}

-

Flugnr. #{{item.id}}

-

Datum: {{item.date | date:'dd.MM.yyyy HH:mm'}}

- -

- - - -

+

+ +

diff --git a/projects/sample/src/app/flight-booking/alt-flight-card/flight-list.ts b/projects/sample/src/app/flight-booking/alt-flight-card/flight-list.ts index 6fd53ec6..2dda6657 100644 --- a/projects/sample/src/app/flight-booking/alt-flight-card/flight-list.ts +++ b/projects/sample/src/app/flight-booking/alt-flight-card/flight-list.ts @@ -3,16 +3,17 @@ import { Flight } from '../../entities/flight'; @Component({ selector: 'flight-list', template: ` -
-
- - -
-
- ` +
+
+ + +
+
+ ` }) export class FlightListComponent { @Input() flights: Flight[] = []; diff --git a/projects/sample/src/app/flight-booking/flight-booking.component.html b/projects/sample/src/app/flight-booking/flight-booking.component.html index 2d531679..038e9915 100644 --- a/projects/sample/src/app/flight-booking/flight-booking.component.html +++ b/projects/sample/src/app/flight-booking/flight-booking.component.html @@ -1,11 +1,10 @@ -
- -
\ No newline at end of file + +
diff --git a/projects/sample/src/app/flight-booking/flight-card/flight-card.component.html b/projects/sample/src/app/flight-booking/flight-card/flight-card.component.html index c372547c..85026747 100644 --- a/projects/sample/src/app/flight-booking/flight-card/flight-card.component.html +++ b/projects/sample/src/app/flight-booking/flight-card/flight-card.component.html @@ -1,17 +1,21 @@ -
+
+

{{ item.from }} - {{ item.to }}

+

Flugnr. #{{ item.id }}

+

Datum: {{ item.date | date: 'dd.MM.yyyy HH:mm' }}

-

{{item.from}} - {{item.to}}

-

Flugnr. #{{item.id}}

-

Datum: {{item.date | date:'dd.MM.yyyy HH:mm'}}

+

+ -

- - - - -

+ +

diff --git a/projects/sample/src/app/flight-booking/flight-edit/flight-edit.component.ts b/projects/sample/src/app/flight-booking/flight-edit/flight-edit.component.ts index ff9f560e..c01310d2 100644 --- a/projects/sample/src/app/flight-booking/flight-edit/flight-edit.component.ts +++ b/projects/sample/src/app/flight-booking/flight-edit/flight-edit.component.ts @@ -3,23 +3,29 @@ import { ActivatedRoute } from '@angular/router'; @Component({ template: ` -

Flight Edit!

-

Hier könnte auch der Datensatz mit der Id {{id}} stehen!

- -
-
- Daten wurden nicht gespeichert! Trotzdem Maske verlassen? -
-
- Ja - Nein -
-
- +

Flight Edit!

+

Hier könnte auch der Datensatz mit der Id {{ id }} stehen!

- - - ` +
+
+ Daten wurden nicht gespeichert! Trotzdem Maske verlassen? +
+
+ Ja + Nein +
+
+ ` }) export class FlightEditComponent implements OnInit { public id: string; diff --git a/projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.css b/projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.css index 58633484..58741db8 100644 --- a/projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.css +++ b/projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.css @@ -1,13 +1,11 @@ - - input.ng-valid { - border-left-style: solid; - border-left-color: forestgreen; - border-left-width: 5px; + border-left-style: solid; + border-left-color: forestgreen; + border-left-width: 5px; } input.ng-invalid { - border-left-style: solid; - border-left-color: hotpink; - border-left-width: 5px; -} \ No newline at end of file + border-left-style: solid; + border-left-color: hotpink; + border-left-width: 5px; +} diff --git a/projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.html b/projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.html index b07bd6db..2ca31e65 100644 --- a/projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.html +++ b/projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.html @@ -5,46 +5,45 @@

Flight Search (Reactive) !

-->
+

Dynamisches Formular

+
+ + +
-

Dynamisches Formular

-
- - -
- -

Statisches Formular

- -
- - +

Statisches Formular

-
- Validierungsfehler. Bitte prüfen Sie Ihre Eingaben. -
- -
- Diese Stadt wird nicht angefolgen -
- -
- Dieses Feld ist ein Pflichtfeld -
+
+ + +
+ Validierungsfehler. Bitte prüfen Sie Ihre Eingaben.
- -
- - -
-
- +
+ Diese Stadt wird nicht angefolgen
+
+ Dieses Feld ist ein Pflichtfeld +
+
+ +
+ + +
+
+ +
-
- - +
+ - - - - -
+ + +
-
Warenkorb
+  
+Warenkorb
 ----------------------
-{{selectedFlight | json}}
-
-
\ No newline at end of file +{{ selectedFlight | json }} + +
diff --git a/projects/sample/src/app/flight-booking/flight-search/flight-search.component.css b/projects/sample/src/app/flight-booking/flight-search/flight-search.component.css index 58633484..58741db8 100644 --- a/projects/sample/src/app/flight-booking/flight-search/flight-search.component.css +++ b/projects/sample/src/app/flight-booking/flight-search/flight-search.component.css @@ -1,13 +1,11 @@ - - input.ng-valid { - border-left-style: solid; - border-left-color: forestgreen; - border-left-width: 5px; + border-left-style: solid; + border-left-color: forestgreen; + border-left-width: 5px; } input.ng-invalid { - border-left-style: solid; - border-left-color: hotpink; - border-left-width: 5px; -} \ No newline at end of file + border-left-style: solid; + border-left-color: hotpink; + border-left-width: 5px; +} diff --git a/projects/sample/src/app/flight-booking/flight-search/flight-search.component.html b/projects/sample/src/app/flight-booking/flight-search/flight-search.component.html index a7b0068e..5c6291f4 100644 --- a/projects/sample/src/app/flight-booking/flight-search/flight-search.component.html +++ b/projects/sample/src/app/flight-booking/flight-search/flight-search.component.html @@ -5,74 +5,72 @@

Flight Search!

-->
- -
- Rund-Flüge sind nicht möglich. -
- - -
- - - -
- Validierungsfehler. Bitte prüfen Sie Ihre Eingaben. -
+  
+ Rund-Flüge sind nicht möglich. +
+ +
+ + + +
+ Validierungsfehler. Bitte prüfen Sie Ihre Eingaben. +
             {{ f.controls.from?.errors | json }}
-            
-
- -
- Async-City: Die Stadt wird gerade wegen eines Unwetters nicht angeflogen. -
- - - -
- - Validierung wird ausgeführt. Bitte etwas warten! - -
- - -
- Dieses Feld ist ein Pflichtfeld. -
+
+
-
- Diese Stadt wird nicht angeflogen. -
+
+ Async-City: Die Stadt wird gerade wegen eines Unwetters nicht angeflogen. +
-
- Bitte erfassen Sie min. 3 Zeichen. -
-
- Bitte nur Buchstaben erfassen. -
+
+ + Validierung wird ausgeführt. Bitte etwas warten! + +
+
+ Dieses Feld ist ein Pflichtfeld.
+
+ Diese Stadt wird nicht angeflogen. +
-
- - +
+ Bitte erfassen Sie min. 3 Zeichen.
-
- +
+ Bitte nur Buchstaben erfassen.
- +
+ +
+ + +
+
+ +
-
- - - +
+ - - -
+
-
Warenkorb
+  
+Warenkorb
 ----------------------
-{{selectedFlight | json}}
-
+{{ selectedFlight | json }} +
- \ No newline at end of file + diff --git a/projects/sample/src/app/flight-booking/flight-search/flight-search.component.ts b/projects/sample/src/app/flight-booking/flight-search/flight-search.component.ts index ea958b4d..b14de224 100644 --- a/projects/sample/src/app/flight-booking/flight-search/flight-search.component.ts +++ b/projects/sample/src/app/flight-booking/flight-search/flight-search.component.ts @@ -36,10 +36,12 @@ export class FlightSearchComponent { } refresh() { - this.oauthService.oidc = true; - if (!this.oauthService.useSilentRefresh && this.oauthService.responseType === 'code') { + if ( + !this.oauthService.useSilentRefresh && + this.oauthService.responseType === 'code' + ) { this.oauthService .refreshToken() .then(info => console.debug('refresh ok', info)) @@ -51,5 +53,4 @@ export class FlightSearchComponent { .catch(err => console.error('silent refresh error', err)); } } - } diff --git a/projects/sample/src/app/flight-booking/passenger-search/passenger-search.component.ts b/projects/sample/src/app/flight-booking/passenger-search/passenger-search.component.ts index 5b356346..794b19b7 100644 --- a/projects/sample/src/app/flight-booking/passenger-search/passenger-search.component.ts +++ b/projects/sample/src/app/flight-booking/passenger-search/passenger-search.component.ts @@ -3,11 +3,10 @@ import { OAuthService } from 'angular-oauth2-oidc'; @Component({ template: ` -

PassengerSearch

-

Platzhalter-Seite. Hier könnte auch Ihre Werbung stehen ;-)

-

- - ` +

PassengerSearch

+

Platzhalter-Seite. Hier könnte auch Ihre Werbung stehen ;-)

+

+ ` }) export class PassengerSearchComponent implements OnInit { constructor(private oauthService: OAuthService) {} diff --git a/projects/sample/src/app/flight-booking/services/flight.service.ts b/projects/sample/src/app/flight-booking/services/flight.service.ts index 26395524..27171c65 100644 --- a/projects/sample/src/app/flight-booking/services/flight.service.ts +++ b/projects/sample/src/app/flight-booking/services/flight.service.ts @@ -22,13 +22,15 @@ export class FlightService { let params = new HttpParams().set('from', from).set('to', to); - this.http.get(url, { headers, params }).subscribe( - flights => { - this.flights = flights; - }, - err => { - console.warn('status', err.status); - } - ); + this.http + .get(url, { headers, params }) + .subscribe( + flights => { + this.flights = flights; + }, + err => { + console.warn('status', err.status); + } + ); } } diff --git a/projects/sample/src/app/flight-history/flight-history.component.ts b/projects/sample/src/app/flight-history/flight-history.component.ts index d3be7b61..2c7cf8d5 100644 --- a/projects/sample/src/app/flight-history/flight-history.component.ts +++ b/projects/sample/src/app/flight-history/flight-history.component.ts @@ -2,12 +2,12 @@ import { Component } from '@angular/core'; @Component({ template: ` -

Flight History

-
    -
  • Graz - Hamburg
  • -
  • Hamburg - Frankfurt
  • -
  • Frankfurt - Graz
  • -
- ` +

Flight History

+
    +
  • Graz - Hamburg
  • +
  • Hamburg - Frankfurt
  • +
  • Frankfurt - Graz
  • +
+ ` }) export class FlightHistoryComponent {} diff --git a/projects/sample/src/app/home/home.component.html b/projects/sample/src/app/home/home.component.html index d17caf7f..05f4248f 100644 --- a/projects/sample/src/app/home/home.component.html +++ b/projects/sample/src/app/home/home.component.html @@ -1,109 +1,109 @@ Status: {{ givenName ? 'logged in' : 'logged out' }}

Welcome!

-

Welcome, {{givenName}} {{familyName}}!

+

Welcome, {{ givenName }} {{ familyName }}!

-
-

Login with Authorization Server

-
- -
+
+

Login with Authorization Server

+
+
-
-

Test settings

-
- -
-
+
+
+

Test settings

+
+ +
+
-
-

Login with Implicit Flow

-

- - -

- Username/Password: max/geheim -
+
+

Login with Implicit Flow

+

+ + +

+ Username/Password: max/geheim +
-
-

Login with Implicit Flow in popup

-

- - -

-

- Username/Password: max/geheim -

-

- Note: When using IE, some security settings block the communication with popups. This prevents that this feature works. -

-
+
+

Login with Implicit Flow in popup

+

+ + +

+

Username/Password: max/geheim

+

+ Note: When using IE, some security settings block the communication + with popups. This prevents that this feature works. +

+
-
-

Login with Code Flow

-

- - -

- Username/Password: alice/alice -
+
+

Login with Code Flow

+

+ + +

+ Username/Password: alice/alice +
-
-

Login with Code Flow in popup

-

- - -

-

- Username/Password: alice/alice -

-

- Note: When using IE, some security settings block the communication with popups. This prevents that this feature works. -

- -
+
+

Login with Code Flow in popup

+

+ + +

+

Username/Password: alice/alice

+

+ Note: When using IE, some security settings block the communication + with popups. This prevents that this feature works. +

+
-
-

- access_token_expiration: {{access_token_expiration}} -

-

- id_token_expiration: {{id_token_expiration}} -

-
+
+

access_token_expiration: {{ access_token_expiration }}

+

id_token_expiration: {{ id_token_expiration }}

+
-
-

- access_token: {{access_token}} -

-

- id_token: {{id_token}} -

-
- user profile: -
{{userProfile | json}}
-
- +
+

access_token: {{ access_token }}

+

id_token: {{ id_token }}

+
+ user profile: +
{{ userProfile | json }}
+
-
-

Further Actions

- +
+

Further Actions

+ - -
+ +
diff --git a/projects/sample/src/app/home/home.component.ts b/projects/sample/src/app/home/home.component.ts index 01ef55e3..d406f930 100644 --- a/projects/sample/src/app/home/home.component.ts +++ b/projects/sample/src/app/home/home.component.ts @@ -11,11 +11,10 @@ export class HomeComponent implements OnInit { userProfile: object; usePopup: boolean; - constructor(private oauthService: OAuthService) { - } + constructor(private oauthService: OAuthService) {} ngOnInit() { - // This would directly (w/o user interaction) redirect the user to the + // This would directly (w/o user interaction) redirect the user to the // login page if they are not already logged in. /* this.oauthService.loadDiscoveryDocumentAndTryLogin().then(_ => { @@ -27,18 +26,16 @@ export class HomeComponent implements OnInit { } async loginImplicit() { - // Tweak config for implicit flow this.oauthService.configure(authConfig); await this.oauthService.loadDiscoveryDocument(); sessionStorage.setItem('flow', 'implicit'); this.oauthService.initLoginFlow('/some-state;p1=1;p2=2?p3=3&p4=4'); - // the parameter here is optional. It's passed around and can be used after logging in + // the parameter here is optional. It's passed around and can be used after logging in } async loginImplicitInPopup() { - // Tweak config for implicit flow this.oauthService.configure(authConfig); await this.oauthService.loadDiscoveryDocument(); @@ -47,28 +44,28 @@ export class HomeComponent implements OnInit { this.oauthService.initLoginFlowInPopup().then(() => { this.loadUserProfile(); }); - // the parameter here is optional. It's passed around and can be used after logging in + // the parameter here is optional. It's passed around and can be used after logging in } async loginCode() { - // Tweak config for code flow - this.oauthService.configure(authCodeFlowConfig); - await this.oauthService.loadDiscoveryDocument(); - sessionStorage.setItem('flow', 'code'); + // Tweak config for code flow + this.oauthService.configure(authCodeFlowConfig); + await this.oauthService.loadDiscoveryDocument(); + sessionStorage.setItem('flow', 'code'); - this.oauthService.initLoginFlow('/some-state;p1=1;p2=2?p3=3&p4=4'); - // the parameter here is optional. It's passed around and can be used after logging in + this.oauthService.initLoginFlow('/some-state;p1=1;p2=2?p3=3&p4=4'); + // the parameter here is optional. It's passed around and can be used after logging in } async loginCodeInPopup() { - // Tweak config for code flow - this.oauthService.configure(authCodeFlowConfig); - await this.oauthService.loadDiscoveryDocument(); - sessionStorage.setItem('flow', 'code'); + // Tweak config for code flow + this.oauthService.configure(authCodeFlowConfig); + await this.oauthService.loadDiscoveryDocument(); + sessionStorage.setItem('flow', 'code'); - this.oauthService.initLoginFlowInPopup().then(() => { - this.loadUserProfile(); - }); + this.oauthService.initLoginFlowInPopup().then(() => { + this.loadUserProfile(); + }); } logout() { @@ -92,10 +89,12 @@ export class HomeComponent implements OnInit { } refresh() { - this.oauthService.oidc = true; - if (!this.oauthService.useSilentRefresh && this.oauthService.responseType === 'code') { + if ( + !this.oauthService.useSilentRefresh && + this.oauthService.responseType === 'code' + ) { this.oauthService .refreshToken() .then(info => console.debug('refresh ok', info)) diff --git a/projects/sample/src/app/password-flow-login/password-flow-login.component.html b/projects/sample/src/app/password-flow-login/password-flow-login.component.html index a37e787c..e744133c 100644 --- a/projects/sample/src/app/password-flow-login/password-flow-login.component.html +++ b/projects/sample/src/app/password-flow-login/password-flow-login.component.html @@ -1,53 +1,47 @@

Welcome!

-

Welcome, {{givenName}} {{familyName}}!

- +

Welcome, {{ givenName }} {{ familyName }}!

-
-

Login with Username/Password

+
+

Login with Username/Password

-

- Login wasn't successfull. -

+

+ Login wasn't successfull. +

-
- - -
-
- - -
-
- - -
+
+ + +
+
+ +
+
+ + +
+
-
- Username/Password: max/geheim -
+
Username/Password: max/geheim
-
-

- access_token_expiration: {{access_token_expiration}} -

-
+
+

access_token_expiration: {{ access_token_expiration }}

+
-
-

- access_token: {{access_token}} -

+
+

access_token: {{ access_token }}

- user profile: -
{{userProfile | json}}
+ user profile: +
{{ userProfile | json }}
- -
+
diff --git a/projects/sample/src/app/shared/date/date.component.ts b/projects/sample/src/app/shared/date/date.component.ts index 813c3c15..6eefdae3 100644 --- a/projects/sample/src/app/shared/date/date.component.ts +++ b/projects/sample/src/app/shared/date/date.component.ts @@ -3,10 +3,8 @@ import { Component, Input, OnInit, OnChanges } from '@angular/core'; @Component({ selector: 'date-component', template: ` -
- {{day}}.{{month}}.{{year}} {{hour}}:{{minute}} -
- ` +
{{ day }}.{{ month }}.{{ year }} {{ hour }}:{{ minute }}
+ ` }) export class DateComponent implements OnInit, OnChanges { @Input() date: string; diff --git a/projects/sample/src/flags.ts b/projects/sample/src/flags.ts index 6ad33393..5aea8cd0 100644 --- a/projects/sample/src/flags.ts +++ b/projects/sample/src/flags.ts @@ -1,4 +1,3 @@ - // Use HashLocationStrategy for routing? export const useHash = false; diff --git a/projects/sample/src/index.html b/projects/sample/src/index.html index 91fd268e..3b3807a0 100644 --- a/projects/sample/src/index.html +++ b/projects/sample/src/index.html @@ -1,16 +1,14 @@ - + - - - Sample - + + + Sample + - - - - - - - - + + + + + + diff --git a/projects/sample/src/polyfills.ts b/projects/sample/src/polyfills.ts index 95979113..9cf976d9 100644 --- a/projects/sample/src/polyfills.ts +++ b/projects/sample/src/polyfills.ts @@ -28,7 +28,6 @@ /** Evergreen browsers require these. **/ - /** ALL Firefox browsers require the following to support `@angular/animation`. **/ // import 'web-animations-js'; // Run `npm install --save web-animations-js`. diff --git a/projects/sample/src/silent-refresh.html b/projects/sample/src/silent-refresh.html index 919fe6af..aee87120 100644 --- a/projects/sample/src/silent-refresh.html +++ b/projects/sample/src/silent-refresh.html @@ -1,20 +1,27 @@ - - - - \ No newline at end of file + (window.opener || window.parent).postMessage(message, location.origin); + + + diff --git a/projects/sample/tsconfig.app.json b/projects/sample/tsconfig.app.json index 996f3c9a..42009447 100644 --- a/projects/sample/tsconfig.app.json +++ b/projects/sample/tsconfig.app.json @@ -4,8 +4,5 @@ "outDir": "../../out-tsc/app", "types": [] }, - "files": [ - "src/main.ts", - "src/polyfills.ts" - ] + "files": ["src/main.ts", "src/polyfills.ts"] } diff --git a/projects/sample/tsconfig.spec.json b/projects/sample/tsconfig.spec.json index a809b0a6..143838d9 100644 --- a/projects/sample/tsconfig.spec.json +++ b/projects/sample/tsconfig.spec.json @@ -2,17 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine", - "node" - ] + "types": ["jasmine", "node"] }, - "files": [ - "src/test.ts", - "src/polyfills.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] + "files": ["src/test.ts", "src/polyfills.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] } diff --git a/projects/sample/tslint.json b/projects/sample/tslint.json index de5d6517..4e1daae7 100644 --- a/projects/sample/tslint.json +++ b/projects/sample/tslint.json @@ -1,26 +1,16 @@ { - "extends": "../../tslint.json", - "rules": { - "directive-selector": [ - true, - "attribute", - null, - "camelCase" - ], - "component-selector": [ - true, - "element", - null, - "kebab-case" - ], - "directive-class-suffix": false, - "prefer-const": false, - "no-trailing-whitespace": false, - "member-ordering": false, - "triple-equals": false, - "no-console": false, - "no-var-keyword": false, - "no-inferrable-types": false, - "comment-format": false - } -} \ No newline at end of file + "extends": "../../tslint.json", + "rules": { + "directive-selector": [true, "attribute", null, "camelCase"], + "component-selector": [true, "element", null, "kebab-case"], + "directive-class-suffix": false, + "prefer-const": false, + "no-trailing-whitespace": false, + "member-ordering": false, + "triple-equals": false, + "no-console": false, + "no-var-keyword": false, + "no-inferrable-types": false, + "comment-format": false + } +} diff --git a/tsconfig.json b/tsconfig.json index 3812bb96..c142bf54 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,30 +11,18 @@ "downlevelIteration": true, "module": "esnext", "target": "es5", - "typeRoots": [ - "node_modules/@types" - ], - "lib": [ - "es2017", - "dom" - ], + "typeRoots": ["node_modules/@types"], + "lib": ["es2017", "dom"], "paths": { - "sample": [ - "dist/sample" - ], - "angular-oauth2-oidc": [ - "projects/lib/src/public_api" - ], + "sample": ["dist/sample"], + "angular-oauth2-oidc": ["projects/lib/src/public_api"], "angular-oauth2-oidc-jwks": [ "projects/angular-oauth2-oidc-jwks/src/public-api" ] - - - }, + } }, "angularCompilerOptions": { "enableIvy": false } - } diff --git a/tsconfig.npm.json b/tsconfig.npm.json index dde6dfd7..e5a19f3e 100644 --- a/tsconfig.npm.json +++ b/tsconfig.npm.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "paths": { - }, - } + "paths": {} + } } diff --git a/tslint.json b/tslint.json index 9851111a..b40a981a 100644 --- a/tslint.json +++ b/tslint.json @@ -1,36 +1,22 @@ { - "rulesDirectory": [ - "node_modules/codelyzer" - ], + "rulesDirectory": ["node_modules/codelyzer"], "rules": { "arrow-return-shorthand": true, "callable-types": true, "class-name": true, - "comment-format": [ - true, - "check-space" - ], + "comment-format": [true, "check-space"], "curly": false, "deprecation": { "severity": "warn" }, "eofline": false, "forin": true, - "import-blacklist": [ - true, - "rxjs/Rx" - ], + "import-blacklist": [true, "rxjs/Rx"], "import-spacing": true, - "indent": [ - true, - "spaces" - ], + "indent": [true, "spaces"], "interface-over-type-literal": true, "label-position": true, - "max-line-length": [ - true, - 140 - ], + "max-line-length": [true, 140], "member-access": false, "member-ordering": [ true, @@ -45,24 +31,14 @@ ], "no-arg": true, "no-bitwise": true, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], + "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], "no-construct": true, "no-debugger": true, "no-duplicate-super": true, "no-empty": false, "no-empty-interface": true, "no-eval": true, - "no-inferrable-types": [ - true, - "ignore-params" - ], + "no-inferrable-types": [true, "ignore-params"], "no-misused-new": true, "no-non-null-assertion": true, "no-shadowed-variable": true, @@ -82,19 +58,10 @@ "check-whitespace" ], "prefer-const": false, - "quotemark": [ - true, - "single" - ], + "quotemark": [true, "single"], "radix": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], + "semicolon": [true, "always"], + "triple-equals": [true, "allow-null-check"], "typedef-whitespace": [ true, {