├── projects ├── sample │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── app │ │ │ ├── app.component.css │ │ │ ├── app.tokens.ts │ │ │ ├── entities │ │ │ │ └── flight.ts │ │ │ ├── flight-booking │ │ │ │ ├── flight-booking.component.ts │ │ │ │ ├── flight-search │ │ │ │ │ ├── flight-search.component.css │ │ │ │ │ ├── flight-search.component.ts │ │ │ │ │ └── flight-search.component.html │ │ │ │ ├── flight-search-reactive │ │ │ │ │ ├── flight-search-reactive.component.css │ │ │ │ │ ├── flight-search-reactive.component.ts │ │ │ │ │ └── flight-search-reactive.component.html │ │ │ │ ├── flight-booking.component.html │ │ │ │ ├── alt-flight-card │ │ │ │ │ ├── alt-flight-card.component.html │ │ │ │ │ ├── alt-flight.card.component.ts │ │ │ │ │ └── flight-list.ts │ │ │ │ ├── flight-card │ │ │ │ │ ├── flight.card.component.ts │ │ │ │ │ └── flight-card.component.html │ │ │ │ ├── passenger-search │ │ │ │ │ └── passenger-search.component.ts │ │ │ │ ├── services │ │ │ │ │ └── flight.service.ts │ │ │ │ ├── flight-booking.routes.ts │ │ │ │ ├── flight-edit │ │ │ │ │ └── flight-edit.component.ts │ │ │ │ └── flight-booking.module.ts │ │ │ ├── flight-history │ │ │ │ └── flight-history.component.ts │ │ │ ├── shared │ │ │ │ ├── auth │ │ │ │ │ └── auth.guard.ts │ │ │ │ ├── deactivation │ │ │ │ │ └── LeaveComponentGuard.ts │ │ │ │ ├── preload │ │ │ │ │ └── custom-preloading.strategy.ts │ │ │ │ ├── pipes │ │ │ │ │ └── city.pipe.ts │ │ │ │ ├── validation │ │ │ │ │ ├── async-city.validator.ts │ │ │ │ │ ├── roundtrip.validator.ts │ │ │ │ │ └── city.validator.ts │ │ │ │ ├── date │ │ │ │ │ └── date.component.ts │ │ │ │ └── shared.module.ts │ │ │ ├── app.component.html │ │ │ ├── auth.config.ts │ │ │ ├── auth-password-flow.config.ts │ │ │ ├── auth.google.config.ts │ │ │ ├── app.component.spec.ts │ │ │ ├── app.routes.ts │ │ │ ├── password-flow-login │ │ │ │ ├── password-flow-login.component.html │ │ │ │ └── password-flow-login.component.ts │ │ │ ├── home │ │ │ │ ├── home.component.html │ │ │ │ └── home.component.ts │ │ │ ├── app.module.ts │ │ │ ├── auth-no-discovery.config.ts │ │ │ └── app.component.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── styles.css │ │ ├── favicon.ico │ │ ├── typings.d.ts │ │ ├── silent-refresh.html │ │ ├── tsconfig.app.json │ │ ├── index.html │ │ ├── main.ts │ │ ├── tsconfig.spec.json │ │ ├── test.ts │ │ └── polyfills.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── tslint.json │ ├── browserslist │ └── karma.conf.js ├── lib │ ├── src │ │ ├── deps.d.ts │ │ ├── tokens.ts │ │ ├── base64-helper.ts │ │ ├── oauth-module.config.ts │ │ ├── interceptors │ │ │ ├── resource-server-error-handler.ts │ │ │ └── default-oauth.interceptor.ts │ │ ├── token-validation │ │ │ ├── null-validation-handler.ts │ │ │ ├── validation-handler.ts │ │ │ ├── validation-handler.js │ │ │ └── jwks-validation-handler.ts │ │ ├── encoder.ts │ │ ├── public_api.ts │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── events.ts │ │ ├── url-helper.service.ts │ │ ├── angular-oauth-oidic.module.ts │ │ ├── types.ts │ │ └── auth.config.ts │ ├── ng-package.prod.json │ ├── ng-package.json │ ├── tsconfig.spec.json │ ├── tslint.json │ ├── package.json │ ├── tsconfig.lib.json │ └── karma.conf.js └── sample-e2e │ ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts │ ├── tsconfig.e2e.json │ └── protractor.conf.js ├── oidc.png ├── docs ├── images │ └── favicon.ico ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── roboto-v15-latin-300.eot │ ├── roboto-v15-latin-300.ttf │ ├── roboto-v15-latin-300.woff │ ├── roboto-v15-latin-700.eot │ ├── roboto-v15-latin-700.ttf │ ├── roboto-v15-latin-700.woff │ ├── roboto-v15-latin-300.woff2 │ ├── roboto-v15-latin-700.woff2 │ ├── roboto-v15-latin-regular.eot │ ├── roboto-v15-latin-regular.ttf │ ├── roboto-v15-latin-regular.woff │ └── roboto-v15-latin-regular.woff2 ├── styles │ ├── style.css │ ├── tablesort.css │ ├── original.css │ ├── reset.css │ ├── stripe.css │ ├── laravel.css │ ├── vagrant.css │ ├── readthedocs.css │ ├── material.css │ ├── postmark.css │ ├── bootstrap-card.css │ └── prism.css ├── js │ ├── compodoc.js │ ├── libs │ │ ├── tablesort.number.min.js │ │ ├── EventDispatcher.js │ │ ├── innersvg.js │ │ ├── promise.min.js │ │ ├── tablesort.min.js │ │ └── bootstrap-native.js │ ├── tabs.js │ ├── sourceCode.js │ ├── svg-pan-zoom.controls.js │ ├── search │ │ └── search-lunr.js │ └── tree.js └── graph │ └── dependencies.svg ├── docs-src ├── getting-started.md ├── events.md ├── custom-query-parameter.md ├── server-side-rendering.md ├── preserving-state.md ├── callback-after-login.md ├── systemjs.md ├── hash-strategy.md ├── id-token-validation.md ├── session-checks.md ├── implicit-flow-config-discovery.md ├── summary.json ├── implicit-flow-config-no-discovery.md ├── interceptors.md ├── silent-refresh.md └── password-flow.md ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.e2e.json └── protractor.conf.js ├── .editorconfig ├── .travis.yml ├── .vscode ├── tasks.json └── settings.json ├── tsconfig.json ├── .gitignore ├── CHANGELOG.md ├── package.json ├── tslint.json ├── README.md └── angular.json /projects/sample/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/sample/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/lib/src/deps.d.ts: -------------------------------------------------------------------------------- 1 | declare var require: any; 2 | -------------------------------------------------------------------------------- /oidc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/oidc.png -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /projects/sample/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/sample/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/sample/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/projects/sample/src/favicon.ico -------------------------------------------------------------------------------- /docs/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-300.eot -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-300.ttf -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-300.woff -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-700.eot -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-700.ttf -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-700.woff -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-300.woff2 -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-700.woff2 -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-regular.eot -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-regular.ttf -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-regular.woff -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bechhansen/angular-oauth2-oidc/HEAD/docs/fonts/roboto-v15-latin-regular.woff2 -------------------------------------------------------------------------------- /projects/sample/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /projects/sample/src/app/app.tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const BASE_URL = new InjectionToken('BASE_URL'); 4 | -------------------------------------------------------------------------------- /docs-src/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Please find the [Getting Started Guide in the README](https://www.npmjs.com/package/angular-oauth2-oidc) above. -------------------------------------------------------------------------------- /projects/sample/src/silent-refresh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /projects/lib/src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { AuthConfig } from './auth.config'; 3 | 4 | export const AUTH_CONFIG = new InjectionToken('AUTH_CONFIG'); 5 | -------------------------------------------------------------------------------- /projects/sample/src/app/entities/flight.ts: -------------------------------------------------------------------------------- 1 | export interface Flight { 2 | id: number; // int + double 3 | from: string; 4 | to: string; 5 | date: string; 6 | // JSON: ISO-String 2016-12-24T17:00:00.000+01:00 7 | } 8 | -------------------------------------------------------------------------------- /docs/styles/style.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./bootstrap.min.css"; 3 | @import "./bootstrap-card.css"; 4 | @import "./prism.css"; 5 | @import "./font-awesome.min.css"; 6 | @import "./compodoc.css"; 7 | @import "./tablesort.css"; 8 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/lib/ng-package.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/lib", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | }, 7 | "whitelistedNonPeerDependencies": [ 8 | "jsrsasign" 9 | ] 10 | } -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /projects/sample-e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/sample/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /projects/sample-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /projects/lib/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/lib", 4 | "deleteDestPath": false, 5 | "lib": { 6 | "entryFile": "src/public_api.ts" 7 | }, 8 | "whitelistedNonPeerDependencies": [ 9 | "jsrsasign" 10 | ] 11 | 12 | } -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-booking.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FlightService } from './services/flight.service'; 3 | 4 | @Component({ 5 | selector: 'flight-booking', 6 | templateUrl: './flight-booking.component.html' 7 | }) 8 | export class FlightBookingComponent {} 9 | -------------------------------------------------------------------------------- /docs-src/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | The library informs you about its tasks and state using events: 4 | 5 | ```TypeScript 6 | this.oauthService.events.subscribe(e => { 7 | console.debug('oauth/oidc event', e); 8 | }) 9 | ``` 10 | 11 | For a full list of available events see the string based enum in the file ``events.ts``. 12 | -------------------------------------------------------------------------------- /projects/sample/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [] 9 | }, 10 | 11 | "exclude": [ 12 | "test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | 5 | before_script: 6 | - npm install cpr 7 | 8 | script: npm run build 9 | 10 | before_deploy: 11 | - cd dist/lib 12 | 13 | deploy: 14 | provider: npm 15 | email: $NPM_EMAIL 16 | api_key: $NPM_KEY 17 | skip_cleanup: true 18 | 19 | on: 20 | tags: true 21 | 22 | -------------------------------------------------------------------------------- /docs-src/custom-query-parameter.md: -------------------------------------------------------------------------------- 1 | # Custom Query Parameters 2 | 3 | You can set the property ``customQueryParams`` to a hash with custom parameter that are transmitted when starting implicit flow. 4 | 5 | ```TypeScript 6 | this.oauthService.customQueryParams = { 7 | 'tenant': '4711', 8 | 'otherParam': 'someValue' 9 | }; 10 | ``` 11 | -------------------------------------------------------------------------------- /projects/lib/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-search/flight-search.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | input.ng-valid { 4 | border-left-style: solid; 5 | border-left-color: forestgreen; 6 | border-left-width: 5px; 7 | } 8 | 9 | input.ng-invalid { 10 | border-left-style: solid; 11 | border-left-color: hotpink; 12 | border-left-width: 5px; 13 | } -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | input.ng-valid { 4 | border-left-style: solid; 5 | border-left-color: forestgreen; 6 | border-left-width: 5px; 7 | } 8 | 9 | input.ng-invalid { 10 | border-left-style: solid; 11 | border-left-color: hotpink; 12 | border-left-width: 5px; 13 | } -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /docs/js/compodoc.js: -------------------------------------------------------------------------------- 1 | var compodoc = { 2 | EVENTS: { 3 | READY: 'compodoc.ready', 4 | SEARCH_READY: 'compodoc.search.ready' 5 | } 6 | }; 7 | 8 | Object.assign( compodoc, EventDispatcher.prototype ); 9 | 10 | document.addEventListener('DOMContentLoaded', function() { 11 | compodoc.dispatchEvent({ 12 | type: compodoc.EVENTS.READY 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /projects/sample-e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /projects/sample/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs-src/server-side-rendering.md: -------------------------------------------------------------------------------- 1 | # Server Side Rendering 2 | 3 | There is a great blog post that shows how this library can be used together with server side rendering: 4 | 5 | https://medium.com/lankapura/angular-server-side-rendering-for-authenticated-users-a021627fd9d3 6 | 7 | The sample for this can be found here: 8 | 9 | https://github.com/lankaapura/Angular-AspNetCore-Idsvr 10 | 11 | -------------------------------------------------------------------------------- /projects/lib/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-history/flight-history.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 |

Flight History

6 | 11 | ` 12 | }) 13 | export class FlightHistoryComponent {} 14 | -------------------------------------------------------------------------------- /projects/sample/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { environment } from './environments/environment'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /projects/sample/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test.ts", 13 | "src/polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /projects/sample/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-oauth2-oidc-codeflow", 3 | "license": "MIT", 4 | "author": { 5 | "name": "Peer Bech Hansen" 6 | }, 7 | "version": "4.0.1", 8 | "repository": "bechhansen/angular-oauth2-oidc", 9 | "dependencies": { 10 | "jsrsasign": "^8.0.12" 11 | }, 12 | "peerDependencies": { 13 | "@angular/common": "^6.0.0", 14 | "@angular/core": "^6.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/sample/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /projects/sample/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-booking.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |
10 | 11 |
-------------------------------------------------------------------------------- /projects/sample/src/app/shared/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | CanActivate, 4 | ActivatedRouteSnapshot, 5 | RouterStateSnapshot 6 | } from '@angular/router'; 7 | 8 | @Injectable() 9 | export class AuthGuard implements CanActivate { 10 | canActivate( 11 | route: ActivatedRouteSnapshot, 12 | state: RouterStateSnapshot 13 | ): boolean { 14 | return true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/sample/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /projects/sample/src/app/shared/deactivation/LeaveComponentGuard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanDeactivate, 3 | ActivatedRouteSnapshot, 4 | RouterStateSnapshot 5 | } from '@angular/router'; 6 | 7 | export class LeaveComponentGuard implements CanDeactivate { 8 | canDeactivate( 9 | component: any, 10 | route: ActivatedRouteSnapshot, 11 | state: RouterStateSnapshot 12 | ): Promise { 13 | return component.canDeactivate(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/sample/src/app/shared/preload/custom-preloading.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PreloadingStrategy, Route } from '@angular/router'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export class CustomPreloadingStrategy implements PreloadingStrategy { 5 | preload(route: Route, fn: () => Observable): Observable { 6 | //return Observable.of(true).delay(7000).flatMap(_ => fn()); 7 | 8 | if (true) { 9 | return fn(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /projects/lib/src/base64-helper.ts: -------------------------------------------------------------------------------- 1 | // see: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22 2 | export function b64DecodeUnicode(str) { 3 | const base64 = str.replace(/\-/g, '+').replace(/\_/g, '/'); 4 | 5 | return decodeURIComponent( 6 | atob(base64) 7 | .split('') 8 | .map(function(c) { 9 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 10 | }) 11 | .join('') 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build:sample", 9 | "problemMatcher": [ 10 | "$tsc" 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /docs-src/preserving-state.md: -------------------------------------------------------------------------------- 1 | # Preserving State (like the Requested URL) 2 | 3 | When calling ``initImplicitFlow``, you can pass an optional state which could be the requested url: 4 | 5 | ```TypeScript 6 | this.oauthService.initImplicitFlow('http://www.myurl.com/x/y/z'); 7 | ``` 8 | 9 | After login succeeded, you can read this state: 10 | 11 | ```TypeScript 12 | this.oauthService.tryLogin({ 13 | onTokenReceived: (info) => { 14 | console.debug('state', info.state); 15 | } 16 | }) 17 | ``` 18 | -------------------------------------------------------------------------------- /projects/lib/src/oauth-module.config.ts: -------------------------------------------------------------------------------- 1 | export abstract class OAuthModuleConfig { 2 | resourceServer: OAuthResourceServerConfig; 3 | } 4 | 5 | export abstract class OAuthResourceServerConfig { 6 | /** 7 | * Urls for which calls should be intercepted. 8 | * If there is an ResourceServerErrorHandler registered, it is used for them. 9 | * If sendAccessToken is set to true, the access_token is send to them too. 10 | */ 11 | allowedUrls?: Array; 12 | sendAccessToken: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /projects/lib/src/interceptors/resource-server-error-handler.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from '@angular/common/http'; 2 | import { Observable, throwError } from 'rxjs'; 3 | 4 | export abstract class OAuthResourceServerErrorHandler { 5 | abstract handleError(err: HttpResponse): Observable; 6 | } 7 | 8 | export class OAuthNoopResourceServerErrorHandler 9 | implements OAuthResourceServerErrorHandler { 10 | handleError(err: HttpResponse): Observable { 11 | return throwError(err); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/alt-flight-card/alt-flight-card.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |

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

5 |

Flugnr. #{{item.id}}

6 |

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

7 | 8 |

9 | 13 | 14 | 15 |

16 |
17 | -------------------------------------------------------------------------------- /docs/js/libs/tablesort.number.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * tablesort v5.0.2 (2017-11-12) 3 | * http://tristen.ca/tablesort/demo/ 4 | * Copyright (c) 2017 ; Licensed MIT 5 | */!function(){var a=function(a){return a.replace(/[^\-?0-9.]/g,"")},b=function(a,b){return a=parseFloat(a),b=parseFloat(b),a=isNaN(a)?0:a,b=isNaN(b)?0:b,a-b};Tablesort.extend("number",function(a){return a.match(/^[-+]?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/)||a.match(/^[-+]?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/)||a.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/)},function(c,d){return c=a(c),d=a(d),b(d,c)})}(); -------------------------------------------------------------------------------- /projects/lib/src/token-validation/null-validation-handler.ts: -------------------------------------------------------------------------------- 1 | import { ValidationHandler, ValidationParams } from './validation-handler'; 2 | 3 | /** 4 | * A validation handler that isn't validating nothing. 5 | * Can be used to skip validation (on your own risk). 6 | */ 7 | export class NullValidationHandler implements ValidationHandler { 8 | validateSignature(validationParams: ValidationParams): Promise { 9 | return Promise.resolve(null); 10 | } 11 | validateAtHash(validationParams: ValidationParams): boolean { 12 | return true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/alt-flight-card/alt-flight.card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Flight } from '../../entities/flight'; 3 | 4 | @Component({ 5 | selector: 'alt-flight-card', 6 | templateUrl: 'alt-flight-card.component.html' 7 | }) 8 | export class AltFlightCardComponent { 9 | @Input() item: Flight; 10 | @Input() selected: boolean; 11 | @Output() selectedChange = new EventEmitter(); 12 | 13 | select() { 14 | this.selectedChange.emit(true); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-card/flight.card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Flight } from '../../entities/flight'; 3 | 4 | @Component({ 5 | selector: 'flight-card', 6 | templateUrl: './flight-card.component.html' 7 | }) 8 | export class FlightCardComponent { 9 | @Input() item: Flight; 10 | @Input() selectedItem: Flight; 11 | @Output() selectedItemChange = new EventEmitter(); 12 | 13 | select() { 14 | this.selectedItemChange.emit(this.item); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-card/flight-card.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |

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

5 |

Flugnr. #{{item.id}}

6 |

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

7 | 8 |

9 | 13 | 14 | 15 | 16 |

17 |
18 | -------------------------------------------------------------------------------- /projects/lib/src/encoder.ts: -------------------------------------------------------------------------------- 1 | import { HttpParameterCodec } from '@angular/common/http'; 2 | /** 3 | * This custom encoder allows charactes like +, % and / to be used in passwords 4 | */ 5 | export class WebHttpUrlEncodingCodec implements HttpParameterCodec { 6 | encodeKey(k: string): string { 7 | return encodeURIComponent(k); 8 | } 9 | 10 | encodeValue(v: string): string { 11 | return encodeURIComponent(v); 12 | } 13 | 14 | decodeKey(k: string): string { 15 | return decodeURIComponent(k); 16 | } 17 | 18 | decodeValue(v: string) { 19 | return decodeURIComponent(v); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs-src/callback-after-login.md: -------------------------------------------------------------------------------- 1 | # Callback after login 2 | 3 | There is a callback ``onTokenReceived``, that is called after a successful login. In this case, the lib received the access_token as 4 | well as the id_token, if it was requested. If there is an id_token, the lib validated it. 5 | 6 | ```TypeScript 7 | this.oauthService.tryLogin({ 8 | onTokenReceived: context => { 9 | // 10 | // Output just for purpose of demonstration 11 | // Don't try this at home ... ;-) 12 | // 13 | console.debug("logged in"); 14 | console.debug(context); 15 | } 16 | }); 17 | ``` 18 | -------------------------------------------------------------------------------- /projects/sample/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 | -------------------------------------------------------------------------------- /projects/lib/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './angular-oauth-oidic.module'; 2 | export * from './oauth-service'; 3 | export * from './token-validation/jwks-validation-handler'; 4 | export * from './token-validation/null-validation-handler'; 5 | export * from './token-validation/validation-handler'; 6 | export * from './url-helper.service'; 7 | export * from './auth.config'; 8 | export * from './types'; 9 | export * from './tokens'; 10 | export * from './events'; 11 | export * from './interceptors/default-oauth.interceptor'; 12 | export * from './interceptors/resource-server-error-handler'; 13 | export * from './oauth-module.config'; 14 | -------------------------------------------------------------------------------- /projects/lib/src/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enabledLanguageIds": [ 3 | "asciidoc", 4 | "c", 5 | "cpp", 6 | "csharp", 7 | "css", 8 | "go", 9 | "handlebars", 10 | "html", 11 | "jade", 12 | "javascript", 13 | "javascriptreact", 14 | "json", 15 | "latex", 16 | "less", 17 | "markdown", 18 | "php", 19 | "plaintext", 20 | "pub", 21 | "python", 22 | "restructuredtext", 23 | "rust", 24 | "scss", 25 | "text", 26 | "typescriptreact", 27 | "yml" 28 | ] 29 | } -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/passenger-search/passenger-search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { OAuthService } from 'angular-oauth2-oidc'; 3 | 4 | @Component({ 5 | template: ` 6 |

PassengerSearch

7 |

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

8 |

9 | 10 | ` 11 | }) 12 | export class PassengerSearchComponent implements OnInit { 13 | constructor(private oauthService: OAuthService) {} 14 | ngOnInit() {} 15 | 16 | refresh() { 17 | this.oauthService.silentRefresh(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ], 19 | "paths": { 20 | "sample": [ 21 | "dist/sample" 22 | ], 23 | "angular-oauth2-oidc": [ 24 | "projects/lib/src/public_api" 25 | //"dist/lib" 26 | ] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enabledLanguageIds": [ 3 | "asciidoc", 4 | "c", 5 | "cpp", 6 | "csharp", 7 | "css", 8 | "go", 9 | "handlebars", 10 | "html", 11 | "jade", 12 | "javascript", 13 | "javascriptreact", 14 | "json", 15 | "latex", 16 | "less", 17 | "php", 18 | "plaintext", 19 | "pub", 20 | "python", 21 | "restructuredtext", 22 | "rust", 23 | "scss", 24 | "text", 25 | "typescriptreact", 26 | "yml" 27 | ], 28 | "spellright.language": "de", 29 | "spellright.documentTypes": [ 30 | "latex", 31 | "plaintext" 32 | ] 33 | } -------------------------------------------------------------------------------- /projects/sample/src/app/shared/pipes/city.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'city', 5 | pure: true 6 | }) 7 | export class CityPipe implements PipeTransform { 8 | transform(value: any, ...args: any[]): any { 9 | let fmt = args[0]; // short, long 10 | let short, long; 11 | 12 | switch (value) { 13 | case 'Graz': 14 | long = 'Flughafen Graz Thalerhof'; 15 | short = 'GRZ'; 16 | break; 17 | case 'Hamburg': 18 | long = 'Airport Hamburg Fuhlsbüttl Helmut Schmidt'; 19 | short = 'HAM'; 20 | break; 21 | default: 22 | long = short = 'ROM'; 23 | } 24 | 25 | if (fmt == 'short') return short; 26 | return long; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/js/libs/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | var EventDispatcher=function(){};Object.assign(EventDispatcher.prototype,{addEventListener:function(i,t){void 0===this._listeners&&(this._listeners={});var e=this._listeners;void 0===e[i]&&(e[i]=[]),-1===e[i].indexOf(t)&&e[i].push(t)},hasEventListener:function(i,t){if(void 0===this._listeners)return!1;var e=this._listeners;return void 0!==e[i]&&-1!==e[i].indexOf(t)},removeEventListener:function(i,t){if(void 0!==this._listeners){var e=this._listeners[i];if(void 0!==e){var s=e.indexOf(t);-1!==s&&e.splice(s,1)}}},dispatchEvent:function(i){if(void 0!==this._listeners){var t=this._listeners[i.type];if(void 0!==t){i.target=this;var e=[],s=0,n=t.length;for(s=0;s { 16 | return new Promise((resolve: Function) => { 17 | setTimeout(() => { 18 | if (ctrl.value == 'Graz' || ctrl.value == 'Hamburg') { 19 | resolve({}); 20 | return; 21 | } 22 | 23 | resolve({ 'async-city': false }); 24 | }, 100); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs-src/systemjs.md: -------------------------------------------------------------------------------- 1 | # Using SystemJS 2 | 3 | Thanks to [Kevin BEAUGRAND](https://github.com/kbeaugrand) for adding this information regarding SystemJS. 4 | 5 | ```TypeScript 6 | System.config({ 7 | ... 8 | meta: { 9 | 'angular-oauth2-oidc': { 10 | deps: ['jsrsasign'] 11 | }, 12 | } 13 | ... 14 | }); 15 | ``` 16 | 17 | Also thanks to [ppanthony](https://github.com/ppanthony) for sharing his SystemJS config: 18 | 19 | ``` 20 | 'angular-oauth2-oidc': { 21 | main: 'angular-oauth2-oidc.umd.js', 22 | format: 'cjs', 23 | defaultExtension: 'js', 24 | map: { 25 | 'jsrsasign': '/node_modules/jsrsasign/lib/jsrsasign', 26 | }, 27 | meta: { 28 | 'angular-oauth2-oidc': { 29 | deps: ['require','jsrsasign'] 30 | }, 31 | } 32 | } 33 | ``` -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /docs/js/tabs.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | var tabs = document.getElementsByClassName('nav-tabs'), 3 | updateAddress = function(e) { 4 | if(history.pushState && e.target.dataset.link) { 5 | history.pushState(null, null, '#' + e.target.dataset.link); 6 | } 7 | }; 8 | if (tabs.length > 0) { 9 | tabs = tabs[0].querySelectorAll('li'); 10 | for (var i = 0; i < tabs.length; i++) { 11 | tabs[i].addEventListener('click', updateAddress); 12 | var linkTag = tabs[i].querySelector('a'); 13 | if (location.hash !== '') { 14 | var currentHash = location.hash.substr(1); 15 | if (currentHash === linkTag.dataset.link) { 16 | linkTag.click(); 17 | } 18 | } 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /projects/sample-e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /projects/lib/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "flatModuleId": "AUTOGENERATED", 27 | "flatModuleOutFile": "AUTOGENERATED" 28 | }, 29 | "exclude": [ 30 | "src/test.ts", 31 | "**/*.spec.ts" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /projects/sample/src/app/shared/validation/roundtrip.validator.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | import { 3 | FormGroup, 4 | Validator, 5 | AbstractControl, 6 | NG_VALIDATORS, 7 | FormGroupDirective 8 | } from '@angular/forms'; 9 | 10 | @Directive({ 11 | selector: 'form[round-trip]', 12 | providers: [{ provide: NG_VALIDATORS, useExisting: RoundTrip, multi: true }] 13 | }) 14 | export class RoundTrip implements Validator { 15 | validate(control: AbstractControl): any { 16 | let formGroup = control; 17 | let fromCtrl = formGroup.controls['from']; 18 | let toCtrl = formGroup.controls['to']; 19 | 20 | if (!fromCtrl || !toCtrl) return {}; 21 | 22 | let from = fromCtrl.value; 23 | let to = toCtrl.value; 24 | 25 | if (from == to) { 26 | return { 27 | 'round-trip': { 28 | city: from 29 | } 30 | }; 31 | } 32 | return {}; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/alt-flight-card/flight-list.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | import { Flight } from '../../entities/flight'; 3 | @Component({ 4 | selector: 'flight-list', 5 | template: ` 6 |
7 |
8 | 12 | 13 |
14 |
15 | ` 16 | }) 17 | export class FlightListComponent { 18 | @Input() flights: Flight[] = []; 19 | @Input() selectedFlight: Flight; 20 | @Output() selectedFlightChange = new EventEmitter(); 21 | 22 | change(f: Flight) { 23 | this.selectedFlightChange.emit(f); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/sample/src/app/shared/date/date.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, OnChanges } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'date-component', 5 | template: ` 6 |
7 | {{day}}.{{month}}.{{year}} {{hour}}:{{minute}} 8 |
9 | ` 10 | }) 11 | export class DateComponent implements OnInit, OnChanges { 12 | @Input() date: string; 13 | 14 | day; 15 | month; 16 | year; 17 | hour; 18 | minute; 19 | 20 | constructor() { 21 | console.debug('ctrl'); 22 | } 23 | 24 | ngOnInit() {} 25 | 26 | ngOnChanges(change) { 27 | // if(change.date) { ... } 28 | 29 | console.debug('change', change); 30 | 31 | let date = new Date(this.date); 32 | 33 | this.day = date.getDate(); 34 | this.month = date.getMonth() + 1; 35 | this.year = date.getFullYear(); 36 | 37 | this.hour = date.getHours(); 38 | this.minute = date.getMinutes(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/sample/src/app/auth.config.ts: -------------------------------------------------------------------------------- 1 | // This api will come in the next version 2 | 3 | import { AuthConfig } from 'angular-oauth2-oidc'; 4 | 5 | export const authConfig: AuthConfig = { 6 | // Url of the Identity Provider 7 | issuer: 'https://steyer-identity-server.azurewebsites.net/identity', 8 | 9 | // URL of the SPA to redirect the user to after login 10 | redirectUri: window.location.origin + '/index.html', 11 | 12 | // URL of the SPA to redirect the user after silent refresh 13 | silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', 14 | 15 | // The SPA's id. The SPA is registerd with this id at the auth-server 16 | clientId: 'spa-demo', 17 | 18 | // set the scope for the permissions the client should request 19 | // The first three are defined by OIDC. The 4th is a usecase-specific one 20 | scope: 'openid profile email voucher', 21 | 22 | showDebugInformation: true, 23 | 24 | sessionChecksEnabled: false 25 | }; 26 | -------------------------------------------------------------------------------- /projects/sample/src/app/auth-password-flow.config.ts: -------------------------------------------------------------------------------- 1 | // This api will come in the next version 2 | 3 | import { AuthConfig } from 'angular-oauth2-oidc'; 4 | 5 | export const authPasswordFlowConfig: AuthConfig = { 6 | // Url of the Identity Provider 7 | issuer: 'https://steyer-identity-server.azurewebsites.net/identity', 8 | 9 | // URL of the SPA to redirect the user to after login 10 | redirectUri: window.location.origin + '/index.html', 11 | 12 | // URL of the SPA to redirect the user after silent refresh 13 | silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', 14 | 15 | // The SPA's id. The SPA is registerd with this id at the auth-server 16 | clientId: 'demo-resource-owner', 17 | 18 | dummyClientSecret: 'geheim', 19 | 20 | // set the scope for the permissions the client should request 21 | // The first three are defined by OIDC. The 4th is a usecase-specific one 22 | scope: 'openid profile email voucher', 23 | 24 | showDebugInformation: true, 25 | 26 | oidc: false 27 | }; 28 | -------------------------------------------------------------------------------- /projects/sample/src/app/auth.google.config.ts: -------------------------------------------------------------------------------- 1 | // This api will come in the next version 2 | 3 | import { AuthConfig } from 'angular-oauth2-oidc'; 4 | 5 | export const googleAuthConfig: AuthConfig = { 6 | // Url of the Identity Provider 7 | issuer: 'https://accounts.google.com', 8 | 9 | // URL of the SPA to redirect the user to after login 10 | redirectUri: window.location.origin + '/index.html', 11 | 12 | // URL of the SPA to redirect the user after silent refresh 13 | silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', 14 | 15 | // The SPA's id. The SPA is registerd with this id at the auth-server 16 | clientId: 17 | '1004270452653-m396kcs7jc3970turlp7ffh6bv4t1b86.apps.googleusercontent.com', 18 | 19 | strictDiscoveryDocumentValidation: false, 20 | 21 | // set the scope for the permissions the client should request 22 | // The first three are defined by OIDC. The 4th is a usecase-specific one 23 | scope: 'openid profile email', 24 | 25 | showDebugInformation: true, 26 | 27 | sessionChecksEnabled: true 28 | }; 29 | -------------------------------------------------------------------------------- /docs-src/hash-strategy.md: -------------------------------------------------------------------------------- 1 | # Routing with the HashStrategy 2 | 3 | If you are leveraging the ``LocationStrategy`` which the Router is using by default, you can skip this section. 4 | 5 | When using the ``HashStrategy`` for Routing, the Router will override the received hash fragment with the tokens when it performs it initial navigation. This prevents the library from reading them. To avoid this, disable initial navigation when setting up the routes for your root module: 6 | 7 | ```TypeScript 8 | export let AppRouterModule = RouterModule.forRoot(APP_ROUTES, { 9 | useHash: true, 10 | initialNavigation: false 11 | }); 12 | ``` 13 | 14 | After tryLogin did its job, you can manually perform the initial navigation: 15 | 16 | ```TypeScript 17 | this.oauthService.tryLogin().then(_ => { 18 | this.router.navigate(['/']); 19 | }) 20 | ``` 21 | 22 | Another solution is the use a redirect uri that already contains the initial route. In this case the router will not override it. An example for such a redirect uri is 23 | 24 | ``` 25 | http://localhost:8080/#/home 26 | ``` 27 | -------------------------------------------------------------------------------- /projects/sample/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /projects/lib/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /docs-src/id-token-validation.md: -------------------------------------------------------------------------------- 1 | # Configure/ Adapt id_token Validation 2 | 3 | You can hook in an implementation of the interface ``TokenValidator`` to validate the signature of the received id_token and its at_hash property. This packages provides two implementations: 4 | 5 | - JwksValidationHandler 6 | - NullValidationHandler 7 | 8 | The former one validates the signature against public keys received via the discovery document (property jwks) and the later one skips the validation on client side. 9 | 10 | ```TypeScript 11 | import { JwksValidationHandler } from 'angular-oauth2-oidc'; 12 | 13 | [...] 14 | 15 | this.oauthService.tokenValidationHandler = new JwksValidationHandler(); 16 | ``` 17 | 18 | In cases where no ValidationHandler is defined, you receive a warning on the console. This means that the library wants you to explicitly decide on this. 19 | 20 | ## Dependency Injection 21 | 22 | You can also setup a ValidationHandler by leveraging dependency injection: 23 | 24 | ```TypeScript 25 | [...] 26 | providers: [ 27 | { provide: ValidationHandler, useClass: JwksValidationHandler }, 28 | ], 29 | [...] 30 | ``` -------------------------------------------------------------------------------- /docs/styles/original.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand, .menu ul.list li.title { 2 | font-weight: bold; 3 | color: #3c3c3c; 4 | padding-bottom: 5px; 5 | } 6 | 7 | .menu ul.list li a[data-type="chapter-link"], .menu ul.list li.chapter .simple { 8 | font-weight: bold; 9 | border-top: 1px solid #ddd; 10 | border-bottom: 1px solid #ddd; 11 | font-size: 14px; 12 | } 13 | 14 | .menu ul.list li a[href="./routes.html"] { 15 | border-bottom: none; 16 | } 17 | 18 | .menu ul.list > li:nth-child(2) { 19 | display: none; 20 | } 21 | 22 | .menu ul.list li.chapter ul.links { 23 | background: #fff; 24 | padding-left: 0; 25 | } 26 | 27 | .menu ul.list li.chapter ul.links li { 28 | border-bottom: 1px solid #ddd; 29 | padding-left: 20px; 30 | } 31 | 32 | .menu ul.list li.chapter ul.links li:last-child { 33 | border-bottom: none; 34 | } 35 | 36 | .menu ul.list li a.active { 37 | color: inherit; 38 | font-weight: bold; 39 | } 40 | 41 | #book-search-input { 42 | margin-bottom: 0; 43 | border-bottom: none; 44 | } 45 | .menu ul.list li.divider { 46 | margin: 0; 47 | } 48 | -------------------------------------------------------------------------------- /projects/sample/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [AppComponent] 9 | }).compileComponents(); 10 | })); 11 | 12 | it('should create the app', async(() => { 13 | const fixture = TestBed.createComponent(AppComponent); 14 | const app = fixture.debugElement.componentInstance; 15 | expect(app).toBeTruthy(); 16 | })); 17 | 18 | it(`should have as title 'app'`, async(() => { 19 | const fixture = TestBed.createComponent(AppComponent); 20 | const app = fixture.debugElement.componentInstance; 21 | expect(app.title).toEqual('app'); 22 | })); 23 | 24 | it('should render title in a h1 tag', async(() => { 25 | const fixture = TestBed.createComponent(AppComponent); 26 | fixture.detectChanges(); 27 | const compiled = fixture.debugElement.nativeElement; 28 | expect(compiled.querySelector('h1').textContent).toContain( 29 | 'Welcome to app!' 30 | ); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-search/flight-search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { OAuthService } from 'angular-oauth2-oidc'; 3 | import { Flight } from '../../entities/flight'; 4 | import { FlightService } from '../services/flight.service'; 5 | 6 | @Component({ 7 | selector: 'flight-search', 8 | templateUrl: './flight-search.component.html', 9 | styleUrls: ['./flight-search.component.css'] 10 | }) 11 | export class FlightSearchComponent { 12 | public from: string = 'Graz'; 13 | public to: string = ''; 14 | public selectedFlight: Flight; 15 | 16 | constructor( 17 | private flightService: FlightService, 18 | private oauthService: OAuthService 19 | ) { 20 | console.debug('access-token', this.oauthService.getAccessToken()); 21 | } 22 | 23 | // cmp.flights 24 | public get flights() { 25 | return this.flightService.flights; 26 | } 27 | 28 | public select(f: Flight): void { 29 | this.selectedFlight = f; 30 | } 31 | 32 | public search(): void { 33 | this.flightService.find(this.from, this.to); 34 | 35 | // .map(function(resp) { return resp.json() }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/services/flight.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 3 | import { BASE_URL } from '../../app.tokens'; 4 | import { Observable } from 'rxjs'; 5 | import { Flight } from '../../entities/flight'; 6 | import { OAuthService } from 'angular-oauth2-oidc'; 7 | 8 | @Injectable() 9 | export class FlightService { 10 | constructor( 11 | private oauthService: OAuthService, 12 | private http: HttpClient, 13 | @Inject(BASE_URL) private baseUrl: string 14 | ) {} 15 | 16 | public flights: Array = []; 17 | 18 | find(from: string, to: string): void { 19 | let url = this.baseUrl + '/api/flight'; 20 | let headers = new HttpHeaders().set('Accept', 'application/json'); 21 | //.set('Authorization', 'Bearer ' + this.oauthService.getAccessToken()); 22 | 23 | let params = new HttpParams().set('from', from).set('to', to); 24 | 25 | this.http.get(url, { headers, params }).subscribe( 26 | flights => { 27 | this.flights = flights; 28 | }, 29 | err => { 30 | console.warn('status', err.status); 31 | } 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/sample/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function() {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /docs/js/sourceCode.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | var $tabSource = document.querySelector('#source-tab'), 3 | $prismPre = document.querySelector('pre.compodoc-sourcecode'); 4 | if ($prismPre) { 5 | $prismCode = $prismPre.querySelector('code'), 6 | $content = document.querySelector('.content'), 7 | prismLinks = document.querySelectorAll('.link-to-prism') 8 | 9 | for (var i = 0; i < prismLinks.length; i++) { 10 | prismLinks[i].addEventListener('click', linkToPrism, false); 11 | } 12 | 13 | function linkToPrism(event) { 14 | var targetLine = event.target.getAttribute('data-line'); 15 | event.preventDefault(); 16 | 17 | $prismPre.setAttribute('data-line', targetLine); 18 | Prism.highlightElement($prismCode, function() {}); 19 | 20 | $tabSource.click(); 21 | 22 | setTimeout(function() { 23 | var $prismHighlightLine = document.querySelector('.line-highlight'), 24 | top = parseInt(getComputedStyle($prismHighlightLine)['top']); 25 | $content.scrollTop = top; 26 | }, 500); 27 | }; 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /projects/sample/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { PasswordFlowLoginComponent } from './password-flow-login/password-flow-login.component'; 2 | import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; 3 | import { HomeComponent } from './home/home.component'; 4 | import { FlightHistoryComponent } from './flight-history/flight-history.component'; 5 | import { CustomPreloadingStrategy } from './shared/preload/custom-preloading.strategy'; 6 | 7 | let APP_ROUTES: Routes = [ 8 | { 9 | path: '', 10 | redirectTo: 'home', 11 | pathMatch: 'full' 12 | }, 13 | { 14 | path: 'home', 15 | component: HomeComponent 16 | }, 17 | { 18 | path: 'password-flow-login', 19 | component: PasswordFlowLoginComponent 20 | }, 21 | { 22 | path: 'flight-booking', 23 | loadChildren: './flight-booking/flight-booking.module#FlightBookingModule' 24 | }, 25 | { 26 | path: 'history', 27 | component: FlightHistoryComponent, 28 | outlet: 'aux' 29 | }, 30 | { 31 | path: '**', 32 | redirectTo: 'home' 33 | } 34 | ]; 35 | 36 | export let AppRouterModule = RouterModule.forRoot(APP_ROUTES, { 37 | preloadingStrategy: CustomPreloadingStrategy 38 | // useHash: true, 39 | // initialNavigation: false 40 | }); 41 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-booking.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RouterModule } from '@angular/router'; 2 | import { FlightSearchComponent } from './flight-search/flight-search.component'; 3 | import { PassengerSearchComponent } from './passenger-search/passenger-search.component'; 4 | import { FlightEditComponent } from './flight-edit/flight-edit.component'; 5 | import { FlightBookingComponent } from './flight-booking.component'; 6 | import { AuthGuard } from '../shared/auth/auth.guard'; 7 | import { LeaveComponentGuard } from '../shared/deactivation/LeaveComponentGuard'; 8 | 9 | let FLIGHT_BOOKING_ROUTES: Routes = [ 10 | { 11 | path: '', 12 | component: FlightBookingComponent, 13 | canActivate: [AuthGuard], 14 | children: [ 15 | { 16 | path: 'flight-search', 17 | component: FlightSearchComponent 18 | }, 19 | { 20 | path: 'passenger-search', 21 | component: PassengerSearchComponent 22 | }, 23 | { 24 | path: 'flight-edit/:id', 25 | component: FlightEditComponent, 26 | canDeactivate: [LeaveComponentGuard] 27 | } 28 | ] 29 | } 30 | ]; 31 | 32 | export let FlightBookingRouterModule = RouterModule.forChild( 33 | FLIGHT_BOOKING_ROUTES 34 | ); 35 | -------------------------------------------------------------------------------- /projects/sample/src/app/shared/validation/city.validator.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, Attribute } from '@angular/core'; 2 | import { 3 | NG_VALIDATORS, 4 | Validator, 5 | AbstractControl, 6 | FormGroup 7 | } from '@angular/forms'; 8 | 9 | @Directive({ 10 | selector: 'input[city]', // 11 | providers: [ 12 | { 13 | provide: NG_VALIDATORS, 14 | useExisting: CityValidatorDirective, 15 | multi: true 16 | } 17 | ] 18 | }) 19 | export class CityValidatorDirective implements Validator { 20 | // @Input() city: string; 21 | 22 | constructor(@Attribute('city') private city: string) {} 23 | 24 | validate(c: AbstractControl): any { 25 | let formGroup = c.root; 26 | let otherValueCtrl = formGroup.controls['to']; 27 | 28 | if (!otherValueCtrl) return {}; 29 | 30 | let otherValue = otherValueCtrl.value; 31 | 32 | if (otherValue == c.value) { 33 | return { 34 | city: 'rundflug' 35 | }; 36 | } 37 | 38 | if (!this.city) return {}; 39 | 40 | let allowed = this.city.split(','); //['Graz', 'Hamburg', 'Wien', 'Frankfurt']; 41 | 42 | if (allowed.indexOf(c.value) == -1) { 43 | return { 44 | city: true 45 | }; 46 | } 47 | 48 | return {}; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /projects/lib/src/events.ts: -------------------------------------------------------------------------------- 1 | export type EventType = 2 | | 'discovery_document_loaded' 3 | | 'received_first_token' 4 | | 'jwks_load_error' 5 | | 'invalid_nonce_in_state' 6 | | 'discovery_document_load_error' 7 | | 'discovery_document_validation_error' 8 | | 'user_profile_loaded' 9 | | 'user_profile_load_error' 10 | | 'token_received' 11 | | 'token_error' 12 | | 'token_refreshed' 13 | | 'token_refresh_error' 14 | | 'silent_refresh_error' 15 | | 'silently_refreshed' 16 | | 'silent_refresh_timeout' 17 | | 'token_validation_error' 18 | | 'token_expires' 19 | | 'session_changed' 20 | | 'session_error' 21 | | 'session_terminated' 22 | | 'logout'; 23 | 24 | export abstract class OAuthEvent { 25 | constructor(readonly type: EventType) {} 26 | } 27 | 28 | export class OAuthSuccessEvent extends OAuthEvent { 29 | constructor(type: EventType, readonly info: any = null) { 30 | super(type); 31 | } 32 | } 33 | 34 | export class OAuthInfoEvent extends OAuthEvent { 35 | constructor(type: EventType, readonly info: any = null) { 36 | super(type); 37 | } 38 | } 39 | 40 | export class OAuthErrorEvent extends OAuthEvent { 41 | constructor( 42 | type: EventType, 43 | readonly reason: object, 44 | readonly params: object = null 45 | ) { 46 | super(type); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/js/libs/innersvg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * innerHTML property for SVGElement 3 | * Copyright(c) 2010, Jeff Schiller 4 | * 5 | * Licensed under the Apache License, Version 2 6 | * 7 | * Minor modifications by Chris Price to only polyfill when required. 8 | */ 9 | !function(e){if(e&&!("innerHTML"in e.prototype)){var t=function(e,r){var i=e.nodeType;if(3==i)r.push(e.textContent.replace(/&/,"&").replace(/",">"));else if(1==i){if(r.push("<",e.tagName),e.hasAttributes())for(var n=e.attributes,s=0,o=n.length;s");for(var h=e.childNodes,s=0,o=h.length;s")}else r.push("/>")}else{if(8!=i)throw"Error serializing XML. Unhandled node of type: "+i;r.push("\x3c!--",e.nodeValue,"--\x3e")}};Object.defineProperty(e.prototype,"innerHTML",{get:function(){for(var e=[],r=this.firstChild;r;)t(r,e),r=r.nextSibling;return e.join("")},set:function(e){for(;this.firstChild;)this.removeChild(this.firstChild);try{var t=new DOMParser;t.async=!1,sXML=""+e+"";for(var r=t.parseFromString(sXML,"text/xml").documentElement.firstChild;r;)this.appendChild(this.ownerDocument.importNode(r,!0)),r=r.nextSibling}catch(e){throw new Error("Error parsing XML string")}}})}}((0,eval)("this").SVGElement); -------------------------------------------------------------------------------- /docs/styles/stripe.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand { 2 | color: #0099e5; 3 | } 4 | 5 | .menu ul.list li a[data-type="chapter-link"], .menu ul.list li.chapter .simple { 6 | color: #939da3; 7 | text-transform: uppercase; 8 | } 9 | 10 | .content h1, .content h2, .content h3, .content h4, .content h5 { 11 | color: #292e31; 12 | font-weight: normal; 13 | } 14 | 15 | .content { 16 | color: #4c555a; 17 | } 18 | 19 | .menu ul.list li.title { 20 | padding: 5px 0; 21 | } 22 | 23 | a { 24 | color: #0099e5; 25 | text-decoration: none; 26 | } 27 | a:hover { 28 | color: #292e31; 29 | text-decoration: none; 30 | } 31 | 32 | .menu ul.list li:nth-child(2) { 33 | margin-top: 0; 34 | } 35 | 36 | .menu ul.list li.title a, .navbar a { 37 | color: #0099e5; 38 | text-decoration: none; 39 | font-size: 16px; 40 | } 41 | 42 | .menu ul.list li a.active { 43 | color: #0099e5; 44 | } 45 | 46 | code { 47 | box-sizing: border-box; 48 | display: inline-block; 49 | padding: 0 5px; 50 | background: #fafcfc; 51 | border-radius: 4px; 52 | color: #b93d6a; 53 | font-size: 13px; 54 | line-height: 20px 55 | } 56 | 57 | pre { 58 | margin: 0; 59 | padding: 12px 12px; 60 | background: #272b2d; 61 | border-radius: 5px; 62 | font-size: 13px; 63 | line-height: 1.5em; 64 | font-weight: 500 65 | } 66 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-edit/flight-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | @Component({ 5 | template: ` 6 |

Flight Edit!

7 |

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

8 | 9 |
10 |
11 | Daten wurden nicht gespeichert! Trotzdem Maske verlassen? 12 |
13 |
14 | Ja 15 | Nein 16 |
17 |
18 | 19 | 20 | 21 | 22 | ` 23 | }) 24 | export class FlightEditComponent implements OnInit { 25 | public id: string; 26 | 27 | constructor(private route: ActivatedRoute) { 28 | route.params.subscribe(p => { 29 | this.id = p['id']; 30 | }); 31 | } 32 | 33 | ngOnInit() {} 34 | 35 | exitWarning = { 36 | show: false, 37 | resolve: null 38 | }; 39 | 40 | decide(decision: boolean) { 41 | this.exitWarning.show = false; 42 | this.exitWarning.resolve(decision); 43 | } 44 | 45 | canDeactivate() { 46 | this.exitWarning.show = true; 47 | return new Promise(resolve => { 48 | this.exitWarning.resolve = resolve; 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /projects/sample/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { CommonModule } from '@angular/common'; 4 | import { CityPipe } from './pipes/city.pipe'; 5 | import { CityValidatorDirective } from './validation/city.validator'; 6 | import { RoundTrip } from './validation/roundtrip.validator'; 7 | import { AsyncCityValidatorDirective } from './validation/async-city.validator'; 8 | import { DateComponent } from './date/date.component'; 9 | import { AuthGuard } from './auth/auth.guard'; 10 | import { LeaveComponentGuard } from './deactivation/LeaveComponentGuard'; 11 | import { CustomPreloadingStrategy } from './preload/custom-preloading.strategy'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | FormsModule, // [(ngModel)] 16 | CommonModule // ngFor, ngIf, ngStyle, ngClass, date, json 17 | ], 18 | providers: [], 19 | declarations: [ 20 | CityPipe, 21 | CityValidatorDirective, 22 | AsyncCityValidatorDirective, 23 | RoundTrip, 24 | DateComponent 25 | ], 26 | exports: [ 27 | CityPipe, 28 | CityValidatorDirective, 29 | AsyncCityValidatorDirective, 30 | RoundTrip, 31 | DateComponent 32 | ] 33 | }) 34 | export class SharedModule { 35 | static forRoot(): ModuleWithProviders { 36 | return { 37 | providers: [AuthGuard, LeaveComponentGuard, CustomPreloadingStrategy], 38 | ngModule: SharedModule 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/styles/laravel.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand { 2 | color: #f4645f; 3 | text-decoration: none; 4 | font-size: 16px; 5 | } 6 | 7 | .menu ul.list li a[data-type="chapter-link"], .menu ul.list li.chapter .simple { 8 | color: #525252; 9 | border-bottom: 1px dashed rgba(0,0,0,.1); 10 | } 11 | 12 | .content h1, .content h2, .content h3, .content h4, .content h5 { 13 | color: #292e31; 14 | font-weight: normal; 15 | } 16 | 17 | .content { 18 | color: #4c555a; 19 | } 20 | 21 | a { 22 | color: #f4645f; 23 | text-decoration: underline; 24 | } 25 | a:hover { 26 | color: #f1362f; 27 | } 28 | 29 | .menu ul.list li:nth-child(2) { 30 | margin-top: 0; 31 | } 32 | 33 | .menu ul.list li.title a { 34 | color: #f4645f; 35 | text-decoration: none; 36 | font-size: 16px; 37 | } 38 | 39 | .menu ul.list li a { 40 | color: #f4645f; 41 | text-decoration: none; 42 | } 43 | .menu ul.list li a.active { 44 | color: #f4645f; 45 | font-weight: bold; 46 | } 47 | 48 | code { 49 | box-sizing: border-box; 50 | display: inline-block; 51 | padding: 0 5px; 52 | background: #f0f2f1; 53 | border-radius: 3px; 54 | color: #b93d6a; 55 | font-size: 13px; 56 | line-height: 20px; 57 | box-shadow: 0 1px 1px rgba(0,0,0,.125); 58 | } 59 | 60 | pre { 61 | margin: 0; 62 | padding: 12px 12px; 63 | background: rgba(238,238,238,.35); 64 | border-radius: 3px; 65 | font-size: 13px; 66 | line-height: 1.5em; 67 | font-weight: 500; 68 | box-shadow: 0 1px 1px rgba(0,0,0,.125); 69 | } 70 | -------------------------------------------------------------------------------- /docs-src/session-checks.md: -------------------------------------------------------------------------------- 1 | # Session Checks 2 | 3 | Beginning with version 2.1, you can receive a notification when the user signs out with the identity provider. 4 | This is implemented as defined by the OpenID Connect Session Management 1.0 spec. 5 | 6 | When this option is activated, the library also automatically ends your local session. This means, the current tokens 7 | are deleted by calling ``logOut``. In addition to that, the library sends a session_terminated event, you can register 8 | for to perform a custom action. 9 | 10 | Please note that this option can only be used when also the identity provider in question supports it. 11 | 12 | ## Configuration 13 | 14 | To activate the session checks that leads to the mentioned notifications, set the configuration property 15 | ``sessionChecksEnabled``: 16 | 17 | ```TypeScript 18 | import { AuthConfig } from 'angular-oauth2-oidc'; 19 | 20 | export const authConfig: AuthConfig = { 21 | issuer: 'https://steyer-identity-server.azurewebsites.net/identity', 22 | redirectUri: window.location.origin + '/index.html', 23 | silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', 24 | clientId: 'spa-demo', 25 | scope: 'openid profile email voucher', 26 | 27 | // Activate Session Checks: 28 | sessionChecksEnabled: true, 29 | } 30 | ``` 31 | 32 | ## Events 33 | To get notified, you can hook up for the event ``session_terminated``: 34 | 35 | ```TypeScript 36 | this.oauthService.events.filter(e => e.type === 'session_terminated').subscribe(e => { 37 | console.debug('Your session has been terminated!'); 38 | }) 39 | ``` -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-booking.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FlightSearchComponent } from './flight-search/flight-search.component'; 3 | import { FlightCardComponent } from './flight-card/flight.card.component'; 4 | import { AltFlightCardComponent } from './alt-flight-card/alt-flight.card.component'; 5 | import { FlightListComponent } from './alt-flight-card/flight-list'; 6 | import { PassengerSearchComponent } from './passenger-search/passenger-search.component'; 7 | import { FlightEditComponent } from './flight-edit/flight-edit.component'; 8 | import { FlightSearchReactiveComponent } from './flight-search-reactive/flight-search-reactive.component'; 9 | import { CommonModule } from '@angular/common'; 10 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 11 | import { SharedModule } from '../shared/shared.module'; 12 | import { FlightBookingRouterModule } from './flight-booking.routes'; 13 | import { FlightBookingComponent } from './flight-booking.component'; 14 | import { FlightService } from './services/flight.service'; 15 | 16 | @NgModule({ 17 | imports: [ 18 | CommonModule, // ngFor 19 | FormsModule, 20 | ReactiveFormsModule, 21 | SharedModule, 22 | FlightBookingRouterModule 23 | ], 24 | declarations: [ 25 | FlightSearchComponent, 26 | FlightCardComponent, 27 | AltFlightCardComponent, 28 | FlightListComponent, 29 | FlightSearchReactiveComponent, 30 | PassengerSearchComponent, 31 | FlightEditComponent, 32 | FlightBookingComponent 33 | ], 34 | providers: [FlightService], 35 | exports: [] 36 | }) 37 | export class FlightBookingModule {} 38 | -------------------------------------------------------------------------------- /projects/lib/src/url-helper.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class UrlHelperService { 5 | public getHashFragmentParams(customHashFragment?: string): object { 6 | let hash = customHashFragment || window.location.hash; 7 | 8 | hash = decodeURIComponent(hash); 9 | 10 | if (hash.indexOf('#') !== 0) { 11 | return {}; 12 | } 13 | 14 | const questionMarkPosition = hash.indexOf('?'); 15 | 16 | if (questionMarkPosition > -1) { 17 | hash = hash.substr(questionMarkPosition + 1); 18 | } else { 19 | hash = hash.substr(1); 20 | } 21 | 22 | return this.parseQueryString(hash); 23 | } 24 | 25 | public parseQueryString(queryString: string): object { 26 | const data = {}; 27 | let 28 | pairs, 29 | pair, 30 | separatorIndex, 31 | escapedKey, 32 | escapedValue, 33 | key, 34 | value; 35 | 36 | if (queryString === null) { 37 | return data; 38 | } 39 | 40 | pairs = queryString.split('&'); 41 | 42 | for (let i = 0; i < pairs.length; i++) { 43 | pair = pairs[i]; 44 | separatorIndex = pair.indexOf('='); 45 | 46 | if (separatorIndex === -1) { 47 | escapedKey = pair; 48 | escapedValue = null; 49 | } else { 50 | escapedKey = pair.substr(0, separatorIndex); 51 | escapedValue = pair.substr(separatorIndex + 1); 52 | } 53 | 54 | key = decodeURIComponent(escapedKey); 55 | value = decodeURIComponent(escapedValue); 56 | 57 | if (key.substr(0, 1) === '/') { key = key.substr(1); } 58 | 59 | data[key] = value; 60 | } 61 | 62 | return data; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /projects/sample/src/app/password-flow-login/password-flow-login.component.html: -------------------------------------------------------------------------------- 1 |

Welcome!

2 |

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

3 | 4 | 5 |
6 |
7 |

Login with Username/Password

8 | 9 |

10 | Login wasn't successfull. 11 |

12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 | Username/Password: max/geheim 31 |
32 |
33 | 34 |
35 |
36 |

37 | access_token_expiration: {{access_token_expiration}} 38 |

39 |
40 |
41 | 42 |
43 |
44 |

45 | access_token: {{access_token}} 46 |

47 |
48 | user profile: 49 |
{{userProfile | json}}
50 |
51 | 52 |
53 |
54 | -------------------------------------------------------------------------------- /docs-src/implicit-flow-config-discovery.md: -------------------------------------------------------------------------------- 1 | # Original Config API 2 | 3 | To configure the library you just have to set some properties on startup. For this, the following sample uses the constructor of the AppComponent which is called before routing kicks in. 4 | 5 | Please note that the following sample uses the original config API. For information about the new config API have a look to the project's README above. 6 | 7 | ```TypeScript 8 | @Component({ ... }) 9 | export class AppComponent { 10 | 11 | constructor(private oauthService: OAuthService) { 12 | 13 | // URL of the SPA to redirect the user to after login 14 | this.oauthService.redirectUri = window.location.origin + "/index.html"; 15 | 16 | // The SPA's id. The SPA is registerd with this id at the auth-server 17 | this.oauthService.clientId = "spa-demo"; 18 | 19 | // set the scope for the permissions the client should request 20 | // The first three are defined by OIDC. The 4th is a usecase-specific one 21 | this.oauthService.scope = "openid profile email voucher"; 22 | 23 | // The name of the auth-server that has to be mentioned within the token 24 | this.oauthService.issuer = "https://steyer-identity-server.azurewebsites.net/identity"; 25 | 26 | // Load Discovery Document and then try to login the user 27 | this.oauthService.loadDiscoveryDocument().then(() => { 28 | 29 | // This method just tries to parse the token(s) within the url when 30 | // the auth-server redirects the user back to the web-app 31 | // It dosn't send the user the the login page 32 | this.oauthService.tryLogin(); 33 | 34 | }); 35 | 36 | } 37 | 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs-src/summary.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Getting Started", 4 | "file": "getting-started.md" 5 | }, 6 | { 7 | "title": "Preserving State (like the Requested URL)", 8 | "file": "preserving-state.md" 9 | }, 10 | { 11 | "title": "Refreshing a Token (Silent Refresh)", 12 | "file": "silent-refresh.md" 13 | }, 14 | { 15 | "title": "Working with HttpInterceptors", 16 | "file": "interceptors.md" 17 | }, 18 | { 19 | "title": "Callback after login", 20 | "file": "callback-after-login.md" 21 | }, 22 | { 23 | "title": "Custom Query Parameters", 24 | "file": "custom-query-parameter.md" 25 | }, 26 | { 27 | "title": "Events", 28 | "file": "events.md" 29 | }, 30 | { 31 | "title": "Routing with the HashStrategy", 32 | "file": "hash-strategy.md" 33 | }, 34 | { 35 | "title": "Adapt id_token Validation", 36 | "file": "id-token-validation.md" 37 | }, 38 | { 39 | "title": "Session Checks", 40 | "file": "session-checks.md" 41 | }, 42 | { 43 | "title": "Server Side Rendering", 44 | "file": "server-side-rendering.md" 45 | }, 46 | { 47 | "title": "Configure Library for Implicit Flow without discovery document", 48 | "file": "implicit-flow-config-no-discovery.md" 49 | }, 50 | { 51 | "title": "Using SystemJS", 52 | "file": "systemjs.md" 53 | }, 54 | { 55 | "title": "Original Config API", 56 | "file": "implicit-flow-config-discovery.md" 57 | }, 58 | { 59 | "title": "Using Password Flow", 60 | "file": "password-flow.md" 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /projects/sample/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | Status: {{ givenName ? 'logged in' : 'logged out' }} 2 |

Welcome!

3 |

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

4 | 5 |
6 |
7 |

Login with Authorization Server

8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 | Username/Password: max/geheim 21 |
22 |
23 | 24 |
25 |
26 |

27 | access_token_expiration: {{access_token_expiration}} 28 |

29 |

30 | id_token_expiration: {{id_token_expiration}} 31 |

32 |
33 |
34 | 35 |
36 |
37 |

38 | access_token: {{access_token}} 39 |

40 |

41 | id_token: {{id_token}} 42 |

43 |
44 | user profile: 45 |
{{userProfile | json}}
46 |
47 | 48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /projects/sample/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { authConfig } from './auth.config'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | import { BrowserModule } from '@angular/platform-browser'; 6 | import { 7 | AuthConfig, 8 | JwksValidationHandler, 9 | OAuthModule, 10 | ValidationHandler 11 | } from 'angular-oauth2-oidc'; 12 | 13 | import { AppComponent } from './app.component'; 14 | import { AppRouterModule } from './app.routes'; 15 | import { BASE_URL } from './app.tokens'; 16 | import { FlightHistoryComponent } from './flight-history/flight-history.component'; 17 | import { HomeComponent } from './home/home.component'; 18 | import { PasswordFlowLoginComponent } from './password-flow-login/password-flow-login.component'; 19 | import { SharedModule } from './shared/shared.module'; 20 | 21 | @NgModule({ 22 | imports: [ 23 | BrowserModule, 24 | FormsModule, 25 | ReactiveFormsModule, 26 | SharedModule.forRoot(), 27 | AppRouterModule, 28 | HttpClientModule, 29 | OAuthModule.forRoot({ 30 | resourceServer: { 31 | allowedUrls: ['http://www.angular.at/api'], 32 | sendAccessToken: true 33 | } 34 | }) 35 | ], 36 | declarations: [ 37 | AppComponent, 38 | HomeComponent, 39 | FlightHistoryComponent, 40 | PasswordFlowLoginComponent 41 | ], 42 | providers: [ 43 | // {provide: AuthConfig, useValue: authConfig }, 44 | // { provide: OAuthStorage, useClass: DemoStorage }, 45 | // { provide: ValidationHandler, useClass: JwksValidationHandler }, 46 | { provide: BASE_URL, useValue: 'http://www.angular.at' } 47 | ], 48 | bootstrap: [AppComponent] 49 | }) 50 | export class AppModule {} 51 | -------------------------------------------------------------------------------- /docs-src/implicit-flow-config-no-discovery.md: -------------------------------------------------------------------------------- 1 | # Configure Library for Implicit Flow (without discovery document) 2 | 3 | When you don't have a discovery document, you have to configure more properties manually: 4 | 5 | Please note that the following sample uses the original config API. For information about the new config API have a look to the project's README above. 6 | 7 | ```TypeScript 8 | @Component({ ... }) 9 | export class AppComponent { 10 | 11 | constructor(private oauthService: OAuthService) { 12 | 13 | // Login-Url 14 | this.oauthService.loginUrl = "https://steyer-identity-server.azurewebsites.net/identity/connect/authorize"; //Id-Provider? 15 | 16 | // URL of the SPA to redirect the user to after login 17 | this.oauthService.redirectUri = window.location.origin + "/index.html"; 18 | 19 | // The SPA's id. Register SPA with this id at the auth-server 20 | this.oauthService.clientId = "spa-demo"; 21 | 22 | // set the scope for the permissions the client should request 23 | this.oauthService.scope = "openid profile email voucher"; 24 | 25 | // Use setStorage to use sessionStorage or another implementation of the TS-type Storage 26 | // instead of localStorage 27 | this.oauthService.setStorage(sessionStorage); 28 | 29 | // To also enable single-sign-out set the url for your auth-server's logout-endpoint here 30 | this.oauthService.logoutUrl = "https://steyer-identity-server.azurewebsites.net/identity/connect/endsession"; 31 | 32 | // This method just tries to parse the token(s) within the url when 33 | // the auth-server redirects the user back to the web-app 34 | // It doesn't send the user the the login page 35 | this.oauthService.tryLogin(); 36 | 37 | 38 | } 39 | 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/graph/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | Legend 13 | 14 |  Declarations 15 | 16 |  Module 17 | 18 |  Bootstrap 19 | 20 |  Providers 21 | 22 |  Exports 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/js/svg-pan-zoom.controls.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | panZoom = svgPanZoom(document.getElementById('module-graph-svg').querySelector('svg'), { 3 | zoomEnabled: true, 4 | minZoom: 1, 5 | maxZoom: 5 6 | }); 7 | 8 | document.getElementById('zoom-in').addEventListener('click', function(ev) { 9 | ev.preventDefault(); 10 | panZoom.zoomIn(); 11 | }); 12 | 13 | document.getElementById('zoom-out').addEventListener('click', function(ev) { 14 | ev.preventDefault(); 15 | panZoom.zoomOut(); 16 | }); 17 | 18 | document.getElementById('reset').addEventListener('click', function(ev) { 19 | ev.preventDefault(); 20 | panZoom.resetZoom(); 21 | panZoom.resetPan(); 22 | }); 23 | 24 | var overviewFullscreen = false, 25 | originalOverviewHeight; 26 | 27 | document.getElementById('fullscreen').addEventListener('click', function(ev) { 28 | if (overviewFullscreen) { 29 | document.getElementById('module-graph-svg').style.height = originalOverviewHeight; 30 | overviewFullscreen = false; 31 | if (ev.target) { 32 | ev.target.classList.remove('fa-compress'); 33 | } 34 | } else { 35 | originalOverviewHeight = document.getElementById('module-graph-svg').style.height; 36 | document.getElementById('module-graph-svg').style.height = '85vh'; 37 | overviewFullscreen = true; 38 | if (ev.target) { 39 | ev.target.classList.add('fa-compress'); 40 | } 41 | } 42 | document.getElementById('module-graph-svg').querySelector('svg').style.height = document.getElementById('module-graph-svg').clientHeight; 43 | setTimeout(function() { 44 | panZoom.resize(); 45 | panZoom.fit(); 46 | panZoom.center(); 47 | }, 0) 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /docs/js/search/search-lunr.js: -------------------------------------------------------------------------------- 1 | (function(compodoc) { 2 | 3 | function LunrSearchEngine() { 4 | this.index = undefined; 5 | this.store = {}; 6 | this.name = 'LunrSearchEngine'; 7 | } 8 | 9 | LunrSearchEngine.prototype.init = function() { 10 | var that = this, 11 | d = new promise.Promise(); 12 | 13 | that.index = lunr.Index.load(COMPODOC_SEARCH_INDEX.index); 14 | that.store = COMPODOC_SEARCH_INDEX.store; 15 | d.done(); 16 | 17 | return d; 18 | }; 19 | 20 | LunrSearchEngine.prototype.search = function(q, offset, length) { 21 | var that = this, 22 | results = [], 23 | d = new promise.Promise(); 24 | 25 | if (this.index) { 26 | results = $.map(this.index.search(q), function(result) { 27 | var doc = that.store[result.ref]; 28 | 29 | return { 30 | title: doc.title, 31 | url: doc.url, 32 | body: doc.summary || doc.body 33 | }; 34 | }); 35 | } 36 | 37 | d.done({ 38 | query: q, 39 | results: results.slice(0, length), 40 | count: results.length 41 | }); 42 | 43 | return d; 44 | }; 45 | 46 | compodoc.addEventListener(compodoc.EVENTS.READY, function(event) { 47 | var engine = new LunrSearchEngine(), 48 | initialized = false; 49 | 50 | function query(q, offset, length) { 51 | if (!initialized) throw new Error('Search has not been initialized'); 52 | return engine.search(q, offset, length); 53 | } 54 | 55 | compodoc.search = { 56 | query: query 57 | }; 58 | 59 | engine.init() 60 | .then(function() { 61 | initialized = true; 62 | compodoc.dispatchEvent({ 63 | type: compodoc.EVENTS.SEARCH_READY 64 | }); 65 | }); 66 | }); 67 | })(compodoc); 68 | -------------------------------------------------------------------------------- /docs/styles/vagrant.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand { 2 | background: white; 3 | color: #8d9ba8; 4 | } 5 | 6 | .menu .list { 7 | background: #0c5593; 8 | } 9 | 10 | .menu .chapter { 11 | padding: 0 20px; 12 | } 13 | 14 | .menu ul.list li a[data-type="chapter-link"], .menu ul.list li.chapter .simple { 15 | color: white; 16 | text-transform: uppercase; 17 | border-bottom: 1px solid rgba(255,255,255,0.4); 18 | } 19 | 20 | .content h1, .content h2, .content h3, .content h4, .content h5 { 21 | color: #292e31; 22 | font-weight: normal; 23 | } 24 | 25 | .content { 26 | color: #4c555a; 27 | } 28 | 29 | a { 30 | color: #0094bf; 31 | text-decoration: underline; 32 | } 33 | a:hover { 34 | color: #f1362f; 35 | } 36 | 37 | .menu ul.list li.title { 38 | background: white; 39 | padding-bottom: 5px; 40 | } 41 | 42 | .menu ul.list li:nth-child(2) { 43 | margin-top: 0; 44 | } 45 | 46 | .menu ul.list li:nth-last-child(2) { 47 | background: none; 48 | } 49 | 50 | .menu ul.list li.title a { 51 | padding: 10px 15px; 52 | } 53 | 54 | .menu ul.list li.title a, .navbar a { 55 | color: #8d9ba8; 56 | text-decoration: none; 57 | font-size: 16px; 58 | font-weight: 300; 59 | } 60 | 61 | .menu ul.list li a { 62 | color: white; 63 | padding: 10px; 64 | font-weight: 300; 65 | text-decoration: none; 66 | } 67 | .menu ul.list li a.active { 68 | color: white; 69 | font-weight: bold; 70 | } 71 | 72 | .copyright { 73 | color: white; 74 | background: #000; 75 | } 76 | 77 | code { 78 | box-sizing: border-box; 79 | display: inline-block; 80 | padding: 0 5px; 81 | background: rgba(0,148,191,0.1); 82 | border-radius: 3px; 83 | color: #0094bf; 84 | font-size: 13px; 85 | line-height: 20px; 86 | } 87 | 88 | pre { 89 | margin: 0; 90 | padding: 12px 12px; 91 | background: rgba(238,238,238,.35); 92 | border-radius: 3px; 93 | font-size: 13px; 94 | line-height: 1.5em; 95 | font-weight: 500; 96 | } 97 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## New Features in Version 4.0.0 4 | 5 | See [Release Notes](https://github.com/manfredsteyer/angular-oauth2-oidc/releases/tag/4.0.0) 6 | 7 | ## New Features in Version 3.1 8 | 9 | See [Release Notes](https://github.com/manfredsteyer/angular-oauth2-oidc/releases/tag/3.1) 10 | 11 | ## New Features in Version 3.0 12 | 13 | See [Release Notes](https://github.com/manfredsteyer/angular-oauth2-oidc/releases/tag/3.0.1) 14 | 15 | ## New Features in Version 2.1 16 | - New Config API (the original one is still supported) 17 | - New convenience methods in OAuthService to streamline default tasks: 18 | - ``setupAutomaticSilentRefresh()`` 19 | - ``loadDiscoveryDocumentAndTryLogin()`` 20 | - Single Sign out through Session Status Change Notification according to the OpenID Connect Session Management specs. This means, you can be notified when the user logs out using at the login provider. 21 | - Possibility to define the ValidationHandler, the Config as well as the OAuthStorage via DI 22 | - Better structured documentation 23 | 24 | ## New Features in Version 2 25 | - Token Refresh for Implicit Flow by implementing "silent refresh" 26 | - Validating the signature of the received id_token 27 | - Providing Events via the observable ``events``. 28 | - The event ``token_expires`` can be used together with a silent refresh to automatically refresh a token when/ before it expires (see also property ``timeoutFactor``). 29 | 30 | ## Breaking Changes in Version 2 31 | - The property ``oidc`` defaults to ``true``. 32 | - If you are just using oauth2, you have to set ``oidc`` to ``false``. Otherwise, the validation of the user profile will fail! 33 | - By default, ``sessionStorage`` is used. To use ``localStorage`` call method setStorage 34 | - Demands using https as OIDC and OAuth2 relay on it. This rule can be relaxed using the property ``requireHttps``, e. g. for local testing. 35 | - Demands that every url provided by the discovery document starts with the issuer's url. This can be relaxed by using the property ``strictDiscoveryDocumentValidation``. 36 | -------------------------------------------------------------------------------- /projects/lib/src/angular-oauth-oidic.module.ts: -------------------------------------------------------------------------------- 1 | import { OAuthStorage } from './types'; 2 | import { NgModule, ModuleWithProviders } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 5 | 6 | import { OAuthService } from './oauth-service'; 7 | import { UrlHelperService } from './url-helper.service'; 8 | 9 | import { OAuthModuleConfig } from './oauth-module.config'; 10 | import { 11 | OAuthResourceServerErrorHandler, 12 | OAuthNoopResourceServerErrorHandler 13 | } from './interceptors/resource-server-error-handler'; 14 | import { DefaultOAuthInterceptor } from './interceptors/default-oauth.interceptor'; 15 | import { ValidationHandler } from './token-validation/validation-handler'; 16 | import { NullValidationHandler } from './token-validation/null-validation-handler'; 17 | 18 | export function createDefaultStorage() { 19 | return typeof sessionStorage !== 'undefined' ? sessionStorage : null; 20 | } 21 | 22 | @NgModule({ 23 | imports: [CommonModule], 24 | declarations: [], 25 | exports: [] 26 | }) 27 | export class OAuthModule { 28 | static forRoot( 29 | config: OAuthModuleConfig = null, 30 | validationHandlerClass = NullValidationHandler 31 | ): ModuleWithProviders { 32 | // const setupInterceptor = config && config.resourceServer && config.resourceServer.allowedUrls; 33 | 34 | return { 35 | ngModule: OAuthModule, 36 | providers: [ 37 | OAuthService, 38 | UrlHelperService, 39 | { provide: OAuthStorage, useFactory: createDefaultStorage }, 40 | { provide: ValidationHandler, useClass: validationHandlerClass}, 41 | { 42 | provide: OAuthResourceServerErrorHandler, 43 | useClass: OAuthNoopResourceServerErrorHandler 44 | }, 45 | { provide: OAuthModuleConfig, useValue: config }, 46 | { 47 | provide: HTTP_INTERCEPTORS, 48 | useClass: DefaultOAuthInterceptor, 49 | multi: true 50 | } 51 | ] 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/styles/readthedocs.css: -------------------------------------------------------------------------------- 1 | .navbar-default { 2 | background: #2980B9; 3 | border: none; 4 | } 5 | .navbar-default .navbar-brand { 6 | color: #fcfcfc; 7 | } 8 | .menu { 9 | background: #343131; 10 | color: #fcfcfc; 11 | } 12 | .menu ul.list li a { 13 | color: #fcfcfc; 14 | } 15 | 16 | .menu ul.list li a.active { 17 | color: #0099e5; 18 | } 19 | 20 | .menu ul.list li.title { 21 | background: #2980B9; 22 | padding-bottom: 5px; 23 | } 24 | 25 | .menu ul.list li:nth-child(2) { 26 | margin-top: 0; 27 | } 28 | 29 | .menu ul.list li.chapter a, .menu ul.list li.chapter .simple { 30 | color: #555; 31 | text-transform: uppercase; 32 | text-decoration: none; 33 | } 34 | 35 | .menu ul.list li.chapter ul.links a { 36 | color: #b3b3b3; 37 | text-transform: none; 38 | padding-left: 35px; 39 | } 40 | .menu ul.list li.chapter ul.links a:hover { 41 | background: #4E4A4A; 42 | } 43 | 44 | .menu ul.list li.chapter ul.links { 45 | padding-left: 0; 46 | } 47 | 48 | .menu ul.list li.divider { 49 | background: rgba(255, 255, 255, 0.07); 50 | } 51 | 52 | #book-search-input input, #book-search-input input:focus, #book-search-input input:hover { 53 | color: #949494; 54 | } 55 | 56 | .copyright { 57 | color: #b3b3b3; 58 | } 59 | 60 | .content { 61 | background: #fcfcfc; 62 | } 63 | 64 | .content a { 65 | color: #2980B9; 66 | } 67 | .content a:hover { 68 | color: #3091d1; 69 | } 70 | .content a:visited { 71 | color: #9B59B6; 72 | } 73 | .copyright { 74 | background: #272525; 75 | } 76 | .menu ul.list li:nth-last-child(2) { 77 | background: none; 78 | } 79 | code { 80 | white-space: nowrap; 81 | max-width: 100%; 82 | background: #fff; 83 | padding: 2px 5px; 84 | color: #E74C3C; 85 | overflow-x: auto; 86 | border-radius: 0; 87 | } 88 | pre { 89 | white-space: pre; 90 | margin: 0; 91 | padding: 12px 12px; 92 | font-size: 12px; 93 | line-height: 1.5; 94 | display: block; 95 | overflow: auto; 96 | color: #404040; 97 | background: rgba(238,238,238,.35); 98 | } 99 | -------------------------------------------------------------------------------- /projects/sample/src/app/password-flow-login/password-flow-login.component.ts: -------------------------------------------------------------------------------- 1 | import { authPasswordFlowConfig } from '../auth-password-flow.config'; 2 | import { OAuthService } from 'angular-oauth2-oidc'; 3 | import { Component, OnInit } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: 'app-password-flow-login', 7 | templateUrl: './password-flow-login.component.html' 8 | }) 9 | export class PasswordFlowLoginComponent implements OnInit { 10 | userName: string; 11 | password: string; 12 | loginFailed: boolean = false; 13 | userProfile: object; 14 | 15 | constructor(private oauthService: OAuthService) { 16 | // Tweak config for password flow 17 | // This is just needed b/c this demo uses both, 18 | // implicit flow as well as password flow 19 | 20 | this.oauthService.configure(authPasswordFlowConfig); 21 | this.oauthService.loadDiscoveryDocument(); 22 | } 23 | 24 | ngOnInit() {} 25 | 26 | loadUserProfile(): void { 27 | this.oauthService.loadUserProfile().then(up => (this.userProfile = up)); 28 | } 29 | 30 | get access_token() { 31 | return this.oauthService.getAccessToken(); 32 | } 33 | 34 | get access_token_expiration() { 35 | return this.oauthService.getAccessTokenExpiration(); 36 | } 37 | 38 | get givenName() { 39 | var claims = this.oauthService.getIdentityClaims(); 40 | if (!claims) return null; 41 | return claims['given_name']; 42 | } 43 | 44 | get familyName() { 45 | var claims = this.oauthService.getIdentityClaims(); 46 | if (!claims) return null; 47 | return claims['family_name']; 48 | } 49 | 50 | loginWithPassword() { 51 | this.oauthService 52 | .fetchTokenUsingPasswordFlowAndLoadUserProfile( 53 | this.userName, 54 | this.password 55 | ) 56 | .then(() => { 57 | console.debug('successfully logged in'); 58 | this.loginFailed = false; 59 | }) 60 | .catch(err => { 61 | console.error('error logging in', err); 62 | this.loginFailed = true; 63 | }); 64 | } 65 | 66 | logout() { 67 | this.oauthService.logOut(true); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Flight } from '../../entities/flight'; 3 | import { FlightService } from '../services/flight.service'; 4 | import { 5 | FormGroup, 6 | FormBuilder, 7 | Validators, 8 | AbstractControl 9 | } from '@angular/forms'; 10 | import { CityValidatorDirective } from '../../shared/validation/city.validator'; 11 | 12 | @Component({ 13 | selector: 'flight-search-reactive', 14 | templateUrl: 'flight-search-reactive.component.html', 15 | providers: [FlightService], 16 | styleUrls: ['flight-search-reactive.component.css'] 17 | }) 18 | export class FlightSearchReactiveComponent { 19 | public flights: Array = []; 20 | public selectedFlight: Flight; 21 | 22 | public filter: FormGroup; 23 | 24 | public formDesc = []; 25 | 26 | constructor(private flightService: FlightService, private fb: FormBuilder) { 27 | this.formDesc.push({ 28 | label: 'Von', 29 | name: 'from' 30 | }); 31 | 32 | this.formDesc.push({ 33 | label: 'Nach', 34 | name: 'to' 35 | }); 36 | 37 | this.filter = fb.group({ 38 | from: [ 39 | 'Graz', 40 | [ 41 | Validators.required, 42 | Validators.minLength(3), 43 | (c: AbstractControl): any => { 44 | if (c.value != 'Graz' && c.value != 'Hamburg') { 45 | return { 46 | city: true 47 | }; 48 | } 49 | return {}; 50 | } 51 | ] 52 | ], 53 | to: ['Hamburg'] 54 | }); 55 | 56 | this.filter.valueChanges.subscribe(e => { 57 | console.debug('formular geändert', e); 58 | }); 59 | 60 | this.filter.controls['from'].valueChanges.subscribe(e => { 61 | console.debug('from geändert', e); 62 | }); 63 | } 64 | 65 | public select(f: Flight): void { 66 | this.selectedFlight = f; 67 | } 68 | 69 | public search(): void { 70 | var value = this.filter.value; 71 | 72 | this.flightService.find(value.from, value.to); 73 | 74 | // .map(function(resp) { return resp.json() }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /projects/lib/src/interceptors/default-oauth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, Optional } from '@angular/core'; 2 | import { OAuthService } from '../oauth-service'; 3 | import { OAuthStorage } from '../types'; 4 | import { 5 | HttpEvent, 6 | HttpHandler, 7 | HttpInterceptor, 8 | HttpRequest, 9 | HttpResponse, 10 | HttpErrorResponse 11 | } from '@angular/common/http'; 12 | import { Observable } from 'rxjs'; 13 | import { catchError } from 'rxjs/operators'; 14 | import { OAuthResourceServerErrorHandler } from './resource-server-error-handler'; 15 | import { OAuthModuleConfig } from '../oauth-module.config'; 16 | 17 | @Injectable() 18 | export class DefaultOAuthInterceptor implements HttpInterceptor { 19 | constructor( 20 | private authStorage: OAuthStorage, 21 | private errorHandler: OAuthResourceServerErrorHandler, 22 | @Optional() private moduleConfig: OAuthModuleConfig 23 | ) { } 24 | 25 | private checkUrl(url: string): boolean { 26 | const found = this.moduleConfig.resourceServer.allowedUrls.find(u => url.startsWith(u)); 27 | return !!found; 28 | } 29 | 30 | public intercept( 31 | req: HttpRequest, 32 | next: HttpHandler 33 | ): Observable> { 34 | const url = req.url.toLowerCase(); 35 | 36 | if (!this.moduleConfig) { 37 | return next.handle(req); 38 | } 39 | if (!this.moduleConfig.resourceServer) { 40 | return next.handle(req); 41 | } 42 | if (this.moduleConfig.resourceServer.allowedUrls && !this.checkUrl(url)) { 43 | return next.handle(req); 44 | } 45 | 46 | const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken; 47 | 48 | if (sendAccessToken && this.authStorage.getItem('access_token')) { 49 | const token = this.authStorage.getItem('access_token'); 50 | const header = 'Bearer ' + token; 51 | 52 | const headers = req.headers.set('Authorization', header); 53 | 54 | req = req.clone({ headers }); 55 | } 56 | 57 | return next 58 | .handle(req) 59 | .pipe(catchError(err => this.errorHandler.handleError(err))); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docs/js/libs/promise.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 (c) Pierre Duquesne 3 | * Licensed under the New BSD License. 4 | * https://github.com/stackp/promisejs 5 | */ 6 | (function(a){function b(){this._callbacks=[];}b.prototype.then=function(a,c){var d;if(this._isdone)d=a.apply(c,this.result);else{d=new b();this._callbacks.push(function(){var b=a.apply(c,arguments);if(b&&typeof b.then==='function')b.then(d.done,d);});}return d;};b.prototype.done=function(){this.result=arguments;this._isdone=true;for(var a=0;a=300)&&j.status!==304);h.done(a,j.responseText,j);}};j.send(k);return h;}function h(a){return function(b,c,d){return g(a,b,c,d);};}var i={Promise:b,join:c,chain:d,ajax:g,get:h('GET'),post:h('POST'),put:h('PUT'),del:h('DELETE'),ENOXHR:1,ETIMEOUT:2,ajaxTimeout:0};if(typeof define==='function'&&define.amd)define(function(){return i;});else a.promise=i;})(this); -------------------------------------------------------------------------------- /docs/styles/material.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | background: none; 3 | } 4 | 5 | a:hover { 6 | text-decoration: none; 7 | } 8 | 9 | /** LINK **/ 10 | 11 | .menu ul.list li a { 12 | text-decoration: none; 13 | } 14 | 15 | .menu ul.list li a:hover, .menu ul.list li.chapter .simple:hover { 16 | background-color: #f8f9fa; 17 | text-decoration: none; 18 | } 19 | 20 | #book-search-input { 21 | margin-bottom: 0; 22 | } 23 | 24 | .menu ul.list li.divider { 25 | margin-top: 0; 26 | background: #e9ecef; 27 | } 28 | 29 | .menu .title:hover { 30 | background-color: #f8f9fa; 31 | } 32 | 33 | /** CARD **/ 34 | 35 | .card { 36 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); 37 | border-radius: 0.125rem; 38 | border: 0; 39 | margin-top: 1px; 40 | } 41 | 42 | .card-header { 43 | background: none; 44 | } 45 | 46 | /** BUTTON **/ 47 | 48 | .btn { 49 | border-radius: 0.125rem; 50 | } 51 | 52 | /** NAV BAR **/ 53 | 54 | .nav { 55 | border: 0; 56 | } 57 | .nav-tabs > li > a { 58 | border: 0; 59 | border-bottom: 0.214rem solid transparent; 60 | color: rgba(0, 0, 0, 0.54); 61 | margin-right: 0; 62 | } 63 | .nav-tabs>li.active>a, .nav-tabs>li.active>a:focus, .nav-tabs>li.active>a:hover { 64 | color: rgba(0, 0, 0, 0.87); 65 | border-top: 0; 66 | border-left: 0; 67 | border-right: 0; 68 | border-bottom: 0.214rem solid transparent; 69 | border-color: #008cff; 70 | font-weight: bold; 71 | } 72 | .nav>li>a:focus, .nav>li>a:hover { 73 | background: none; 74 | } 75 | 76 | /** LIST **/ 77 | 78 | .list-group-item:first-child { 79 | border-top-left-radius: 0.125rem; 80 | border-top-right-radius: 0.125rem; 81 | } 82 | .list-group-item:last-child { 83 | border-bottom-left-radius: 0.125rem; 84 | border-bottom-right-radius: 0.125rem; 85 | } 86 | 87 | /** MISC **/ 88 | 89 | .modifier { 90 | border-radius: 0.125rem; 91 | } 92 | 93 | pre[class*="language-"] { 94 | border-radius: 0.125rem; 95 | } 96 | 97 | /** TABLE **/ 98 | 99 | .table-hover>tbody>tr:hover { 100 | background: rgba(0, 0, 0, 0.075); 101 | } 102 | 103 | table.params thead { 104 | background: none; 105 | } 106 | table.params thead td { 107 | color: rgba(0, 0, 0, 0.54); 108 | font-weight: bold; 109 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve --project sample -o", 8 | "build": "ng build --prod --project lib && npm run copy:readme", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e", 12 | "tsc": "tsc", 13 | "docs": "npm run docs:build -- --disableCoverage --disablePrivate --disableInternal --includes docs-src", 14 | "docs:build": "compodoc -p projects/lib/tsconfig.lib.json -n angular-oauth2-oidc -d docs --hideGenerator", 15 | "docs:serve": "npm run docs:build -- -s", 16 | "docs:watch": "npm run docs:build -- -s -w", 17 | "format": "prettier --single-quote --write projects/**/*.ts", 18 | "copy:readme": "cpr README.md dist/lib/README.md --overwrite" 19 | }, 20 | "private": true, 21 | "dependencies": { 22 | "@angular/animations": "6.0.0", 23 | "@angular/common": "6.0.0", 24 | "@angular/compiler": "6.0.0", 25 | "@angular/core": "6.0.0", 26 | "@angular/elements": "6.0.0", 27 | "@angular/forms": "6.0.0", 28 | "@angular/http": "6.0.0", 29 | "@angular/platform-browser": "6.0.0", 30 | "@angular/platform-browser-dynamic": "6.0.0", 31 | "@angular/router": "6.0.0", 32 | "@webcomponents/custom-elements": "^1.1.0", 33 | "angular-oauth2-oidc": "^2.1.8", 34 | "bootstrap": "^3.3.7", 35 | "core-js": "^2.5.1", 36 | "jsrsasign": "^8.0.12", 37 | "rxjs": "6.1.0", 38 | "rxjs-compat": "^6.0.0-rc.0", 39 | "zone.js": "^0.8.26" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "~0.5.0", 43 | "@angular-devkit/build-ng-packagr": "^0.6.1", 44 | "@angular/cli": "^6.0.0", 45 | "@angular/compiler-cli": "6.0.0", 46 | "@angular/language-service": "6.0.0", 47 | "@compodoc/compodoc": "^1.1.2", 48 | "@types/jasmine": "~2.6.3", 49 | "@types/jasminewd2": "~2.0.3", 50 | "@types/node": "~8.0.51", 51 | "codelyzer": "~4.0.1", 52 | "jasmine-core": "~2.8.0", 53 | "jasmine-spec-reporter": "~4.2.1", 54 | "karma": "~1.7.1", 55 | "karma-chrome-launcher": "~2.2.0", 56 | "karma-cli": "~1.0.1", 57 | "karma-coverage-istanbul-reporter": "^1.3.0", 58 | "karma-jasmine": "~1.1.0", 59 | "karma-jasmine-html-reporter": "^0.2.2", 60 | "ng-packagr": "^2.4.4", 61 | "prettier": "1.12.1", 62 | "protractor": "~5.2.0", 63 | "ts-node": "~3.3.0", 64 | "tslint": "~5.8.0", 65 | "typescript": "^2.7.2", 66 | "webpack": "^4.4.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /projects/sample/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | import 'core-js/es6/function'; 25 | import 'core-js/es6/parse-int'; 26 | import 'core-js/es6/parse-float'; 27 | import 'core-js/es6/number'; 28 | import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | import 'core-js/es6/regexp'; 33 | import 'core-js/es6/map'; 34 | import 'core-js/es6/weak-map'; 35 | import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 41 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | import 'core-js/es7/reflect'; 46 | 47 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 48 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 49 | 50 | /*************************************************************************************************** 51 | * Zone JS is required by Angular itself. 52 | */ 53 | import 'zone.js/dist/zone'; // Included with Angular CLI. 54 | 55 | /*************************************************************************************************** 56 | * APPLICATION IMPORTS 57 | */ 58 | 59 | /** 60 | * Date, currency, decimal and percent pipes. 61 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 62 | */ 63 | // import 'intl'; // Run `npm install --save intl`. 64 | /** 65 | * Need to import at least one locale-data with intl. 66 | */ 67 | // import 'intl/locale-data/jsonp/en'; 68 | -------------------------------------------------------------------------------- /docs/js/libs/tablesort.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * tablesort v5.0.2 (2017-11-12) 3 | * http://tristen.ca/tablesort/demo/ 4 | * Copyright (c) 2017 ; Licensed MIT 5 | */!function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a){return a.getAttribute("data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&l.push(k),m++;if(!l)return}for(m=0;m Promise; 8 | } 9 | 10 | /** 11 | * Interface for Handlers that are hooked in to 12 | * validate tokens. 13 | */ 14 | export abstract class ValidationHandler { 15 | /** 16 | * Validates the signature of an id_token. 17 | */ 18 | public abstract validateSignature( 19 | validationParams: ValidationParams 20 | ): Promise; 21 | 22 | /** 23 | * Validates the at_hash in an id_token against the received access_token. 24 | */ 25 | public abstract validateAtHash(validationParams: ValidationParams): boolean; 26 | } 27 | 28 | /** 29 | * This abstract implementation of ValidationHandler already implements 30 | * the method validateAtHash. However, to make use of it, 31 | * you have to override the method calcHash. 32 | */ 33 | export abstract class AbstractValidationHandler implements ValidationHandler { 34 | /** 35 | * Validates the signature of an id_token. 36 | */ 37 | abstract validateSignature(validationParams: ValidationParams): Promise; 38 | 39 | /** 40 | * Validates the at_hash in an id_token against the received access_token. 41 | */ 42 | validateAtHash(params: ValidationParams): boolean { 43 | let hashAlg = this.inferHashAlgorithm(params.idTokenHeader); 44 | 45 | let tokenHash = this.calcHash(params.accessToken, hashAlg); // sha256(accessToken, { asString: true }); 46 | 47 | let leftMostHalf = tokenHash.substr(0, tokenHash.length / 2); 48 | 49 | let tokenHashBase64 = btoa(leftMostHalf); 50 | 51 | let atHash = tokenHashBase64 52 | .replace(/\+/g, '-') 53 | .replace(/\//g, '_') 54 | .replace(/=/g, ''); 55 | let claimsAtHash = params.idTokenClaims['at_hash'].replace(/=/g, ''); 56 | 57 | if (atHash !== claimsAtHash) { 58 | console.error('exptected at_hash: ' + atHash); 59 | console.error('actual at_hash: ' + claimsAtHash); 60 | } 61 | 62 | return atHash === claimsAtHash; 63 | } 64 | 65 | /** 66 | * Infers the name of the hash algorithm to use 67 | * from the alg field of an id_token. 68 | * 69 | * @param jwtHeader the id_token's parsed header 70 | */ 71 | protected inferHashAlgorithm(jwtHeader: object): string { 72 | let alg: string = jwtHeader['alg']; 73 | 74 | if (!alg.match(/^.S[0-9]{3}$/)) { 75 | throw new Error('Algorithm not supported: ' + alg); 76 | } 77 | 78 | return 'sha' + alg.substr(2); 79 | } 80 | 81 | /** 82 | * Calculates the hash for the passed value by using 83 | * the passed hash algorithm. 84 | * 85 | * @param valueToHash 86 | * @param algorithm 87 | */ 88 | protected abstract calcHash(valueToHash: string, algorithm: string): string; 89 | } 90 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-search-reactive/flight-search-reactive.component.html: -------------------------------------------------------------------------------- 1 |

Flight Search (Reactive) !

2 | 3 | 6 | 7 |
8 | 9 |

Dynamisches Formular

10 |
11 | 12 | 13 |
14 | 15 |

Statisches Formular

16 | 17 |
18 | 19 | 20 | 21 |
22 | Validierungsfehler. Bitte prüfen Sie Ihre Eingaben. 23 |
24 | 25 |
26 | Diese Stadt wird nicht angefolgen 27 |
28 | 29 |
30 | Dieses Feld ist ein Pflichtfeld 31 |
32 | 33 |
34 | 35 | 36 |
37 | 38 | 39 |
40 |
41 | 46 |
47 | 48 |
49 | 52 | 64 | 65 |
66 |
67 | 69 | 70 | 71 | 75 | 76 | 80 | 81 | 82 |
83 |
84 | 85 | 89 | 90 |
91 |
Warenkorb
92 | ----------------------
93 | {{selectedFlight | json}}
94 | 
95 |
-------------------------------------------------------------------------------- /projects/sample/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { authConfig } from '../auth.config'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { OAuthService } from 'angular-oauth2-oidc'; 4 | 5 | @Component({ 6 | templateUrl: './home.component.html' 7 | }) 8 | export class HomeComponent implements OnInit { 9 | loginFailed: boolean = false; 10 | userProfile: object; 11 | 12 | constructor(private oauthService: OAuthService) { 13 | // Tweak config for implicit flow. 14 | // This is just needed b/c this demo uses both, 15 | // implicit flow as well as password flow 16 | this.oauthService.configure(authConfig); 17 | this.oauthService.loadDiscoveryDocument(); 18 | } 19 | 20 | ngOnInit() { 21 | /* 22 | this.oauthService.loadDiscoveryDocumentAndTryLogin().then(_ => { 23 | if (!this.oauthService.hasValidIdToken() || !this.oauthService.hasValidAccessToken()) { 24 | this.oauthService.initImplicitFlow('some-state'); 25 | } 26 | }); 27 | */ 28 | } 29 | 30 | login() { 31 | this.oauthService.initImplicitFlow('/some-state;p1=1;p2=2'); 32 | // the parameter here is optional. It's passed around and can be used after logging in 33 | } 34 | 35 | logout() { 36 | this.oauthService.logOut(); 37 | } 38 | 39 | loadUserProfile(): void { 40 | this.oauthService.loadUserProfile().then(up => (this.userProfile = up)); 41 | } 42 | 43 | get givenName() { 44 | var claims = this.oauthService.getIdentityClaims(); 45 | if (!claims) return null; 46 | return claims['given_name']; 47 | } 48 | 49 | get familyName() { 50 | var claims = this.oauthService.getIdentityClaims(); 51 | if (!claims) return null; 52 | return claims['family_name']; 53 | } 54 | 55 | testSilentRefresh() { 56 | /* 57 | * Tweak config for implicit flow. 58 | * This is needed b/c this sample uses both flows 59 | */ 60 | //this.oauthService.clientId = "spa-demo"; 61 | this.oauthService.oidc = true; 62 | 63 | this.oauthService 64 | .silentRefresh() 65 | .then(info => console.debug('refresh ok', info)) 66 | .catch(err => console.error('refresh error', err)); 67 | } 68 | 69 | set requestAccessToken(value: boolean) { 70 | this.oauthService.requestAccessToken = value; 71 | localStorage.setItem('requestAccessToken', '' + value); 72 | } 73 | 74 | get requestAccessToken() { 75 | return this.oauthService.requestAccessToken; 76 | } 77 | 78 | get id_token() { 79 | return this.oauthService.getIdToken(); 80 | } 81 | 82 | get access_token() { 83 | return this.oauthService.getAccessToken(); 84 | } 85 | 86 | get id_token_expiration() { 87 | return this.oauthService.getIdTokenExpiration(); 88 | } 89 | 90 | get access_token_expiration() { 91 | return this.oauthService.getAccessTokenExpiration(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /projects/sample/src/app/auth-no-discovery.config.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from 'angular-oauth2-oidc'; 2 | 3 | export const noDiscoveryAuthConfig: AuthConfig = { 4 | clientId: 5 | '1004270452653-m396kcs7jc3970turlp7ffh6bv4t1b86.apps.googleusercontent.com', 6 | redirectUri: 'http://localhost:4200/index.html', 7 | postLogoutRedirectUri: '', 8 | loginUrl: 'https://accounts.google.com/o/oauth2/v2/auth', 9 | scope: 'openid profile email', 10 | resource: '', 11 | rngUrl: '', 12 | oidc: true, 13 | requestAccessToken: true, 14 | options: null, 15 | issuer: 'https://accounts.google.com', 16 | clearHashAfterLogin: true, 17 | tokenEndpoint: 'https://www.googleapis.com/oauth2/v4/token', 18 | userinfoEndpoint: 'https://www.googleapis.com/oauth2/v3/userinfo', 19 | responseType: 'token', 20 | showDebugInformation: true, 21 | silentRefreshRedirectUri: 'http://localhost:4200/silent-refresh.html', 22 | silentRefreshMessagePrefix: '', 23 | silentRefreshShowIFrame: false, 24 | silentRefreshTimeout: 20000, 25 | dummyClientSecret: null, 26 | requireHttps: 'remoteOnly', 27 | strictDiscoveryDocumentValidation: false, 28 | jwks: { 29 | keys: [ 30 | { 31 | kty: 'RSA', 32 | alg: 'RS256', 33 | use: 'sig', 34 | kid: '7540561fdb04b89d824a1b7b9e8849873e7cb50e', 35 | n: 36 | 'sSFZrLIrXzvXBCehdPR10T-mfHWFU5ZtGzW9buI7wT_tJzZ1SRUc2l1NH92kGV9bmWRtDLjWcWFwMG7rbjX25-R-62lD1k15gQiO4bhx7gbV05e36os2vXTs0ypj9GS9y8X_2fYAnxxulMLwz4m24Ejo2tQI43-V-3Tec6cSXe0FjhRaPbGdS8GHPDKkhpJ1NHMZ38vhddIImOfvtVuz3lt_zwjBsAC6Q7PHs2GOm3KtC22DCwXMYSri4QOQcasuvTlZxIQSIksTyuH0T02IH5SJvQZSx46Vfq8BM4JP-zEEjzadoyxQPouRM6TrUeaqNv5B1f1lbH6G0G_r_ddYWQ', 37 | e: 'AQAB' 38 | }, 39 | { 40 | kty: 'RSA', 41 | alg: 'RS256', 42 | use: 'sig', 43 | kid: '778233e8f6f342ea09e867aad25f543adeebf372', 44 | n: 45 | '8MMxQ9F7R1zJ57QvLX-HqUlTVLLofCzZ3-lxohJr8ivJDGZoCqll7ZTNO0nGMgnPpIO-3BQLkaNGQDCpnID1vNIjClFFl0E3cN5bDX15uxCQeQDsm25fTlphpy5FkdoHCviswtrsl2KKUPeRlKqCqMjlDO27KuxIwzIPdNSqv4tseZmI-biFt2JlO9htgODrVqaawdm27t9HcWfOK_a5czRFDHWck2-ZwjbCOF9CtF1ggYm11aV0TElExXr5fgjAQdZ1yGmJvir127BRUgyIy5cpyf7VRRf2Cv7whSMoVJr4W3OK0H9vkuFLnlBiBNYQmH_eWy5U4jBfZjBqvA7Oww', 46 | e: 'AQAB' 47 | }, 48 | { 49 | kty: 'RSA', 50 | alg: 'RS256', 51 | use: 'sig', 52 | kid: '8ec17994394464d95b0b3d906326f1cdde8aee64', 53 | n: 54 | 'w49KfvzGWVXH4vyUxvP29_QTmJfvLp4RPT1WlI6Wo2aNvn6j9vRSLDrK2CnOvvrrlUKvR-8FTcyNi9pRKXDwDhEJcyVFBJVi4PqDh0KIX_dOGYCulr5FUvU0HXQxlMWSHIsJjfGbMMUwM0p09y8KHL-kipiipzn80EpBmrI4Q3t6XOAZJSmbIPaGZJDjyoWWV0TDdVDBMfkqII6tOOB7Ha189AZjz7FHYXR9CIc0Jm6rFy0tVpdHFEG3ptcNQEDQ5ghyMM4PDM4ZmQ5uk3WgHVqnpdmGEfKekLwmYFWgnI-ux_MabltIxr9TE1qubEmebM64rOusHBF0mSbEwggbyw', 55 | e: 'AQAB' 56 | } 57 | ] 58 | }, 59 | customQueryParams: null, 60 | silentRefreshIFrameName: 'angular-oauth-oidc-silent-refresh-iframe', 61 | timeoutFactor: 0.75, 62 | sessionCheckIntervall: 3000, 63 | sessionCheckIFrameName: 'angular-oauth-oidc-check-session-iframe', 64 | disableAtHashCheck: false, 65 | skipSubjectCheck: false 66 | }; 67 | -------------------------------------------------------------------------------- /projects/lib/src/token-validation/validation-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | /** 4 | * Interface for Handlers that are hooked in to 5 | * validate tokens. 6 | */ 7 | var /** 8 | * Interface for Handlers that are hooked in to 9 | * validate tokens. 10 | */ 11 | ValidationHandler = /** @class */ (function () { 12 | function ValidationHandler() { 13 | } 14 | return ValidationHandler; 15 | }()); 16 | exports.ValidationHandler = ValidationHandler; 17 | /** 18 | * This abstract implementation of ValidationHandler already implements 19 | * the method validateAtHash. However, to make use of it, 20 | * you have to override the method calcHash. 21 | */ 22 | var /** 23 | * This abstract implementation of ValidationHandler already implements 24 | * the method validateAtHash. However, to make use of it, 25 | * you have to override the method calcHash. 26 | */ 27 | AbstractValidationHandler = /** @class */ (function () { 28 | function AbstractValidationHandler() { 29 | } 30 | /** 31 | * Validates the at_hash in an id_token against the received access_token. 32 | */ 33 | /** 34 | * Validates the at_hash in an id_token against the received access_token. 35 | */ 36 | AbstractValidationHandler.prototype.validateAtHash = /** 37 | * Validates the at_hash in an id_token against the received access_token. 38 | */ 39 | function (params) { 40 | var hashAlg = this.inferHashAlgorithm(params.idTokenHeader); 41 | var tokenHash = this.calcHash(params.accessToken, hashAlg); // sha256(accessToken, { asString: true }); 42 | var leftMostHalf = tokenHash.substr(0, tokenHash.length / 2); 43 | var tokenHashBase64 = btoa(leftMostHalf); 44 | var atHash = tokenHashBase64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 45 | var claimsAtHash = params.idTokenClaims['at_hash'].replace(/=/g, ''); 46 | if (atHash !== claimsAtHash) { 47 | console.error('exptected at_hash: ' + atHash); 48 | console.error('actual at_hash: ' + claimsAtHash); 49 | } 50 | return (atHash === claimsAtHash); 51 | }; 52 | /** 53 | * Infers the name of the hash algorithm to use 54 | * from the alg field of an id_token. 55 | * 56 | * @param jwtHeader the id_token's parsed header 57 | */ 58 | /** 59 | * Infers the name of the hash algorithm to use 60 | * from the alg field of an id_token. 61 | * 62 | * @param jwtHeader the id_token's parsed header 63 | */ 64 | AbstractValidationHandler.prototype.inferHashAlgorithm = /** 65 | * Infers the name of the hash algorithm to use 66 | * from the alg field of an id_token. 67 | * 68 | * @param jwtHeader the id_token's parsed header 69 | */ 70 | function (jwtHeader) { 71 | var alg = jwtHeader['alg']; 72 | if (!alg.match(/^.S[0-9]{3}$/)) { 73 | throw new Error('Algorithm not supported: ' + alg); 74 | } 75 | return 'sha' + alg.substr(2); 76 | }; 77 | return AbstractValidationHandler; 78 | }()); 79 | exports.AbstractValidationHandler = AbstractValidationHandler; 80 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-operator", 115 | "check-separator", 116 | "check-type" 117 | ], 118 | "no-output-on-prefix": true, 119 | "use-input-property-decorator": true, 120 | "use-output-property-decorator": true, 121 | "use-host-property-decorator": true, 122 | "no-input-rename": true, 123 | "no-output-rename": true, 124 | "use-life-cycle-interface": true, 125 | "use-pipe-transform-interface": true, 126 | "component-class-suffix": true, 127 | "directive-class-suffix": true 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /docs-src/interceptors.md: -------------------------------------------------------------------------------- 1 | # Interceptors 2 | 3 | Since 3.1 the library uses a default HttpInterceptor that takes care about transmitting the access_token to the resource server and about error handling for security related errors (HTTP status codes 401 and 403) received from the resource server. To put in on, just set ``sendAccessToken`` to ``true`` and set ``allowedUrls`` to an array with prefixes for the respective urls. Use lower case for the prefixes: 4 | 5 | ```TypeScript 6 | OAuthModule.forRoot({ 7 | resourceServer: { 8 | allowedUrls: ['http://www.angular.at/api'], 9 | sendAccessToken: true 10 | } 11 | }) 12 | ``` 13 | 14 | You can provide an error handler for the urls white listed here by provding a service for the token ``OAuthResourceServerErrorHandler``. 15 | 16 | To implement such a service, implement the abstract class ``OAuthResourceServerErrorHandler``. The following example shows the default implemantion that just passes the cought error through: 17 | 18 | ```TypeScript 19 | export class OAuthNoopResourceServerErrorHandler implements OAuthResourceServerErrorHandler { 20 | 21 | handleError(err: HttpResponse): Observable { 22 | return _throw(err); 23 | } 24 | 25 | } 26 | ``` 27 | 28 | ## Custom Interceptors 29 | 30 | Feel free to write custom interceptors but keep in mind that injecting the ``OAuthService`` into them creates a circular dependency which leads to an error. The easiest way to prevent this is to use the OAuthStorage directly which also provides the access_token: 31 | 32 | ```TypeScript 33 | import { Injectable, Inject, Optional } from '@angular/core'; 34 | import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; 35 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse, HttpErrorResponse } from '@angular/common/http'; 36 | import {Observable} from 'rxjs/Observable'; 37 | import { OAuthResourceServerErrorHandler } from "./resource-server-error-handler"; 38 | import { OAuthModuleConfig } from "../oauth-module.config"; 39 | 40 | import 'rxjs/add/operator/catch'; 41 | 42 | @Injectable() 43 | export class DefaultOAuthInterceptor implements HttpInterceptor { 44 | 45 | constructor( 46 | private authStorage: OAuthStorage, 47 | private errorHandler: OAuthResourceServerErrorHandler, 48 | @Optional() private moduleConfig: OAuthModuleConfig 49 | ) { 50 | } 51 | 52 | private checkUrl(url: string): boolean { 53 | let found = this.moduleConfig.resourceServer.allowedUrls.find(u => url.startsWith(u)); 54 | return !!found; 55 | } 56 | 57 | public intercept(req: HttpRequest, next: HttpHandler): Observable> { 58 | 59 | let url = req.url.toLowerCase(); 60 | 61 | if (!this.moduleConfig) return next.handle(req); 62 | if (!this.moduleConfig.resourceServer) return next.handle(req); 63 | if (!this.moduleConfig.resourceServer.allowedUrls) return next.handle(req); 64 | if (!this.checkUrl(url)) return next.handle(req); 65 | 66 | let sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken; 67 | 68 | if (sendAccessToken) { 69 | 70 | let token = this.authStorage.getItem('access_token'); 71 | let header = 'Bearer ' + token; 72 | 73 | let headers = req.headers 74 | .set('Authorization', header); 75 | 76 | req = req.clone({ headers }); 77 | } 78 | 79 | return next.handle(req).catch(err => this.errorHandler.handleError(err)); 80 | 81 | } 82 | 83 | } 84 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-oauth2-oidc-codeflow 2 | [![Build Status](https://travis-ci.org/bechhansen/angular-oauth2-oidc.svg?branch=master)](https://travis-ci.org/bechhansen/angular-oauth2-oidc) 3 | 4 | 5 | angular-oauth2-oidc-codeflow is an OAuth2 and OpenId Connect (OIDC) client for Angular. 6 | The library is a Github fork of [manfredsteyer/angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc). 7 | The code of this library is found at [bechhansen/angular-oauth2-oidc](https://github.com/bechhansen/angular-oauth2-oidc). 8 | 9 | The fork extends the existing library so it do also support: 10 | - [Authorization Code Grant flow](https://tools.ietf.org/html/rfc6749#page-24) 11 | 12 | 13 | ## Installing 14 | 15 | ``` 16 | npm i angular-oauth2-oidc-codeflow --save 17 | ``` 18 | 19 | ## Importing the NgModule 20 | 21 | ```TypeScript 22 | import { OAuthModule } from 'angular-oauth2-oidc-codeflow'; 23 | [...] 24 | 25 | @NgModule({ 26 | imports: [ 27 | [...] 28 | HttpModule, 29 | OAuthModule.forRoot() 30 | ], 31 | declarations: [ 32 | AppComponent, 33 | HomeComponent, 34 | [...] 35 | ], 36 | bootstrap: [ 37 | AppComponent 38 | ] 39 | }) 40 | export class AppModule { 41 | } 42 | 43 | ``` 44 | 45 | ## Configuring for Code Grant Flow 46 | 47 | To configure the library the following sample uses the new configuration API introduced with Version 2.1. 48 | Hence, The original API is still supported. 49 | 50 | ```TypeScript 51 | import { AuthConfig } from 'angular-oauth2-oidc-codeflow'; 52 | 53 | export const authConfig: AuthConfig = { 54 | 55 | // Url of the Identity Provider 56 | issuer: 'https://steyer-identity-server.azurewebsites.net/identity', 57 | 58 | // URL of the SPA to redirect the user to after login 59 | redirectUri: window.location.origin + '/index.html', 60 | 61 | // The SPA's id. The SPA is registerd with this id at the auth-server 62 | clientId: 'spa-demo', 63 | 64 | // set the scope for the permissions the client should request 65 | // The first three are defined by OIDC. The 4th is a usecase-specific one 66 | scope: 'openid profile email voucher', 67 | } 68 | ``` 69 | 70 | Configure the OAuthService with this config object when the application starts up: 71 | 72 | ```TypeScript 73 | import { OAuthService } from 'angular-oauth2-oidc'; 74 | import { JwksValidationHandler } from 'angular-oauth2-oidc'; 75 | import { authConfig } from './auth.config'; 76 | import { Component } from '@angular/core'; 77 | 78 | @Component({ 79 | selector: 'flight-app', 80 | templateUrl: './app.component.html' 81 | }) 82 | export class AppComponent { 83 | 84 | constructor(private oauthService: OAuthService) { 85 | this.configureWithNewConfigApi(); 86 | } 87 | 88 | private configureWithNewConfigApi() { 89 | this.oauthService.configure(authConfig); 90 | this.oauthService.tokenValidationHandler = new JwksValidationHandler(); 91 | 92 | this.oauthService.loadDiscoveryDocumentAndTryLogin().then(_ => { 93 | console.log("Logged in"); 94 | }).catch(err => { 95 | console.log("Unable to login"); 96 | }) 97 | 98 | 99 | // Call this.oauthService.tryLogin() if discovery document is not used. 100 | // All configurations must be set manually. 101 | } 102 | } 103 | ``` 104 | 105 | To initialize the login login flow, make a button call 'initAuthorizationCodeFlow()': 106 | ```TypeScript 107 | this.oauthService.initAuthorizationCodeFlow(); 108 | ``` 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /docs-src/silent-refresh.md: -------------------------------------------------------------------------------- 1 | # Refreshing a Token when using Implicit Flow (Silent Refresh) 2 | 3 | To refresh your tokens when using implicit flow you can use a silent refresh. This is a well-known solution that compensates the fact that implicit flow does not allow for issuing a refresh token. It uses a hidden iframe to get another token from the auth-server. When the user is there still logged in (by using a cookie) it will respond without user interaction and provide new tokens. 4 | 5 | To use this approach, setup a redirect uri for the silent refresh. 6 | 7 | For this, you can set the property silentRefreshRedirectUri in the config object: 8 | 9 | ```TypeScript 10 | // This api will come in the next version 11 | 12 | import { AuthConfig } from 'angular-oauth2-oidc'; 13 | 14 | export const authConfig: AuthConfig = { 15 | 16 | // Url of the Identity Provider 17 | issuer: 'https://steyer-identity-server.azurewebsites.net/identity', 18 | 19 | // URL of the SPA to redirect the user to after login 20 | redirectUri: window.location.origin + '/index.html', 21 | 22 | // URL of the SPA to redirect the user after silent refresh 23 | silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', 24 | 25 | // The SPA's id. The SPA is registerd with this id at the auth-server 26 | clientId: 'spa-demo', 27 | 28 | // set the scope for the permissions the client should request 29 | // The first three are defined by OIDC. The 4th is a usecase-specific one 30 | scope: 'openid profile email voucher', 31 | } 32 | ``` 33 | 34 | As an alternative, you can set the same property directly with the OAuthService: 35 | 36 | ```TypeScript 37 | this.oauthService.silentRefreshRedirectUri = window.location.origin + "/silent-refresh.html"; 38 | ``` 39 | 40 | Please keep in mind that this uri has to be configured at the auth-server too. 41 | 42 | This file is loaded into the hidden iframe after getting new tokens. Its only task is to send the received tokens to the main application: 43 | 44 | ```HTML 45 | 46 | 47 | 50 | 51 | 52 | ``` 53 | 54 | Please make sure that this file is copied to your output directory by your build task. When using the CLI you can define it as an asset for this. For this, you have to add the following line to the file ``.angular-cli.json``: 55 | 56 | ```JSON 57 | "assets": [ 58 | [...], 59 | "silent-refresh.html" 60 | ], 61 | ``` 62 | 63 | To perform a silent refresh, just call the following method: 64 | 65 | ```TypeScript 66 | this 67 | .oauthService 68 | .silentRefresh() 69 | .then(info => console.debug('refresh ok', info)) 70 | .catch(err => console.error('refresh error', err)); 71 | ``` 72 | 73 | When there is an error in the iframe that prevents the communication with the main application, silentRefresh will give you a timeout. To configure the timespan for this, you can set the property ``siletRefreshTimeout`` (msec). The default value is 20.000 (20 seconds). 74 | 75 | ### Automatically refreshing a token when/ before it expires 76 | 77 | To automatically refresh a token when/ some time before it expires, just call the following method after configuring the OAuthService: 78 | 79 | ```TypeScript 80 | this.oauthService.setupAutomaticSilentRefresh(); 81 | ``` 82 | 83 | By default, this event is fired after 75% of the token's life time is over. You can adjust this factor by setting the property ``timeoutFactor`` to a value between 0 and 1. For instance, 0.5 means, that the event is fired after half of the life time is over and 0.33 triggers the event after a third. 84 | -------------------------------------------------------------------------------- /projects/sample/src/app/flight-booking/flight-search/flight-search.component.html: -------------------------------------------------------------------------------- 1 |

Flight Search!

2 | 3 | 6 | 7 |
8 | 9 |
10 | Rund-Flüge sind nicht möglich. 11 |
12 | 13 | 14 |
15 | 16 | 25 | 26 |
27 | Validierungsfehler. Bitte prüfen Sie Ihre Eingaben. 28 |
 29 |             {{ f.controls.from?.errors | json }}
 30 |             
31 |
32 | 33 |
34 | Async-City: Die Stadt wird gerade wegen eines Unwetters nicht angeflogen. 35 |
36 | 37 | 38 | 39 |
40 | 41 | Validierung wird ausgeführt. Bitte etwas warten! 42 | 43 |
44 | 45 | 46 |
47 | Dieses Feld ist ein Pflichtfeld. 48 |
49 | 50 |
51 | Diese Stadt wird nicht angeflogen. 52 |
53 | 54 |
55 | Bitte erfassen Sie min. 3 Zeichen. 56 |
57 |
58 | Bitte nur Buchstaben erfassen. 59 |
60 | 61 |
62 | 63 | 64 |
65 | 66 | 67 |
68 |
69 | 74 |
75 | 76 |
77 | 80 | 92 | 93 |
94 |
95 | 97 | 98 | 99 | 100 | 104 | 111 |
112 |
113 | 114 | 118 | 119 |
120 |
Warenkorb
121 | ----------------------
122 | {{selectedFlight | json}}
123 | 
124 |
-------------------------------------------------------------------------------- /docs/styles/postmark.css: -------------------------------------------------------------------------------- 1 | .navbar-default { 2 | background: #FFDE00; 3 | border: none; 4 | } 5 | .navbar-default .navbar-brand { 6 | color: #333; 7 | font-weight: bold; 8 | } 9 | .menu { 10 | background: #333; 11 | color: #fcfcfc; 12 | } 13 | .menu ul.list li a { 14 | color: #333; 15 | } 16 | 17 | .menu ul.list li.title { 18 | background: #FFDE00; 19 | color: #333; 20 | padding-bottom: 5px; 21 | } 22 | 23 | .menu ul.list li:nth-child(2) { 24 | margin-top: 0; 25 | } 26 | 27 | .menu ul.list li.chapter a, .menu ul.list li.chapter .simple { 28 | color: white; 29 | text-decoration: none; 30 | } 31 | 32 | .menu ul.list li.chapter ul.links a { 33 | color: #949494; 34 | text-transform: none; 35 | padding-left: 35px; 36 | } 37 | .menu ul.list li.chapter ul.links a:hover, .menu ul.list li.chapter ul.links a.active { 38 | color: #FFDE00; 39 | } 40 | 41 | .menu ul.list li.chapter ul.links { 42 | padding-left: 0; 43 | } 44 | 45 | .menu ul.list li.divider { 46 | background: rgba(255, 255, 255, 0.07); 47 | } 48 | 49 | #book-search-input input, #book-search-input input:focus, #book-search-input input:hover { 50 | color: #949494; 51 | } 52 | 53 | .copyright { 54 | color: #b3b3b3; 55 | } 56 | 57 | .content { 58 | background: #fcfcfc; 59 | } 60 | 61 | .content a { 62 | color: #007DCC; 63 | } 64 | .content a:visited { 65 | color: #0165a5; 66 | } 67 | .copyright { 68 | background: #272525; 69 | } 70 | .menu ul.list li:nth-last-child(2) { 71 | background: none; 72 | } 73 | .list-group-item:first-child, .list-group-item:last-child { 74 | border-radius: 0; 75 | } 76 | 77 | .menu ul.list li.title a { 78 | text-decoration: none; 79 | font-weight: bold; 80 | } 81 | .menu ul.list li.title a:hover { 82 | background: rgba(255,255,255,0.1); 83 | } 84 | 85 | .breadcrumb>li+li:before { 86 | content: "»\00a0" 87 | } 88 | 89 | .breadcrumb { 90 | padding-bottom: 15px; 91 | border-bottom: 1px solid #e1e4e5; 92 | } 93 | code { 94 | white-space: nowrap; 95 | max-width: 100%; 96 | background: #F5F5F5; 97 | padding: 2px 5px; 98 | color: #666666; 99 | overflow-x: auto; 100 | border-radius: 0; 101 | } 102 | pre { 103 | white-space: pre; 104 | margin: 0; 105 | padding: 12px 12px; 106 | font-size: 12px; 107 | line-height: 1.5; 108 | display: block; 109 | overflow: auto; 110 | color: #404040; 111 | background: #f3f3f3; 112 | } 113 | pre code.hljs { 114 | border: none; 115 | background: inherit; 116 | } 117 | 118 | /* 119 | Atom One Light by Daniel Gamage 120 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax 121 | base: #fafafa 122 | mono-1: #383a42 123 | mono-2: #686b77 124 | mono-3: #a0a1a7 125 | hue-1: #0184bb 126 | hue-2: #4078f2 127 | hue-3: #a626a4 128 | hue-4: #50a14f 129 | hue-5: #e45649 130 | hue-5-2: #c91243 131 | hue-6: #986801 132 | hue-6-2: #c18401 133 | */ 134 | 135 | .hljs { 136 | display: block; 137 | overflow-x: auto; 138 | padding: 0.5em; 139 | color: #383a42; 140 | background: #fafafa; 141 | } 142 | 143 | .hljs-comment, 144 | .hljs-quote { 145 | color: #a0a1a7; 146 | font-style: italic; 147 | } 148 | 149 | .hljs-doctag, 150 | .hljs-keyword, 151 | .hljs-formula { 152 | color: #a626a4; 153 | } 154 | 155 | .hljs-section, 156 | .hljs-name, 157 | .hljs-selector-tag, 158 | .hljs-deletion, 159 | .hljs-subst { 160 | color: #e45649; 161 | } 162 | 163 | .hljs-literal { 164 | color: #0184bb; 165 | } 166 | 167 | .hljs-string, 168 | .hljs-regexp, 169 | .hljs-addition, 170 | .hljs-attribute, 171 | .hljs-meta-string { 172 | color: #50a14f; 173 | } 174 | 175 | .hljs-built_in, 176 | .hljs-class .hljs-title { 177 | color: #c18401; 178 | } 179 | 180 | .hljs-attr, 181 | .hljs-variable, 182 | .hljs-template-variable, 183 | .hljs-type, 184 | .hljs-selector-class, 185 | .hljs-selector-attr, 186 | .hljs-selector-pseudo, 187 | .hljs-number { 188 | color: #986801; 189 | } 190 | 191 | .hljs-symbol, 192 | .hljs-bullet, 193 | .hljs-link, 194 | .hljs-meta, 195 | .hljs-selector-id, 196 | .hljs-title { 197 | color: #4078f2; 198 | } 199 | 200 | .hljs-emphasis { 201 | font-style: italic; 202 | } 203 | 204 | .hljs-strong { 205 | font-weight: bold; 206 | } 207 | 208 | .hljs-link { 209 | text-decoration: underline; 210 | } 211 | -------------------------------------------------------------------------------- /projects/sample/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { noDiscoveryAuthConfig } from './auth-no-discovery.config'; 2 | import { googleAuthConfig } from './auth.google.config'; 3 | import { authConfig } from './auth.config'; 4 | import { FlightHistoryComponent } from './flight-history/flight-history.component'; 5 | import { Component } from '@angular/core'; 6 | import { OAuthService, AuthConfig, NullValidationHandler } from 'angular-oauth2-oidc'; 7 | // import { JwksValidationHandler } from 'angular-oauth2-oidc'; 8 | import { Router } from '@angular/router'; 9 | import { filter, delay } from 'rxjs/operators'; 10 | import { of, race } from 'rxjs'; 11 | 12 | @Component({ 13 | // tslint:disable-next-line:component-selector 14 | selector: 'flight-app', 15 | templateUrl: './app.component.html' 16 | }) 17 | export class AppComponent { 18 | constructor(private router: Router, private oauthService: OAuthService) { 19 | // this.configureWithoutDiscovery(); 20 | this.configureWithNewConfigApi(); 21 | // this.configureAuth(); 22 | // this.configurePasswordFlow(); 23 | } 24 | 25 | private configureWithoutDiscovery() { 26 | this.oauthService.configure(noDiscoveryAuthConfig); 27 | this.oauthService.tokenValidationHandler = new NullValidationHandler(); 28 | this.oauthService.tryLogin(); 29 | } 30 | 31 | // This api will come in the next version 32 | private configureWithNewConfigApi() { 33 | this.oauthService.configure(authConfig); 34 | this.oauthService.setStorage(localStorage); 35 | this.oauthService.tokenValidationHandler = new NullValidationHandler(); 36 | this.oauthService.loadDiscoveryDocumentAndTryLogin(); 37 | 38 | 39 | // Optional 40 | this.oauthService.setupAutomaticSilentRefresh(); 41 | 42 | this.oauthService.events.subscribe(e => { 43 | // tslint:disable-next-line:no-console 44 | console.debug('oauth/oidc event', e); 45 | }); 46 | 47 | this.oauthService.events 48 | .pipe(filter(e => e.type === 'session_terminated')) 49 | .subscribe(e => { 50 | // tslint:disable-next-line:no-console 51 | console.debug('Your session has been terminated!'); 52 | }); 53 | 54 | this.oauthService.events 55 | .pipe(filter(e => e.type === 'token_received')) 56 | .subscribe(e => { 57 | // this.oauthService.loadUserProfile(); 58 | }); 59 | } 60 | 61 | private configureAuth() { 62 | // 63 | // This method demonstrated the old API; see configureWithNewConfigApi for new one 64 | // 65 | 66 | // URL of the SPA to redirect the user to after login 67 | this.oauthService.redirectUri = window.location.origin + '/index.html'; 68 | 69 | // URL of the SPA to redirect the user after silent refresh 70 | this.oauthService.silentRefreshRedirectUri = 71 | window.location.origin + '/silent-refresh.html'; 72 | 73 | // The SPA's id. The SPA is registerd with this id at the auth-server 74 | this.oauthService.clientId = 'spa-demo'; 75 | 76 | // set the scope for the permissions the client should request 77 | // The first three are defined by OIDC. The 4th is a usecase-specific one 78 | this.oauthService.scope = 'openid profile email voucher'; 79 | 80 | // Url of the Identity Provider 81 | this.oauthService.issuer = 82 | 'https://steyer-identity-server.azurewebsites.net/identity'; 83 | 84 | this.oauthService.tokenValidationHandler = new NullValidationHandler(); 85 | 86 | this.oauthService.events.subscribe(e => { 87 | // tslint:disable-next-line:no-console 88 | console.debug('oauth/oidc event', e); 89 | }); 90 | 91 | // Load Discovery Document and then try to login the user 92 | this.oauthService.loadDiscoveryDocument().then(doc => { 93 | this.oauthService.tryLogin(); 94 | }); 95 | 96 | this.oauthService.events 97 | .pipe(filter(e => e.type === 'token_expires')) 98 | .subscribe(e => { 99 | // tslint:disable-next-line:no-console 100 | console.debug('received token_expires event', e); 101 | this.oauthService.silentRefresh(); 102 | }); 103 | } 104 | 105 | private configurePasswordFlow() { 106 | // Set a dummy secret 107 | // Please note that the auth-server used here demand the client to transmit a client secret, although 108 | // the standard explicitly cites that the password flow can also be used without it. Using a client secret 109 | // does not make sense for a SPA that runs in the browser. That's why the property is called dummyClientSecret 110 | // Using such a dummy secreat is as safe as using no secret. 111 | this.oauthService.dummyClientSecret = 'geheim'; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /projects/lib/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Additional options that can be passt to tryLogin. 3 | */ 4 | export class LoginOptions { 5 | /** 6 | * Is called, after a token has been received and 7 | * successfully validated. 8 | * 9 | * Deprecated: Use property ``events`` on OAuthService instead. 10 | */ 11 | onTokenReceived?: (receivedTokens: ReceivedTokens) => void; 12 | 13 | /** 14 | * Hook, to validate the received tokens. 15 | * Deprecated: Use property ``tokenValidationHandler`` on OAuthService instead. 16 | */ 17 | validationHandler?: (receivedTokens: ReceivedTokens) => Promise; 18 | 19 | /** 20 | * Called when tryLogin detects that the auth server 21 | * included an error message into the hash fragment. 22 | * 23 | * Deprecated: Use property ``events`` on OAuthService instead. 24 | */ 25 | onLoginError?: (params: object) => void; 26 | 27 | /** 28 | * A custom hash fragment to be used instead of the 29 | * actual one. This is used for silent refreshes, to 30 | * pass the iframes hash fragment to this method. 31 | */ 32 | customHashFragment?: string; 33 | 34 | /** 35 | * Set this to true to disable the oauth2 state 36 | * check which is a best practice to avoid 37 | * security attacks. 38 | * As OIDC defines a nonce check that includes 39 | * this, this can be set to true when only doing 40 | * OIDC. 41 | */ 42 | disableOAuth2StateCheck?: boolean; 43 | 44 | /** 45 | * Normally, you want to clear your hash fragment after 46 | * the lib read the token(s) so that they are not displayed 47 | * anymore in the url. If not, set this to true. 48 | */ 49 | preventClearHashAfterLogin? = false; 50 | } 51 | 52 | /** 53 | * Defines a simple storage that can be used for 54 | * storing the tokens at client side. 55 | * Is compatible to localStorage and sessionStorage, 56 | * but you can also create your own implementations. 57 | */ 58 | export abstract class OAuthStorage { 59 | abstract getItem(key: string): string | null; 60 | abstract removeItem(key: string): void; 61 | abstract setItem(key: string, data: string): void; 62 | } 63 | 64 | /** 65 | * Represents the received tokens, the received state 66 | * and the parsed claims from the id-token. 67 | */ 68 | export class ReceivedTokens { 69 | idToken: string; 70 | accessToken: string; 71 | idClaims?: object; 72 | state?: string; 73 | } 74 | 75 | /** 76 | * Represents the parsed and validated id_token. 77 | */ 78 | export interface ParsedIdToken { 79 | idToken: string; 80 | idTokenClaims: object; 81 | idTokenHeader: object; 82 | idTokenClaimsJson: string; 83 | idTokenHeaderJson: string; 84 | idTokenExpiresAt: number; 85 | } 86 | 87 | /** 88 | * Represents the response from the token endpoint 89 | * http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint 90 | */ 91 | export interface TokenResponse { 92 | access_token: string; 93 | id_token: string; 94 | token_type: string; 95 | expires_in: number; 96 | refresh_token: string; 97 | scope: string; 98 | state?: string; 99 | } 100 | 101 | /** 102 | * Represents the response from the user info endpoint 103 | * http://openid.net/specs/openid-connect-core-1_0.html#UserInfo 104 | */ 105 | export interface UserInfo { 106 | sub: string; 107 | [key: string]: any; 108 | } 109 | 110 | /** 111 | * Represents an OpenID Connect discovery document 112 | */ 113 | export interface OidcDiscoveryDoc { 114 | issuer: string; 115 | authorization_endpoint: string; 116 | token_endpoint: string; 117 | token_endpoint_auth_methods_supported: string[]; 118 | token_endpoint_auth_signing_alg_values_supported: string[]; 119 | userinfo_endpoint: string; 120 | check_session_iframe: string; 121 | end_session_endpoint: string; 122 | jwks_uri: string; 123 | registration_endpoint: string; 124 | scopes_supported: string[]; 125 | response_types_supported: string[]; 126 | acr_values_supported: string[]; 127 | response_modes_supported: string[]; 128 | grant_types_supported: string[]; 129 | subject_types_supported: string[]; 130 | userinfo_signing_alg_values_supported: string[]; 131 | userinfo_encryption_alg_values_supported: string[]; 132 | userinfo_encryption_enc_values_supported: string[]; 133 | id_token_signing_alg_values_supported: string[]; 134 | id_token_encryption_alg_values_supported: string[]; 135 | id_token_encryption_enc_values_supported: string[]; 136 | request_object_signing_alg_values_supported: string[]; 137 | display_values_supported: string[]; 138 | claim_types_supported: string[]; 139 | claims_supported: string[]; 140 | claims_parameter_supported: boolean; 141 | service_documentation: string; 142 | ui_locales_supported: string[]; 143 | } 144 | -------------------------------------------------------------------------------- /projects/lib/src/token-validation/jwks-validation-handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbstractValidationHandler, 3 | ValidationParams 4 | } from './validation-handler'; 5 | 6 | // declare var require: any; 7 | // let rs = require('jsrsasign'); 8 | 9 | import * as rs from 'jsrsasign'; 10 | 11 | /** 12 | * Validates the signature of an id_token against one 13 | * of the keys of an JSON Web Key Set (jwks). 14 | * 15 | * This jwks can be provided by the discovery document. 16 | */ 17 | export class JwksValidationHandler extends AbstractValidationHandler { 18 | /** 19 | * Allowed algorithms 20 | */ 21 | allowedAlgorithms: string[] = [ 22 | 'HS256', 23 | 'HS384', 24 | 'HS512', 25 | 'RS256', 26 | 'RS384', 27 | 'RS512', 28 | 'ES256', 29 | 'ES384', 30 | 'PS256', 31 | 'PS384', 32 | 'PS512' 33 | ]; 34 | 35 | /** 36 | * Time period in seconds the timestamp in the signature can 37 | * differ from the current time. 38 | */ 39 | gracePeriodInSec = 600; 40 | 41 | validateSignature(params: ValidationParams, retry = false): Promise { 42 | if (!params.idToken) throw new Error('Parameter idToken expected!'); 43 | if (!params.idTokenHeader) 44 | throw new Error('Parameter idTokenHandler expected.'); 45 | if (!params.jwks) throw new Error('Parameter jwks expected!'); 46 | 47 | if ( 48 | !params.jwks['keys'] || 49 | !Array.isArray(params.jwks['keys']) || 50 | params.jwks['keys'].length === 0 51 | ) { 52 | throw new Error('Array keys in jwks missing!'); 53 | } 54 | 55 | // console.debug('validateSignature: retry', retry); 56 | 57 | let kid: string = params.idTokenHeader['kid']; 58 | let keys: object[] = params.jwks['keys']; 59 | let key: object; 60 | 61 | let alg = params.idTokenHeader['alg']; 62 | 63 | if (kid) { 64 | key = keys.find(k => k['kid'] === kid /* && k['use'] === 'sig' */); 65 | } else { 66 | let kty = this.alg2kty(alg); 67 | let matchingKeys = keys.filter( 68 | k => k['kty'] === kty && k['use'] === 'sig' 69 | ); 70 | 71 | /* 72 | if (matchingKeys.length == 0) { 73 | let error = 'No matching key found.'; 74 | console.error(error); 75 | return Promise.reject(error); 76 | }*/ 77 | if (matchingKeys.length > 1) { 78 | let error = 79 | 'More than one matching key found. Please specify a kid in the id_token header.'; 80 | console.error(error); 81 | return Promise.reject(error); 82 | } else if (matchingKeys.length === 1) { 83 | key = matchingKeys[0]; 84 | } 85 | } 86 | 87 | if (!key && !retry && params.loadKeys) { 88 | return params 89 | .loadKeys() 90 | .then(loadedKeys => (params.jwks = loadedKeys)) 91 | .then(_ => this.validateSignature(params, true)); 92 | } 93 | 94 | if (!key && retry && !kid) { 95 | let error = 'No matching key found.'; 96 | console.error(error); 97 | return Promise.reject(error); 98 | } 99 | 100 | if (!key && retry && kid) { 101 | let error = 102 | 'expected key not found in property jwks. ' + 103 | 'This property is most likely loaded with the ' + 104 | 'discovery document. ' + 105 | 'Expected key id (kid): ' + 106 | kid; 107 | 108 | console.error(error); 109 | return Promise.reject(error); 110 | } 111 | 112 | let keyObj = rs.KEYUTIL.getKey(key); 113 | let validationOptions = { 114 | alg: this.allowedAlgorithms, 115 | gracePeriod: this.gracePeriodInSec 116 | }; 117 | let isValid = rs.KJUR.jws.JWS.verifyJWT( 118 | params.idToken, 119 | keyObj, 120 | validationOptions 121 | ); 122 | 123 | if (isValid) { 124 | return Promise.resolve(); 125 | } else { 126 | return Promise.reject('Signature not valid'); 127 | } 128 | } 129 | 130 | private alg2kty(alg: string) { 131 | switch (alg.charAt(0)) { 132 | case 'R': 133 | return 'RSA'; 134 | case 'E': 135 | return 'EC'; 136 | default: 137 | throw new Error('Cannot infer kty from alg: ' + alg); 138 | } 139 | } 140 | 141 | calcHash(valueToHash: string, algorithm: string): string { 142 | let hashAlg = new rs.KJUR.crypto.MessageDigest({ alg: algorithm }); 143 | let result = hashAlg.digestString(valueToHash); 144 | let byteArrayAsString = this.toByteArrayAsString(result); 145 | return byteArrayAsString; 146 | } 147 | 148 | toByteArrayAsString(hexString: string) { 149 | let result = ''; 150 | for (let i = 0; i < hexString.length; i += 2) { 151 | let hexDigit = hexString.charAt(i) + hexString.charAt(i + 1); 152 | let num = parseInt(hexDigit, 16); 153 | result += String.fromCharCode(num); 154 | } 155 | return result; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /docs/styles/bootstrap-card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | position: relative; 3 | display: block; 4 | margin-bottom: 20px; 5 | background-color: #fff; 6 | border: 1px solid #ddd; 7 | border-radius: 4px; 8 | } 9 | 10 | .card-block { 11 | padding: 15px; 12 | } 13 | .card-block:before, .card-block:after { 14 | content: " "; 15 | display: table; 16 | } 17 | .card-block:after { 18 | clear: both; 19 | } 20 | 21 | .card-title { 22 | margin: 5px; 23 | margin-bottom: 2px; 24 | text-align: center; 25 | } 26 | 27 | .card-subtitle { 28 | margin-top: -10px; 29 | margin-bottom: 0; 30 | } 31 | 32 | .card-text:last-child { 33 | margin-bottom: 0; 34 | margin-top: 10px; 35 | } 36 | 37 | .card-link:hover { 38 | text-decoration: none; 39 | } 40 | .card-link + .card-link { 41 | margin-left: 15px; 42 | } 43 | 44 | .card > .list-group:first-child .list-group-item:first-child { 45 | border-top-right-radius: 4px; 46 | border-top-left-radius: 4px; 47 | } 48 | .card > .list-group:last-child .list-group-item:last-child { 49 | border-bottom-right-radius: 4px; 50 | border-bottom-left-radius: 4px; 51 | } 52 | 53 | .card-header { 54 | padding: 10px 15px; 55 | background-color: #f5f5f5; 56 | border-bottom: 1px solid #ddd; 57 | } 58 | .card-header:before, .card-header:after { 59 | content: " "; 60 | display: table; 61 | } 62 | .card-header:after { 63 | clear: both; 64 | } 65 | .card-header:first-child { 66 | border-radius: 4px 4px 0 0; 67 | } 68 | 69 | .card-footer { 70 | padding: 10px 15px; 71 | background-color: #f5f5f5; 72 | border-top: 1px solid #ddd; 73 | } 74 | .card-footer:before, .card-footer:after { 75 | content: " "; 76 | display: table; 77 | } 78 | .card-footer:after { 79 | clear: both; 80 | } 81 | .card-footer:last-child { 82 | border-radius: 0 0 4px 4px; 83 | } 84 | 85 | .card-header-tabs { 86 | margin-right: -5px; 87 | margin-bottom: -10px; 88 | margin-left: -5px; 89 | border-bottom: 0; 90 | } 91 | 92 | .card-header-pills { 93 | margin-right: -5px; 94 | margin-left: -5px; 95 | } 96 | 97 | .card-primary { 98 | background-color: #337ab7; 99 | border-color: #337ab7; 100 | } 101 | .card-primary .card-header, 102 | .card-primary .card-footer { 103 | background-color: transparent; 104 | } 105 | 106 | .card-success { 107 | background-color: #5cb85c; 108 | border-color: #5cb85c; 109 | } 110 | .card-success .card-header, 111 | .card-success .card-footer { 112 | background-color: transparent; 113 | } 114 | 115 | .card-info { 116 | background-color: #5bc0de; 117 | border-color: #5bc0de; 118 | } 119 | .card-info .card-header, 120 | .card-info .card-footer { 121 | background-color: transparent; 122 | } 123 | 124 | .card-warning { 125 | background-color: #f0ad4e; 126 | border-color: #f0ad4e; 127 | } 128 | .card-warning .card-header, 129 | .card-warning .card-footer { 130 | background-color: transparent; 131 | } 132 | 133 | .card-danger { 134 | background-color: #d9534f; 135 | border-color: #d9534f; 136 | } 137 | .card-danger .card-header, 138 | .card-danger .card-footer { 139 | background-color: transparent; 140 | } 141 | 142 | .card-outline-primary { 143 | background-color: transparent; 144 | border-color: #337ab7; 145 | } 146 | 147 | .card-outline-secondary { 148 | background-color: transparent; 149 | border-color: #ccc; 150 | } 151 | 152 | .card-outline-info { 153 | background-color: transparent; 154 | border-color: #5bc0de; 155 | } 156 | 157 | .card-outline-success { 158 | background-color: transparent; 159 | border-color: #5cb85c; 160 | } 161 | 162 | .card-outline-warning { 163 | background-color: transparent; 164 | border-color: #f0ad4e; 165 | } 166 | 167 | .card-outline-danger { 168 | background-color: transparent; 169 | border-color: #d9534f; 170 | } 171 | 172 | .card-inverse .card-header, 173 | .card-inverse .card-footer { 174 | border-color: rgba(255, 255, 255, 0.2); 175 | } 176 | .card-inverse .card-header, 177 | .card-inverse .card-footer, 178 | .card-inverse .card-title, 179 | .card-inverse .card-blockquote { 180 | color: #fff; 181 | } 182 | .card-inverse .card-link, 183 | .card-inverse .card-text, 184 | .card-inverse .card-subtitle, 185 | .card-inverse .card-blockquote .blockquote-footer { 186 | color: rgba(255, 255, 255, 0.65); 187 | } 188 | .card-inverse .card-link:hover, .card-inverse .card-link:focus { 189 | color: #fff; 190 | } 191 | 192 | .card-blockquote { 193 | padding: 0; 194 | margin-bottom: 0; 195 | border-left: 0; 196 | } 197 | 198 | .card-img { 199 | border-radius: .25em; 200 | } 201 | 202 | .card-img-overlay { 203 | position: absolute; 204 | top: 0; 205 | right: 0; 206 | bottom: 0; 207 | left: 0; 208 | padding: 15px; 209 | } 210 | 211 | .card-img-top { 212 | border-top-right-radius: 4px; 213 | border-top-left-radius: 4px; 214 | } 215 | 216 | .card-img-bottom { 217 | border-bottom-right-radius: 4px; 218 | border-bottom-left-radius: 4px; 219 | } 220 | -------------------------------------------------------------------------------- /docs/js/libs/bootstrap-native.js: -------------------------------------------------------------------------------- 1 | // Native Javascript for Bootstrap 3 v1.1.0 | © dnp_theme | MIT-License 2 | !function(t,e){if("function"==typeof define&&define.amd)define([],e);else if("object"==typeof module&&module.exports)module.exports=e();else{var o=e();t.Affix=o.Affix,t.Alert=o.Alert,t.Button=o.Button,t.Carousel=o.Carousel,t.Collapse=o.Collapse,t.Dropdown=o.Dropdown,t.Modal=o.Modal,t.Popover=o.Popover,t.ScrollSpy=o.ScrollSpy,t.Tab=o.Tab,t.Tooltip=o.Tooltip}}(this,function(){for(var t=function(t,e){t.classList?t.classList.add(e):(t.className+=" "+e,t.offsetWidth)},e=function(t,e){t.classList?t.classList.remove(e):t.className=t.className.replace(e,"").replace(/^\s+|\s+$/g,"")},o=null!=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})").exec(navigator.userAgent)&&parseFloat(RegExp.$1),n=function(t,e){for(var o=e.charAt(0);t&&t!==document;t=t.parentNode){if("."===o&&void 0!==document.querySelector(e))return t;if("#"===o&&t.id===e.substr(1))return t}return!1},i=(document,function(i,a){a=a||{},this.btn="object"==typeof i?i:document.querySelector(i),this.accordion=null,this.collapse=null,this.duration=300,this.options={},this.options.duration=o&&o<10?0:a.duration||this.duration;var r=this,s=function(t){var e=t&&(t.currentStyle||window.getComputedStyle(t)),o=/px/.test(e.borderTopWidth)?Math.round(e.borderTopWidth.replace("px","")):0,n=/px/.test(e.marginTop)?Math.round(e.marginTop.replace("px","")):0,i=/px/.test(e.marginBottom)?Math.round(e.marginBottom.replace("px","")):0,a=/em/.test(e.marginTop)?Math.round(e.marginTop.replace("em","")*parseInt(e.fontSize)):0,r=/em/.test(e.marginBottom)?Math.round(e.marginBottom.replace("em","")*parseInt(e.fontSize)):0;return t.clientHeight+parseInt(o)+parseInt(n)+parseInt(i)+parseInt(a)+parseInt(r)};this.toggle=function(t){t.preventDefault(),/\bin/.test(r.collapse.className)?r.close():r.open()},this.close=function(){this._close(this.collapse),t(this.btn,"collapsed")},this.open=function(){if(this._open(this.collapse),e(this.btn,"collapsed"),null!==this.accordion){var t=this.accordion.querySelectorAll(".collapse.in"),o=t.length,n=0;for(n;n1?t[t.length-1]:void 0:t[0]},this.getActiveContent=function(){var t=this.getActiveTab().getElementsByTagName("A")[0].getAttribute("href").replace("#","");return t&&document.getElementById("c-"+t)},this.tab.addEventListener("click",this.handle,!1)},d=document.querySelectorAll("[data-toggle='tab'], [data-toggle='pill']"),u=0,h=d.length;u'; 59 | newNode.color = '#FB7E81'; 60 | newNode.name = COMPONENTS[i].name; 61 | } 62 | } 63 | for(var i = 0; i < DIRECTIVES.length; i++) { 64 | if (value.attributes) { 65 | for(attr in value.attributes) { 66 | if (DIRECTIVES[i].selector.indexOf(attr) !== -1) { 67 | newNode.font = { 68 | multi: 'html' 69 | }; 70 | newNode.label = '' + newNode.label + ''; 71 | newNode.color = '#FF9800'; 72 | newNode.name = DIRECTIVES[i].name; 73 | } 74 | } 75 | } 76 | } 77 | newNodes.push(newNode); 78 | newEdges.push({ 79 | from: parentNode._parent._id, 80 | to: value._id, 81 | arrows: 'to' 82 | }); 83 | } 84 | } 85 | 86 | newNodes.shift(); 87 | 88 | var container = document.getElementById('tree-container'), 89 | data = { 90 | nodes: newNodes, 91 | edges: newEdges 92 | }, 93 | options = { 94 | layout: { 95 | hierarchical: { 96 | sortMethod: 'directed', 97 | enabled: true 98 | } 99 | }, 100 | nodes: { 101 | shape: 'ellipse', 102 | fixed: true 103 | } 104 | }, 105 | 106 | handleClickNode = function(params) { 107 | var clickeNodeId; 108 | if (params.nodes.length > 0) { 109 | clickeNodeId = params.nodes[0]; 110 | for(var i = 0; i < newNodes.length; i++) { 111 | if (newNodes[i].id === clickeNodeId) { 112 | for(var j = 0; j < COMPONENTS.length; j++) { 113 | if (COMPONENTS[j].name === newNodes[i].name) { 114 | document.location.href = currentLocation.origin + currentLocation.pathname.replace(ACTUAL_COMPONENT.name, newNodes[i].name); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | }, 121 | 122 | tabs = document.getElementsByClassName('nav-tabs')[0], 123 | tabsCollection = tabs.getElementsByTagName('A'), 124 | treeTab; 125 | var i = 0, 126 | len = tabsCollection.length; 127 | for(i; i { 41 | // Do what ever you want here 42 | }); 43 | 44 | } 45 | 46 | } 47 | ``` 48 | 49 | ## Configure Library for Password Flow (without discovery document) 50 | 51 | In cases where you don't have an OIDC based discovery document you have to configure some more properties manually: 52 | 53 | ```TypeScript 54 | @Component({ ... }) 55 | export class AppComponent { 56 | 57 | constructor(private oauthService: OAuthService) { 58 | 59 | // Login-Url 60 | this.oauthService.tokenEndpoint = "https://steyer-identity-server.azurewebsites.net/identity/connect/token"; 61 | 62 | // Url with user info endpoint 63 | // This endpont is described by OIDC and provides data about the loggin user 64 | // This sample uses it, because we don't get an id_token when we use the password flow 65 | // If you don't want this lib to fetch data about the user (e. g. id, name, email) you can skip this line 66 | this.oauthService.userinfoEndpoint = "https://steyer-identity-server.azurewebsites.net/identity/connect/userinfo"; 67 | 68 | // The SPA's id. Register SPA with this id at the auth-server 69 | this.oauthService.clientId = "demo-resource-owner"; 70 | 71 | // set the scope for the permissions the client should request 72 | this.oauthService.scope = "openid profile email voucher offline_access"; 73 | 74 | // Set a dummy secret 75 | // Please note that the auth-server used here demand the client to transmit a client secret, although 76 | // the standard explicitly cites that the password flow can also be used without it. Using a client secret 77 | // does not make sense for a SPA that runs in the browser. That's why the property is called dummyClientSecret 78 | // Using such a dummy secret is as safe as using no secret. 79 | this.oauthService.dummyClientSecret = "geheim"; 80 | 81 | } 82 | 83 | } 84 | ``` 85 | 86 | ## Fetching an Access Token by providing the current user's credentials 87 | 88 | ```TypeScript 89 | this.oauthService.fetchTokenUsingPasswordFlow('max', 'geheim').then((resp) => { 90 | 91 | // Loading data about the user 92 | return this.oauthService.loadUserProfile(); 93 | 94 | }).then(() => { 95 | 96 | // Using the loaded user data 97 | let claims = this.oAuthService.getIdentityClaims(); 98 | if (claims) console.debug('given_name', claims.given_name); 99 | 100 | }) 101 | ``` 102 | 103 | There is also a short form for fetching the token and loading the user profile: 104 | 105 | ```TypeScript 106 | this.oauthService.fetchTokenUsingPasswordFlowAndLoadUserProfile('max', 'geheim').then(() => { 107 | let claims = this.oAuthService.getIdentityClaims(); 108 | if (claims) console.debug('given_name', claims.given_name); 109 | }); 110 | ``` 111 | 112 | ## Refreshing the current Access Token 113 | 114 | Using the password flow you MIGHT get a refresh token (which isn't the case with the implicit flow by design!). You can use this token later to get a new access token, e. g. after it expired. 115 | 116 | ```TypeScript 117 | this.oauthService.refreshToken().then(() => { 118 | console.debug('ok'); 119 | }) 120 | ``` -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "lib": { 7 | "root": "projects/lib", 8 | "sourceRoot": "projects/lib/src", 9 | "projectType": "library", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-ng-packagr:build", 14 | "options": { 15 | "tsConfig": "projects/lib/tsconfig.lib.json", 16 | "project": "projects/lib/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "project": "projects/lib/ng-package.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "main": "projects/lib/src/test.ts", 28 | "tsConfig": "projects/lib/tsconfig.spec.json", 29 | "karmaConfig": "projects/lib/karma.conf.js" 30 | } 31 | }, 32 | "lint": { 33 | "builder": "@angular-devkit/build-angular:tslint", 34 | "options": { 35 | "tsConfig": [ 36 | "projects/lib/tsconfig.lib.json", 37 | "projects/lib/tsconfig.spec.json" 38 | ], 39 | "exclude": [ 40 | "**/node_modules/**" 41 | ] 42 | } 43 | } 44 | } 45 | }, 46 | "sample": { 47 | "root": "projects/sample/", 48 | "sourceRoot": "projects/sample/src", 49 | "projectType": "application", 50 | "prefix": "app", 51 | "schematics": {}, 52 | "architect": { 53 | "build": { 54 | "builder": "@angular-devkit/build-angular:browser", 55 | "options": { 56 | "outputPath": "dist/sample", 57 | "index": "projects/sample/src/index.html", 58 | "main": "projects/sample/src/main.ts", 59 | "polyfills": "projects/sample/src/polyfills.ts", 60 | "tsConfig": "projects/sample/tsconfig.app.json", 61 | "assets": [ 62 | "projects/sample/src/favicon.ico", 63 | "projects/sample/src/assets", 64 | "projects/sample/src/silent-refresh.html" 65 | ], 66 | "styles": [ 67 | "projects/sample/src/styles.css", 68 | "node_modules/bootstrap/dist/css/bootstrap.css" 69 | ], 70 | "scripts": [] 71 | }, 72 | "configurations": { 73 | "production": { 74 | "fileReplacements": [ 75 | { 76 | "replace": "projects/sample/src/environments/environment.ts", 77 | "with": "projects/sample/src/environments/environment.prod.ts" 78 | } 79 | ], 80 | "optimization": true, 81 | "outputHashing": "all", 82 | "sourceMap": false, 83 | "extractCss": true, 84 | "namedChunks": false, 85 | "aot": true, 86 | "extractLicenses": true, 87 | "vendorChunk": false, 88 | "buildOptimizer": true 89 | } 90 | } 91 | }, 92 | "serve": { 93 | "builder": "@angular-devkit/build-angular:dev-server", 94 | "options": { 95 | "browserTarget": "sample:build" 96 | }, 97 | "configurations": { 98 | "production": { 99 | "browserTarget": "sample:build:production" 100 | } 101 | } 102 | }, 103 | "extract-i18n": { 104 | "builder": "@angular-devkit/build-angular:extract-i18n", 105 | "options": { 106 | "browserTarget": "sample:build" 107 | } 108 | }, 109 | "test": { 110 | "builder": "@angular-devkit/build-angular:karma", 111 | "options": { 112 | "main": "projects/sample/src/test.ts", 113 | "polyfills": "projects/sample/src/polyfills.ts", 114 | "tsConfig": "projects/sample/tsconfig.spec.json", 115 | "karmaConfig": "projects/sample/karma.conf.js", 116 | "styles": [ 117 | "projects/sample/styles.css" 118 | ], 119 | "scripts": [], 120 | "assets": [ 121 | "projects/sample/src/favicon.ico", 122 | "projects/sample/src/assets" 123 | ] 124 | } 125 | }, 126 | "lint": { 127 | "builder": "@angular-devkit/build-angular:tslint", 128 | "options": { 129 | "tsConfig": [ 130 | "projects/sample/tsconfig.app.json", 131 | "projects/sample/tsconfig.spec.json" 132 | ], 133 | "exclude": [ 134 | "**/node_modules/**" 135 | ] 136 | } 137 | } 138 | } 139 | }, 140 | "sample-e2e": { 141 | "root": "projects/sample-e2e/", 142 | "projectType": "application", 143 | "architect": { 144 | "e2e": { 145 | "builder": "@angular-devkit/build-angular:protractor", 146 | "options": { 147 | "protractorConfig": "projects/sample-e2e/protractor.conf.js", 148 | "devServerTarget": "sample:serve" 149 | } 150 | }, 151 | "lint": { 152 | "builder": "@angular-devkit/build-angular:tslint", 153 | "options": { 154 | "tsConfig": "projects/sample-e2e/tsconfig.e2e.json", 155 | "exclude": [ 156 | "**/node_modules/**" 157 | ] 158 | } 159 | } 160 | } 161 | } 162 | }, 163 | "schematics": { 164 | "@schematics/angular:component": { 165 | "styleext": "css" 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /docs/styles/prism.css: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=markup+css+clike+javascript+json+markdown+typescript&plugins=line-highlight+line-numbers+toolbar+copy-to-clipboard */ 2 | /** 3 | * okaidia theme for JavaScript, CSS and HTML 4 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/ 5 | * @author ocodia 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: #f8f8f2; 11 | background: none; 12 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | word-wrap: normal; 19 | line-height: 1.5; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | /* Code blocks */ 32 | pre[class*="language-"] { 33 | padding: 1em; 34 | margin: .5em 0; 35 | overflow: auto; 36 | border-radius: 0.3em; 37 | } 38 | 39 | :not(pre) > code[class*="language-"], 40 | pre[class*="language-"] { 41 | background: #272822; 42 | } 43 | 44 | /* Inline code */ 45 | :not(pre) > code[class*="language-"] { 46 | padding: .1em; 47 | border-radius: .3em; 48 | white-space: normal; 49 | } 50 | 51 | .token.comment, 52 | .token.prolog, 53 | .token.doctype, 54 | .token.cdata { 55 | color: slategray; 56 | } 57 | 58 | .token.punctuation { 59 | color: #f8f8f2; 60 | } 61 | 62 | .namespace { 63 | opacity: .7; 64 | } 65 | 66 | .token.property, 67 | .token.tag, 68 | .token.constant, 69 | .token.symbol, 70 | .token.deleted { 71 | color: #f92672; 72 | } 73 | 74 | .token.boolean, 75 | .token.number { 76 | color: #ae81ff; 77 | } 78 | 79 | .token.selector, 80 | .token.attr-name, 81 | .token.string, 82 | .token.char, 83 | .token.builtin, 84 | .token.inserted { 85 | color: #a6e22e; 86 | } 87 | 88 | .token.operator, 89 | .token.entity, 90 | .token.url, 91 | .language-css .token.string, 92 | .style .token.string, 93 | .token.variable { 94 | color: #f8f8f2; 95 | } 96 | 97 | .token.atrule, 98 | .token.attr-value, 99 | .token.function { 100 | color: #e6db74; 101 | } 102 | 103 | .token.keyword { 104 | color: #66d9ef; 105 | } 106 | 107 | .token.regex, 108 | .token.important { 109 | color: #fd971f; 110 | } 111 | 112 | .token.important, 113 | .token.bold { 114 | font-weight: bold; 115 | } 116 | .token.italic { 117 | font-style: italic; 118 | } 119 | 120 | .token.entity { 121 | cursor: help; 122 | } 123 | 124 | pre[data-line] { 125 | position: relative; 126 | padding: 1em 0 1em 3em; 127 | } 128 | 129 | .line-highlight { 130 | position: absolute; 131 | left: 0; 132 | right: 0; 133 | padding: inherit 0; 134 | margin-top: 1em; /* Same as .prism’s padding-top */ 135 | 136 | background: hsla(24, 20%, 50%,.08); 137 | background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); 138 | 139 | pointer-events: none; 140 | 141 | line-height: inherit; 142 | white-space: pre; 143 | } 144 | 145 | .line-highlight:before, 146 | .line-highlight[data-end]:after { 147 | content: attr(data-start); 148 | position: absolute; 149 | top: .4em; 150 | left: .6em; 151 | min-width: 1em; 152 | padding: 0 .5em; 153 | background-color: hsla(24, 20%, 50%,.4); 154 | color: hsl(24, 20%, 95%); 155 | font: bold 65%/1.5 sans-serif; 156 | text-align: center; 157 | vertical-align: .3em; 158 | border-radius: 999px; 159 | text-shadow: none; 160 | box-shadow: 0 1px white; 161 | } 162 | 163 | .line-highlight[data-end]:after { 164 | content: attr(data-end); 165 | top: auto; 166 | bottom: .4em; 167 | } 168 | 169 | pre.line-numbers { 170 | position: relative; 171 | padding-left: 3.8em; 172 | counter-reset: linenumber; 173 | } 174 | 175 | pre.line-numbers > code { 176 | position: relative; 177 | white-space: inherit; 178 | } 179 | 180 | .line-numbers .line-numbers-rows { 181 | position: absolute; 182 | pointer-events: none; 183 | top: 0; 184 | font-size: 100%; 185 | left: -3.8em; 186 | width: 3em; /* works for line-numbers below 1000 lines */ 187 | letter-spacing: -1px; 188 | border-right: 1px solid #999; 189 | 190 | -webkit-user-select: none; 191 | -moz-user-select: none; 192 | -ms-user-select: none; 193 | user-select: none; 194 | 195 | } 196 | 197 | .line-numbers-rows > span { 198 | pointer-events: none; 199 | display: block; 200 | counter-increment: linenumber; 201 | } 202 | 203 | .line-numbers-rows > span:before { 204 | content: counter(linenumber); 205 | color: #999; 206 | display: block; 207 | padding-right: 0.8em; 208 | text-align: right; 209 | } 210 | pre.code-toolbar { 211 | position: relative; 212 | } 213 | 214 | pre.code-toolbar > .toolbar { 215 | position: absolute; 216 | top: .3em; 217 | right: .2em; 218 | transition: opacity 0.3s ease-in-out; 219 | opacity: 0; 220 | } 221 | 222 | pre.code-toolbar:hover > .toolbar { 223 | opacity: 1; 224 | } 225 | 226 | pre.code-toolbar > .toolbar .toolbar-item { 227 | display: inline-block; 228 | } 229 | 230 | pre.code-toolbar > .toolbar a { 231 | cursor: pointer; 232 | } 233 | 234 | pre.code-toolbar > .toolbar button { 235 | background: none; 236 | border: 0; 237 | color: inherit; 238 | font: inherit; 239 | line-height: normal; 240 | overflow: visible; 241 | padding: 0; 242 | -webkit-user-select: none; /* for button */ 243 | -moz-user-select: none; 244 | -ms-user-select: none; 245 | } 246 | 247 | pre.code-toolbar > .toolbar a, 248 | pre.code-toolbar > .toolbar button, 249 | pre.code-toolbar > .toolbar span { 250 | color: #bbb; 251 | font-size: .8em; 252 | padding: 0 .5em; 253 | background: #f5f2f0; 254 | background: rgba(224, 224, 224, 0.2); 255 | box-shadow: 0 2px 0 0 rgba(0,0,0,0.2); 256 | border-radius: .5em; 257 | } 258 | 259 | pre.code-toolbar > .toolbar a:hover, 260 | pre.code-toolbar > .toolbar a:focus, 261 | pre.code-toolbar > .toolbar button:hover, 262 | pre.code-toolbar > .toolbar button:focus, 263 | pre.code-toolbar > .toolbar span:hover, 264 | pre.code-toolbar > .toolbar span:focus { 265 | color: inherit; 266 | text-decoration: none; 267 | } 268 | 269 | -------------------------------------------------------------------------------- /projects/lib/src/auth.config.ts: -------------------------------------------------------------------------------- 1 | export class AuthConfig { 2 | /** 3 | * The client's id as registered with the auth server 4 | */ 5 | public clientId? = ''; 6 | 7 | /** 8 | * The client's redirectUri as registered with the auth server 9 | */ 10 | public redirectUri? = ''; 11 | 12 | /** 13 | * An optional second redirectUri where the auth server 14 | * redirects the user to after logging out. 15 | */ 16 | public postLogoutRedirectUri? = ''; 17 | 18 | /** 19 | * The auth server's endpoint that allows to log 20 | * the user in when using implicit flow. 21 | */ 22 | public loginUrl? = ''; 23 | 24 | /** 25 | * The requested scopes 26 | */ 27 | public scope? = 'openid profile'; 28 | 29 | public resource? = ''; 30 | 31 | public rngUrl? = ''; 32 | 33 | /** 34 | * Defines whether to use OpenId Connect during 35 | * implicit flow. 36 | */ 37 | public oidc? = true; 38 | 39 | /** 40 | * Defines whether to request a access token during 41 | * implicit flow. 42 | */ 43 | public requestAccessToken? = true; 44 | 45 | public options?: any = null; 46 | 47 | /** 48 | * The issuer's uri. 49 | */ 50 | public issuer? = ''; 51 | 52 | /** 53 | * The logout url. 54 | */ 55 | public logoutUrl? = ''; 56 | 57 | /** 58 | * Defines whether to clear the hash fragment after logging in. 59 | */ 60 | public clearHashAfterLogin? = true; 61 | 62 | /** 63 | * Url of the token endpoint as defined by OpenId Connect and OAuth 2. 64 | */ 65 | public tokenEndpoint?: string = null; 66 | 67 | /** 68 | * Url of the userinfo endpoint as defined by OpenId Connect. 69 | * 70 | */ 71 | public userinfoEndpoint?: string = null; 72 | 73 | public responseType? = 'token'; 74 | 75 | /** 76 | * Defines whether additional debug information should 77 | * be shown at the console. 78 | */ 79 | public showDebugInformation? = false; 80 | 81 | /** 82 | * The redirect uri used when doing silent refresh. 83 | */ 84 | public silentRefreshRedirectUri? = ''; 85 | 86 | public silentRefreshMessagePrefix? = ''; 87 | 88 | /** 89 | * Set this to true to display the iframe used for 90 | * silent refresh for debugging. 91 | */ 92 | public silentRefreshShowIFrame? = false; 93 | 94 | /** 95 | * Timeout for silent refresh. 96 | * @internal 97 | * depreacted b/c of typo, see silentRefreshTimeout 98 | */ 99 | public siletRefreshTimeout?: number = 1000 * 20; 100 | 101 | /** 102 | * Timeout for silent refresh. 103 | */ 104 | public silentRefreshTimeout?: number = 1000 * 20; 105 | 106 | /** 107 | * Some auth servers don't allow using password flow 108 | * w/o a client secreat while the standards do not 109 | * demand for it. In this case, you can set a password 110 | * here. As this passwort is exposed to the public 111 | * it does not bring additional security and is therefore 112 | * as good as using no password. 113 | */ 114 | public dummyClientSecret?: string = null; 115 | 116 | /** 117 | * Defines whether https is required. 118 | * The default value is remoteOnly which only allows 119 | * http for localhost, while every other domains need 120 | * to be used with https. 121 | */ 122 | public requireHttps?: boolean | 'remoteOnly' = 'remoteOnly'; 123 | 124 | /** 125 | * Defines whether every url provided by the discovery 126 | * document has to start with the issuer's url. 127 | */ 128 | public strictDiscoveryDocumentValidation? = true; 129 | 130 | /** 131 | * JSON Web Key Set (https://tools.ietf.org/html/rfc7517) 132 | * with keys used to validate received id_tokens. 133 | * This is taken out of the disovery document. Can be set manually too. 134 | */ 135 | public jwks?: object = null; 136 | 137 | /** 138 | * Map with additional query parameter that are appended to 139 | * the request when initializing implicit flow. 140 | */ 141 | public customQueryParams?: object = null; 142 | 143 | public silentRefreshIFrameName? = 'angular-oauth-oidc-silent-refresh-iframe'; 144 | 145 | /** 146 | * Defines when the token_timeout event should be raised. 147 | * If you set this to the default value 0.75, the event 148 | * is triggered after 75% of the token's life time. 149 | */ 150 | public timeoutFactor? = 0.75; 151 | 152 | /** 153 | * If true, the lib will try to check whether the user 154 | * is still logged in on a regular basis as described 155 | * in http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification 156 | */ 157 | public sessionChecksEnabled? = false; 158 | 159 | /** 160 | * Intervall in msec for checking the session 161 | * according to http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification 162 | */ 163 | public sessionCheckIntervall? = 3 * 1000; 164 | 165 | /** 166 | * Url for the iframe used for session checks 167 | */ 168 | public sessionCheckIFrameUrl?: string = null; 169 | 170 | /** 171 | * Name of the iframe to use for session checks 172 | */ 173 | public sessionCheckIFrameName? = 'angular-oauth-oidc-check-session-iframe'; 174 | 175 | /** 176 | * This property has been introduced to disable at_hash checks 177 | * and is indented for Identity Provider that does not deliver 178 | * an at_hash EVEN THOUGH its recommended by the OIDC specs. 179 | * Of course, when disabling these checks the we are bypassing 180 | * a security check which means we are more vulnerable. 181 | */ 182 | public disableAtHashCheck? = false; 183 | 184 | /* 185 | * Defines wether to check the subject of a refreshed token after silent refresh. 186 | * Normally, it should be the same as before. 187 | */ 188 | public skipSubjectCheck? = false; 189 | 190 | public useIdTokenHintForSilentRefresh? = false; 191 | 192 | /* 193 | * Defined whether to skip the validation of the issuer in the discovery document. 194 | * Normally, the discovey document's url starts with the url of the issuer. 195 | */ 196 | public skipIssuerCheck? = false; 197 | 198 | /** 199 | * According to rfc6749 it is recommended (but not required) that the auth 200 | * server exposes the access_token's life time in seconds. 201 | * This is a fallback value for the case this value is not exposed. 202 | */ 203 | public fallbackAccessTokenExpirationTimeInSec?: number; 204 | 205 | /* 206 | * final state sent to issuer is built as follows: 207 | * state = nonce + nonceStateSeparator + additional state 208 | * Default separator is ';' (encoded %3B). 209 | * In rare cases, this character might be forbidden or inconvenient to use by the issuer so it can be customized. 210 | */ 211 | public nonceStateSeparator? = ';'; 212 | 213 | /* 214 | * set this to true to use HTTP BASIC auth for password flow 215 | */ 216 | public useHttpBasicAuthForPasswordFlow? = false; 217 | 218 | public disableNonceCheck? = false; 219 | 220 | constructor(json?: Partial) { 221 | if (json) { 222 | Object.assign(this, json); 223 | } 224 | } 225 | 226 | /** 227 | * This property allows you to override the method that is used to open the login url, 228 | * allowing a way for implementations to specify their own method of routing to new 229 | * urls. 230 | */ 231 | public openUri?: ((uri: string) => void) = uri => { 232 | location.href = uri; 233 | } 234 | } 235 | --------------------------------------------------------------------------------