├── src ├── keycloak │ ├── files │ │ ├── client-import.json │ │ └── __sourceDir__ │ │ │ ├── __appRoot__ │ │ │ └── keycloak-service │ │ │ │ ├── keycloak.guard.ts │ │ │ │ ├── keycloak.interceptor.ts │ │ │ │ ├── keycloak.http.ts │ │ │ │ ├── keycloak.service.ts │ │ │ │ ├── keycloak.d.ts │ │ │ │ └── keycloak.js │ │ │ └── main.ts │ ├── schema.d.ts │ ├── schema.json │ ├── index.ts │ └── index_spec.ts └── collection.json ├── .npmignore ├── tsconfig.json ├── package.json ├── README.md ├── .gitignore └── LICENSE /src/keycloak/files/client-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientId": "<%= clientId %>", 3 | "name" : "<%= clientId %>", 4 | "description" : "Created with keycloak-schematic", 5 | "rootUrl": "http://localhost:4200", 6 | "baseUrl": "http://localhost:4200", 7 | "redirectUris": [ 8 | "/*" 9 | ], 10 | "webOrigins": [ 11 | "http://localhost:4200" 12 | ] 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/keycloak/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | /** 3 | * The path to create keycloak-service (usually 'app'). 4 | */ 5 | appRoot: string; 6 | /** 7 | * The path of the source directory (usually 'src'). 8 | */ 9 | sourceDir: string; 10 | /** 11 | * The the url to the Keycloak auth server. 12 | */ 13 | url: string; 14 | /** 15 | * The Keycloak realm for this app. 16 | */ 17 | realm: string; 18 | /** 19 | * The Keycloak client id for this app. 20 | */ 21 | clientId: string; 22 | } 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/keycloak/files/__sourceDir__/__appRoot__/keycloak-service/keycloak.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router'; 3 | import {KeycloakService} from './keycloak.service'; 4 | 5 | @Injectable() 6 | export class KeycloakGuard implements CanActivate { 7 | constructor(private keycloakService: KeycloakService) {} 8 | 9 | canActivate(route: ActivatedRouteSnapshot, 10 | state: RouterStateSnapshot): boolean { 11 | if (this.keycloakService.authenticated()) { 12 | return true; 13 | } 14 | 15 | this.keycloakService.login({redirectUri: state.url}); 16 | return false; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | ################### 3 | .idea 4 | *.iml 5 | 6 | # Eclipse # 7 | ########### 8 | .project 9 | .settings 10 | .classpath 11 | 12 | # NetBeans # 13 | ############ 14 | nbactions.xml 15 | nb-configuration.xml 16 | catalog.xml 17 | nbproject 18 | 19 | # other IDEs 20 | jsconfig.json 21 | .vscode/ 22 | 23 | # Misc 24 | npm-debug.log* 25 | yarn-error.log* 26 | 27 | # Mac OSX Finder files. 28 | **/.DS_Store 29 | .DS_Store 30 | /nbproject/private/ 31 | 32 | # Packages # 33 | ############ 34 | # it's better to unpack these files and commit the raw source 35 | # git has its own built in compression methods 36 | *.7z 37 | *.dmg 38 | *.gz 39 | *.iso 40 | *.jar 41 | *.rar 42 | *.tar 43 | *.zip 44 | *.tgz 45 | 46 | # Logs and databases # 47 | ###################### 48 | *.log -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "tsconfig", 4 | "lib": [ 5 | "es2017", 6 | "dom" 7 | ], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noUnusedParameters": true, 15 | "noUnusedLocals": true, 16 | "rootDir": "src/", 17 | "skipDefaultLibCheck": true, 18 | "skipLibCheck": true, 19 | "sourceMap": true, 20 | "strictNullChecks": true, 21 | "target": "es6", 22 | "types": [ 23 | "jasmine", 24 | "node" 25 | ] 26 | }, 27 | "include": [ 28 | "src/**/*" 29 | ], 30 | "exclude": [ 31 | "src/*/files/**/*" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ssilvert/keycloak-schematic", 3 | "version": "0.1.3", 4 | "description": "A schematic that installs Keycloak Client in Angular CLI Applications", 5 | "scripts": { 6 | "build": "tsc -p tsconfig.json", 7 | "test": "npm run build && jasmine **/*_spec.js" 8 | }, 9 | "keywords": [ 10 | "schematics", 11 | "keycloak", 12 | "angular" 13 | ], 14 | "author": "Stan Silvert", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/ssilvert/keycloak-schematic.git" 18 | }, 19 | "license": "Apache-2.0", 20 | "licenses": [ 21 | { 22 | "type": "Apache-2.0", 23 | "url": "http://www.apache.org/licenses/LICENSE-2.0" 24 | } 25 | ], 26 | "schematics": "./src/collection.json", 27 | "dependencies": { 28 | "@angular-devkit/core": "^0.0.22", 29 | "@angular-devkit/schematics": "^0.0.42", 30 | "@types/node": "^8.0.31" 31 | }, 32 | "devDependencies": { 33 | "@schematics/angular": "^0.0.42", 34 | "@types/jasmine": "^2.6.0", 35 | "jasmine": "^2.8.0", 36 | "typescript": "^2.5.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/keycloak/files/__sourceDir__/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from './<%= appRoot %>/app.module'; 5 | import {environment} from './environments/environment'; 6 | 7 | import {KeycloakService} from './<%= appRoot %>/keycloak-service/keycloak.service'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | const configOptions = { 14 | url: '<%= url %>', 15 | realm: '<%= realm %>', 16 | clientId: '<%= clientId %>' 17 | }; 18 | 19 | // You can also use a keycloak.json file generated from the Keycloak admin console. 20 | // Just download the file and copy to your /assets directory. Then uncomment 21 | // below and use the url instead of the configOptions above. 22 | //const configOptions:string = 'http://localhost:4200/assets/keycloak.json'; 23 | 24 | 25 | KeycloakService.init(configOptions, {onLoad: 'login-required'}) 26 | .then(() => { 27 | 28 | platformBrowserDynamic().bootstrapModule(AppModule) 29 | .catch(err => console.log(err)); 30 | 31 | }) 32 | .catch((e: any) => { 33 | console.log('Error in bootstrap: ' + JSON.stringify(e)); 34 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## keycloak-schematic 2 | ### Schematic to add [Keycloak](http://www.keycloak.org) support to Angular CLI applications. 3 | With one simple command, add authentication to your Angular CLI application. 4 | Then you have access to a full security suite that provides everything you need 5 | from simple username/password management to full-scale enterprise security. 6 | 7 | ## Prerequisites 8 | 9 | * Angular CLI 1.4 or higher 10 | * Node 6.9.0 or higher 11 | * NPM 3 or higher 12 | 13 | ## Table of Contents 14 | 15 | * [Installation](#installation) 16 | * [Usage](#usage) 17 | * [Documentation](#documentation) 18 | * [License](#license) 19 | 20 | ## Installation 21 | 22 | **BEFORE YOU INSTALL:** please read the [prerequisites](#prerequisites) 23 | 24 | ```bash 25 | npm install -g @ssilvert/keycloak-schematic 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```bash 31 | ng generate keycloak --collection @ssilvert/keycloak-schematic --clientId=myClientName 32 | ``` 33 | `clientId` is required. For other options, see [documentation](https://github.com/ssilvert/keycloak-schematic/wiki). 34 | 35 | ## Documentation 36 | 37 | The documentation for the keycloak-schematic is located in this repo's [wiki](https://github.com/ssilvert/keycloak-schematic/wiki). 38 | 39 | ## License 40 | 41 | Apache 2.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Outputs 2 | src/**/*.js 3 | src/**/*.js.map 4 | src/**/*.d.ts 5 | 6 | # For now, need to include these files explicitly 7 | !src/keycloak/files/__sourceDir__/__appRoot__/keycloak-service/keycloak.d.ts 8 | !src/keycloak/files/__sourceDir__/__approot__/keycloak-service/keycloak.js 9 | !src/keycloak/schema.d.ts 10 | 11 | # Intellij 12 | ################### 13 | .idea 14 | *.iml 15 | 16 | # Eclipse # 17 | ########### 18 | .project 19 | .settings 20 | .classpath 21 | 22 | # NetBeans # 23 | ############ 24 | nbactions.xml 25 | nb-configuration.xml 26 | catalog.xml 27 | nbproject 28 | 29 | # other IDEs 30 | jsconfig.json 31 | .vscode/ 32 | 33 | # Misc 34 | node_modules/ 35 | npm-debug.log* 36 | yarn-error.log* 37 | 38 | # Mac OSX Finder files. 39 | **/.DS_Store 40 | .DS_Store 41 | /nbproject/private/ 42 | 43 | # Compiled source # 44 | ################### 45 | *.com 46 | *.class 47 | *.dll 48 | *.exe 49 | *.o 50 | *.so 51 | 52 | # Packages # 53 | ############ 54 | # it's better to unpack these files and commit the raw source 55 | # git has its own built in compression methods 56 | *.7z 57 | *.dmg 58 | *.gz 59 | *.iso 60 | *.jar 61 | *.rar 62 | *.tar 63 | *.zip 64 | *.tgz 65 | 66 | # Logs and databases # 67 | ###################### 68 | *.log 69 | 70 | # Maven # 71 | ######### 72 | target 73 | 74 | # Maven shade 75 | ############# 76 | *dependency-reduced-pom.xml -------------------------------------------------------------------------------- /src/collection.json: -------------------------------------------------------------------------------- 1 | // By default, collection.json is a Loose-format JSON5 format, which means it's loaded using a 2 | // special loader and you can use comments, as well as single quotes or no-quotes for standard 3 | // JavaScript identifiers. 4 | // Note that this is only true for collection.json and it depends on the tooling itself. 5 | // We read package.json using a require() call, which is standard JSON. 6 | { 7 | // This is just to indicate to your IDE that there is a schema for collection.json. 8 | "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json", 9 | 10 | // Schematics are listed as a map of schematicName => schematicDescription. 11 | // Each description contains a description field which is required, a factory reference, 12 | // an extends field and a schema reference. 13 | // The extends field points to another schematic (either in the same collection or a 14 | // separate collection using the format collectionName:schematicName). 15 | // The factory is required, except when using the extends field. The the factory can 16 | // overwrite the extended schematic factory. 17 | "schematics": { 18 | "keycloak": { 19 | "description": "A schematic that adds a Keycloak client.", 20 | "factory": "./keycloak/index#keycloakSchematic", 21 | "schema": "./keycloak/schema.json" 22 | }, 23 | "service": { 24 | "extends": "@schematics/angular:service" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/keycloak/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "KeycloakSchematicsSchema", 4 | "title": "Keycloak Schematics Schema", 5 | "type": "object", 6 | "properties": { 7 | "appRoot": { 8 | "type": "string", 9 | "description": "The path to create keycloak-service (usually 'app').", 10 | "default": "app", 11 | "visible": true 12 | }, 13 | "sourceDir": { 14 | "type": "string", 15 | "description": "The path of the source directory (usually 'src').", 16 | "default": "src", 17 | "alias": "sd", 18 | "visible": true 19 | }, 20 | "url": { 21 | "type": "string", 22 | "description": "The the url to the Keycloak auth server.", 23 | "default": "http://localhost:8080/auth", 24 | "alias": "u", 25 | "visible": true 26 | }, 27 | "realm": { 28 | "type": "string", 29 | "description": "The Keycloak realm for this app.", 30 | "default": "master", 31 | "alias": "r", 32 | "visible": true 33 | }, 34 | "clientId": { 35 | "type": "string", 36 | "description": "The Keycloak client id for this app.", 37 | "alias": "cid", 38 | "visible": true 39 | } 40 | }, 41 | "required": [ 42 | "clientId" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/keycloak/files/__sourceDir__/__appRoot__/keycloak-service/keycloak.interceptor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Red Hat, Inc. and/or its affiliates 3 | * and other contributors as indicated by the @author tags. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { Injectable } from '@angular/core'; 19 | import { 20 | HttpRequest, 21 | HttpHandler, 22 | HttpEvent, 23 | HttpResponse, 24 | HttpErrorResponse, 25 | HttpInterceptor, 26 | HTTP_INTERCEPTORS 27 | } from '@angular/common/http'; 28 | import { Observable } from 'rxjs/Observable'; 29 | import { KeycloakService } from './keycloak.service'; 30 | import 'rxjs/add/observable/fromPromise'; 31 | import 'rxjs/add/operator/concatMap'; 32 | import 'rxjs/add/operator/map'; 33 | 34 | @Injectable() 35 | export class KeycloakInterceptor implements HttpInterceptor { 36 | 37 | constructor(private _keycloakService: KeycloakService) { } 38 | 39 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 40 | if (!this._keycloakService.authenticated()) { return next.handle(request); } 41 | 42 | const tokenPromise: Promise = this._keycloakService.getToken(); 43 | const tokenObservable: Observable = Observable.fromPromise(tokenPromise); 44 | 45 | return tokenObservable.map((token) => { 46 | request = request.clone({ 47 | setHeaders: { 48 | Authorization: `Bearer ${token}` 49 | } 50 | }); 51 | return request; 52 | }).concatMap((newRequest) => { 53 | return next.handle(newRequest); 54 | }); 55 | 56 | } 57 | } 58 | 59 | export const KEYCLOAK_HTTP_INTERCEPTOR = { 60 | provide: HTTP_INTERCEPTORS, 61 | useClass: KeycloakInterceptor, 62 | multi: true 63 | }; -------------------------------------------------------------------------------- /src/keycloak/index.ts: -------------------------------------------------------------------------------- 1 | import { Path, normalize } from '@angular-devkit/core'; 2 | import { 3 | MergeStrategy, 4 | Rule, 5 | SchematicContext, 6 | SchematicsException, 7 | Tree, 8 | apply, 9 | chain, 10 | mergeWith, 11 | template, 12 | schematic, 13 | url, 14 | } from '@angular-devkit/schematics'; 15 | import {Schema as KeycloakOptions} from './schema'; 16 | 17 | function servicePath(options: KeycloakOptions): Path { 18 | return normalize('/' + options.appRoot + '/keycloak-service'); 19 | } 20 | 21 | export function keycloakSchematic(options: KeycloakOptions): Rule { 22 | 23 | if (!options.clientId) { 24 | throw new SchematicsException(`clientId option is required.`); 25 | } 26 | 27 | const mainFromPath: string = normalize('/' + options.sourceDir + '/main.ts'); 28 | const mainToPath: string = normalize('/' + options.sourceDir + '/main.ts.no-keycloak'); 29 | 30 | const serviceOps = { 31 | name: 'keycloak', 32 | path: servicePath(options), 33 | module: 'app.module', // where to find @NgModule (i.e. app.module.ts) 34 | appRoot: options.appRoot, 35 | spec: false 36 | }; 37 | 38 | return chain([ 39 | 40 | (_tree: Tree, context: SchematicContext) => { 41 | // Show the options for this Schematics. 42 | context.logger.info('Keycloak Schematic: ' + JSON.stringify(options)); 43 | }, 44 | 45 | // Move the developer's main.ts to a backup file. 46 | // Have to do it using read/create/delete because tree.move() does a 47 | // 'symbolic' move that is applied at the end. Won't work when we want 48 | // to replace the file with something coming from our ./files 49 | (_tree: Tree) => { 50 | const bytes: Buffer = _tree.read(mainFromPath); 51 | _tree.create(mainToPath, bytes); 52 | _tree.delete(mainFromPath); 53 | }, 54 | 55 | // Use the existing service schematic to add our service to app.module. 56 | // The skeleton service it creates will be overwritten by ./files 57 | schematic('service', serviceOps), 58 | 59 | mergeWith(apply(url('./files'), [ 60 | template({ 61 | sourceDir: options.sourceDir, 62 | appRoot: options.appRoot, 63 | realm: options.realm, 64 | clientId: options.clientId, 65 | url: options.url, 66 | }), 67 | 68 | ]), MergeStrategy.Overwrite), 69 | 70 | ]); 71 | } 72 | -------------------------------------------------------------------------------- /src/keycloak/files/__sourceDir__/__appRoot__/keycloak-service/keycloak.http.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Red Hat, Inc. and/or its affiliates 3 | * and other contributors as indicated by the @author tags. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import {Injectable} from '@angular/core'; 19 | import {Http, Request, XHRBackend, ConnectionBackend, RequestOptions, RequestOptionsArgs, Response, Headers} from '@angular/http'; 20 | 21 | import {KeycloakService} from './keycloak.service'; 22 | import {Observable} from 'rxjs/Observable'; 23 | import 'rxjs/add/observable/fromPromise'; 24 | import 'rxjs/add/operator/concatMap'; 25 | import 'rxjs/add/operator/map'; 26 | 27 | 28 | /** 29 | * This provides a wrapper over the ng2 Http class that insures tokens are refreshed on each request. 30 | */ 31 | @Injectable() 32 | export class KeycloakHttp extends Http { 33 | constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions, private _keycloakService: KeycloakService) { 34 | super(_backend, _defaultOptions); 35 | } 36 | 37 | request(url: string | Request, options?: RequestOptionsArgs): Observable { 38 | if (!this._keycloakService.authenticated()) return super.request(url, options); 39 | 40 | const tokenPromise: Promise = this._keycloakService.getToken(); 41 | const tokenObservable: Observable = Observable.fromPromise(tokenPromise); 42 | 43 | if (typeof url === 'string') { 44 | return tokenObservable.map(token => { 45 | const authOptions = new RequestOptions({headers: new Headers({'Authorization': 'Bearer ' + token})}); 46 | return new RequestOptions().merge(options).merge(authOptions); 47 | }).concatMap(opts => super.request(url, opts)); 48 | } else if (url instanceof Request) { 49 | return tokenObservable.map(token => { 50 | url.headers.set('Authorization', 'Bearer ' + token); 51 | return url; 52 | }).concatMap(request => super.request(request)); 53 | } 54 | } 55 | } 56 | 57 | export function keycloakHttpFactory(backend: XHRBackend, defaultOptions: RequestOptions, keycloakService: KeycloakService) { 58 | return new KeycloakHttp(backend, defaultOptions, keycloakService); 59 | } 60 | 61 | export const KEYCLOAK_HTTP_PROVIDER = { 62 | provide: Http, 63 | useFactory: keycloakHttpFactory, 64 | deps: [XHRBackend, RequestOptions, KeycloakService] 65 | }; 66 | -------------------------------------------------------------------------------- /src/keycloak/index_spec.ts: -------------------------------------------------------------------------------- 1 | import {Tree, VirtualTree} from '@angular-devkit/schematics'; 2 | import {SchematicTestRunner} from '@angular-devkit/schematics/testing'; 3 | import * as path from 'path'; 4 | 5 | const collectionPath = path.join(__dirname, '../collection.json'); 6 | 7 | let appTree: Tree; 8 | 9 | beforeEach(() => { 10 | appTree = new VirtualTree(); 11 | appTree = createAppModule(appTree); 12 | appTree = createMain(appTree); 13 | }); 14 | 15 | describe('keycloak-schematic', () => { 16 | const runner = new SchematicTestRunner('@ssilvert/keycloak-schematic', collectionPath); 17 | 18 | it('backs up main.ts', () => { 19 | const startMain: string = getFileContent(appTree, '/src/main.ts'); 20 | const tree = runner.runSchematic('keycloak', {'clientId': 'ngApp'}, appTree); 21 | const newMain: string = getFileContent(tree, '/src/main.ts'); 22 | const backedUpMain: string = getFileContent(tree, '/src/main.ts.no-keycloak') 23 | 24 | expect(newMain).not.toEqual(startMain); 25 | expect(startMain).toEqual(backedUpMain); 26 | }); 27 | 28 | it('copies needed files', () => { 29 | const tree = runner.runSchematic('keycloak', {'clientId': 'ngApp'}, appTree); 30 | const files: string[] = tree.files; 31 | 32 | expect(files.indexOf('/client-import.json')).toBeGreaterThanOrEqual(0); 33 | expect(files.indexOf('/src/main.ts')).toBeGreaterThanOrEqual(0); 34 | expect(files.indexOf('/src/app/keycloak-service/keycloak.d.ts')).toBeGreaterThanOrEqual(0); 35 | expect(files.indexOf('/src/app/keycloak-service/keycloak.http.ts')).toBeGreaterThanOrEqual(0); 36 | expect(files.indexOf('/src/app/keycloak-service/keycloak.js')).toBeGreaterThanOrEqual(0); 37 | expect(files.indexOf('/src/app/keycloak-service/keycloak.service.ts')).toBeGreaterThanOrEqual(0); 38 | expect(files.indexOf('/src/app/keycloak-service/keycloak.guard.ts')).toBeGreaterThanOrEqual(0); 39 | expect(files.indexOf('/src/app/keycloak-service/keycloak.interceptor.ts')).toBeGreaterThanOrEqual(0); 40 | }); 41 | 42 | it('adds KeycloakService provider to app.module.ts ', () => { 43 | const tree = runner.runSchematic('keycloak', {'clientId': 'ngApp'}, appTree); 44 | const moduleContent = getFileContent(tree, '/src/app/app.module.ts'); 45 | expect(moduleContent).toMatch(/import.*KeycloakService.*from '.\/keycloak-service\/keycloak.service'/); 46 | expect(moduleContent).toMatch(/providers:\s*\[KeycloakService\]/m); 47 | }); 48 | }); 49 | 50 | function createAppModule(tree: Tree, path?: string): Tree { 51 | tree.create(path || '/src/app/app.module.ts', ` 52 | import { BrowserModule } from '@angular/platform-browser'; 53 | import { NgModule } from '@angular/core'; 54 | import { AppComponent } from './app.component'; 55 | 56 | @NgModule({ 57 | declarations: [ 58 | AppComponent 59 | ], 60 | imports: [ 61 | BrowserModule 62 | ], 63 | providers: [], 64 | bootstrap: [AppComponent] 65 | }) 66 | export class AppModule { } 67 | `); 68 | 69 | return tree; 70 | } 71 | 72 | function createMain(tree: Tree, path?: string): Tree { 73 | tree.create(path || '/src/main.ts', ` 74 | import { enableProdMode } from '@angular/core'; 75 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 76 | 77 | import { AppModule } from './app/app.module'; 78 | import { environment } from './environments/environment'; 79 | 80 | if (environment.production) { 81 | enableProdMode(); 82 | } 83 | 84 | platformBrowserDynamic().bootstrapModule(AppModule) 85 | .catch(err => console.log(err)); 86 | `); 87 | 88 | return tree; 89 | } 90 | 91 | export function getFileContent(tree: Tree, path: string): string { 92 | const fileEntry = tree.get(path); 93 | 94 | if (!fileEntry) { 95 | throw new Error(`The file (${path}) does not exist.`); 96 | } 97 | 98 | return fileEntry.content.toString(); 99 | } -------------------------------------------------------------------------------- /src/keycloak/files/__sourceDir__/__appRoot__/keycloak-service/keycloak.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Red Hat, Inc. and/or its affiliates 3 | * and other contributors as indicated by the @author tags. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | import {Injectable} from '@angular/core'; 18 | import {KeycloakLoginOptions} from './keycloak.d'; 19 | 20 | // If using a local keycloak.js, uncomment this import. With keycloak.js fetched 21 | // from the server, you get a compile-time warning on use of the Keycloak() 22 | // method below. I'm not sure how to fix this, but it's certainly cleaner 23 | // to get keycloak.js from the server. 24 | // 25 | import * as Keycloak from './keycloak'; 26 | 27 | export type KeycloakClient = Keycloak.KeycloakInstance; 28 | type InitOptions = Keycloak.KeycloakInitOptions; 29 | 30 | @Injectable() 31 | export class KeycloakService { 32 | static keycloakAuth: KeycloakClient; 33 | 34 | /** 35 | * Configure and initialize the Keycloak adapter. 36 | * 37 | * @param configOptions Optionally, a path to keycloak.json, or an object containing 38 | * url, realm, and clientId. 39 | * @param adapterOptions Optional initiaization options. See javascript adapter docs 40 | * for details. 41 | * @returns {Promise} 42 | */ 43 | static init(configOptions?: string|{}, initOptions?: InitOptions): Promise { 44 | KeycloakService.keycloakAuth = Keycloak(configOptions); 45 | 46 | return new Promise((resolve, reject) => { 47 | KeycloakService.keycloakAuth.init(initOptions) 48 | .success(() => { 49 | resolve(); 50 | }) 51 | .error((errorData: any) => { 52 | reject(errorData); 53 | }); 54 | }); 55 | } 56 | 57 | /** 58 | * Expose the underlying Keycloak javascript adapter. 59 | */ 60 | client(): KeycloakClient { 61 | return KeycloakService.keycloakAuth; 62 | } 63 | 64 | authenticated(): boolean { 65 | return KeycloakService.keycloakAuth.authenticated; 66 | } 67 | 68 | login(options?: KeycloakLoginOptions) { 69 | KeycloakService.keycloakAuth.login(options); 70 | } 71 | 72 | logout(redirectUri?: string) { 73 | KeycloakService.keycloakAuth.logout({redirectUri: redirectUri}); 74 | } 75 | 76 | account() { 77 | KeycloakService.keycloakAuth.accountManagement(); 78 | } 79 | 80 | authServerUrl(): string { 81 | return KeycloakService.keycloakAuth.authServerUrl; 82 | } 83 | 84 | realm(): string { 85 | return KeycloakService.keycloakAuth.realm; 86 | } 87 | 88 | getToken(): Promise { 89 | return new Promise((resolve, reject) => { 90 | if (KeycloakService.keycloakAuth.token) { 91 | KeycloakService.keycloakAuth 92 | .updateToken(5) 93 | .success(() => { 94 | resolve(KeycloakService.keycloakAuth.token); 95 | }) 96 | .error(() => { 97 | reject('Failed to refresh token'); 98 | }); 99 | } else { 100 | reject('Not loggen in'); 101 | } 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/keycloak/files/__sourceDir__/__appRoot__/keycloak-service/keycloak.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright 2017 Brett Epps 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 7 | * associated documentation files (the "Software"), to deal in the Software without restriction, including 8 | * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 10 | * following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all copies or substantial 13 | * portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 16 | * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | export as namespace Keycloak; 22 | 23 | export = Keycloak; 24 | 25 | /** 26 | * Creates a new Keycloak client instance. 27 | * @param config Path to a JSON config file or a plain config object. 28 | */ 29 | declare function Keycloak(config?: string|{}): Keycloak.KeycloakInstance; 30 | 31 | declare namespace Keycloak { 32 | type KeycloakAdapterName = 'cordova'|'default'; 33 | type KeycloakOnLoad = 'login-required'|'check-sso'; 34 | type KeycloakResponseMode = 'query'|'fragment'; 35 | type KeycloakResponseType = 'code'|'id_token token'|'code id_token token'; 36 | type KeycloakFlow = 'standard'|'implicit'|'hybrid'; 37 | 38 | interface KeycloakInitOptions { 39 | /** 40 | * @private Undocumented. 41 | */ 42 | adapter?: KeycloakAdapterName; 43 | 44 | /** 45 | * Specifies an action to do on load. 46 | */ 47 | onLoad?: KeycloakOnLoad; 48 | 49 | /** 50 | * Set an initial value for the token. 51 | */ 52 | token?: string; 53 | 54 | /** 55 | * Set an initial value for the refresh token. 56 | */ 57 | refreshToken?: string; 58 | 59 | /** 60 | * Set an initial value for the id token (only together with `token` or 61 | * `refreshToken`). 62 | */ 63 | idToken?: string; 64 | 65 | /** 66 | * Set an initial value for skew between local time and Keycloak server in 67 | * seconds (only together with `token` or `refreshToken`). 68 | */ 69 | timeSkew?: number; 70 | 71 | /** 72 | * Set to enable/disable monitoring login state. 73 | * @default true 74 | */ 75 | checkLoginIframe?: boolean; 76 | 77 | /** 78 | * Set the interval to check login state (in seconds). 79 | * @default 5 80 | */ 81 | checkLoginIframeInterval?: boolean; 82 | 83 | /** 84 | * Set the OpenID Connect response mode to send to Keycloak upon login. 85 | * @default fragment After successful authentication Keycloak will redirect 86 | * to JavaScript application with OpenID Connect parameters 87 | * added in URL fragment. This is generally safer and 88 | * recommended over query. 89 | */ 90 | responseMode?: KeycloakResponseMode; 91 | 92 | /** 93 | * Set the OpenID Connect flow. 94 | * @default standard 95 | */ 96 | flow?: KeycloakFlow; 97 | } 98 | 99 | interface KeycloakLoginOptions { 100 | /** 101 | * @private Undocumented. 102 | */ 103 | scope?: string; 104 | 105 | /** 106 | * Specifies the uri to redirect to after login. 107 | */ 108 | redirectUri?: string; 109 | 110 | /** 111 | * By default the login screen is displayed if the user is not logged into 112 | * Keycloak. To only authenticate to the application if the user is already 113 | * logged in and not display the login page if the user is not logged in, set 114 | * this option to `'none'`. To always require re-authentication and ignore 115 | * SSO, set this option to `'login'`. 116 | */ 117 | prompt?: 'none'|'login'; 118 | 119 | /** 120 | * If value is `'register'` then user is redirected to registration page, 121 | * otherwise to login page. 122 | */ 123 | action?: 'register'; 124 | 125 | /** 126 | * Used just if user is already authenticated. Specifies maximum time since 127 | * the authentication of user happened. If user is already authenticated for 128 | * longer time than `'maxAge'`, the SSO is ignored and he will need to 129 | * authenticate again. 130 | */ 131 | maxAge?: number; 132 | 133 | /** 134 | * Used to pre-fill the username/email field on the login form. 135 | */ 136 | loginHint?: string; 137 | 138 | /** 139 | * Used to tell Keycloak which IDP the user wants to authenticate with. 140 | */ 141 | idpHint?: string; 142 | 143 | /** 144 | * Specifies the desired locale for the UI. 145 | */ 146 | locale?: string; 147 | } 148 | 149 | type KeycloakPromiseCallback = (result: T) => void; 150 | 151 | interface KeycloakPromise { 152 | /** 153 | * Function to call if the promised action succeeds. 154 | */ 155 | success(callback: KeycloakPromiseCallback): KeycloakPromise; 156 | 157 | /** 158 | * Function to call if the promised action throws an error. 159 | */ 160 | error(callback: KeycloakPromiseCallback): KeycloakPromise; 161 | } 162 | 163 | interface KeycloakError { 164 | error: string; 165 | error_description: string; 166 | } 167 | 168 | interface KeycloakAdapter { 169 | login(options?: KeycloakLoginOptions): KeycloakPromise; 170 | logout(options?: any): KeycloakPromise; 171 | register(options?: KeycloakLoginOptions): KeycloakPromise; 172 | accountManagement(): KeycloakPromise; 173 | redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string; 174 | } 175 | 176 | interface KeycloakProfile { 177 | id?: string; 178 | username?: string; 179 | email?: string; 180 | firstName?: string; 181 | lastName?: string; 182 | enabled?: boolean; 183 | emailVerified?: boolean; 184 | totp?: boolean; 185 | createdTimestamp?: number; 186 | } 187 | 188 | // export interface KeycloakUserInfo {} 189 | 190 | /** 191 | * A client for the Keycloak authentication server. 192 | * @see {@link https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/javascript-adapter.html|Keycloak JS adapter documentation} 193 | */ 194 | interface KeycloakInstance { 195 | /** 196 | * Is true if the user is authenticated, false otherwise. 197 | */ 198 | authenticated?: boolean; 199 | 200 | /** 201 | * The user id. 202 | */ 203 | subject?: string; 204 | 205 | /** 206 | * Response mode passed in init (default value is `'fragment'`). 207 | */ 208 | responseMode?: KeycloakResponseMode; 209 | 210 | /** 211 | * Response type sent to Keycloak with login requests. This is determined 212 | * based on the flow value used during initialization, but can be overridden 213 | * by setting this value. 214 | */ 215 | responseType?: KeycloakResponseType; 216 | 217 | /** 218 | * Flow passed in init. 219 | */ 220 | flow?: KeycloakFlow; 221 | 222 | /** 223 | * The realm roles associated with the token. 224 | */ 225 | realmAccess?: { roles: string[] }; 226 | 227 | /** 228 | * The resource roles associated with the token. 229 | */ 230 | resourceAccess?: string[]; 231 | 232 | /** 233 | * The base64 encoded token that can be sent in the Authorization header in 234 | * requests to services. 235 | */ 236 | token?: string; 237 | 238 | /** 239 | * The parsed token as a JavaScript object. 240 | */ 241 | tokenParsed?: { 242 | exp?: number; 243 | iat?: number; 244 | nonce?: string; 245 | sub?: string; 246 | session_state?: string; 247 | realm_access?: { roles: string[] }; 248 | resource_access?: string[]; 249 | }; 250 | 251 | /** 252 | * The base64 encoded refresh token that can be used to retrieve a new token. 253 | */ 254 | refreshToken?: string; 255 | 256 | /** 257 | * The parsed refresh token as a JavaScript object. 258 | */ 259 | refreshTokenParsed?: { nonce?: string }; 260 | 261 | /** 262 | * The base64 encoded ID token. 263 | */ 264 | idToken?: string; 265 | 266 | /** 267 | * The parsed id token as a JavaScript object. 268 | */ 269 | idTokenParsed?: { nonce?: string }; 270 | 271 | /** 272 | * The estimated time difference between the browser time and the Keycloak 273 | * server in seconds. This value is just an estimation, but is accurate 274 | * enough when determining if a token is expired or not. 275 | */ 276 | timeSkew?: number; 277 | 278 | /** 279 | * @private Undocumented. 280 | */ 281 | loginRequired?: boolean; 282 | 283 | /** 284 | * @private Undocumented. 285 | */ 286 | authServerUrl?: string; 287 | 288 | /** 289 | * @private Undocumented. 290 | */ 291 | realm?: string; 292 | 293 | /** 294 | * @private Undocumented. 295 | */ 296 | clientId?: string; 297 | 298 | /** 299 | * @private Undocumented. 300 | */ 301 | clientSecret?: string; 302 | 303 | /** 304 | * @private Undocumented. 305 | */ 306 | redirectUri?: string; 307 | 308 | /** 309 | * @private Undocumented. 310 | */ 311 | sessionId?: string; 312 | 313 | /** 314 | * @private Undocumented. 315 | */ 316 | profile?: KeycloakProfile; 317 | 318 | /** 319 | * @private Undocumented. 320 | */ 321 | userInfo?: {}; // KeycloakUserInfo; 322 | 323 | /** 324 | * Called when the adapter is initialized. 325 | */ 326 | onReady?(authenticated?: boolean): void; 327 | 328 | /** 329 | * Called when a user is successfully authenticated. 330 | */ 331 | onAuthSuccess?(): void; 332 | 333 | /** 334 | * Called if there was an error during authentication. 335 | */ 336 | onAuthError?(errorData: KeycloakError): void; 337 | 338 | /** 339 | * Called when the token is refreshed. 340 | */ 341 | onAuthRefreshSuccess?(): void; 342 | 343 | /** 344 | * Called if there was an error while trying to refresh the token. 345 | */ 346 | onAuthRefreshError?(): void; 347 | 348 | /** 349 | * Called if the user is logged out (will only be called if the session 350 | * status iframe is enabled, or in Cordova mode). 351 | */ 352 | onAuthLogout?(): void; 353 | 354 | /** 355 | * Called when the access token is expired. If a refresh token is available 356 | * the token can be refreshed with Keycloak#updateToken, or in cases where 357 | * it's not (ie. with implicit flow) you can redirect to login screen to 358 | * obtain a new access token. 359 | */ 360 | onTokenExpired?(): void; 361 | 362 | /** 363 | * Called to initialize the adapter. 364 | * @param initOptions Initialization options. 365 | * @returns A promise to set functions to be invoked on success or error. 366 | */ 367 | init(initOptions: KeycloakInitOptions): KeycloakPromise; 368 | 369 | /** 370 | * Redirects to login form. 371 | * @param options Login options. 372 | */ 373 | login(options?: KeycloakLoginOptions): KeycloakPromise; 374 | 375 | /** 376 | * Redirects to logout. 377 | * @param options Logout options. 378 | * @param options.redirectUri Specifies the uri to redirect to after logout. 379 | */ 380 | logout(options?: any): KeycloakPromise; 381 | 382 | /** 383 | * Redirects to registration form. 384 | * @param options Supports same options as Keycloak#login but `action` is 385 | * set to `'register'`. 386 | */ 387 | register(options?: any): KeycloakPromise; 388 | 389 | /** 390 | * Redirects to the Account Management Console. 391 | */ 392 | accountManagement(): KeycloakPromise; 393 | 394 | /** 395 | * Returns the URL to login form. 396 | * @param options Supports same options as Keycloak#login. 397 | */ 398 | createLoginUrl(options?: KeycloakLoginOptions): string; 399 | 400 | /** 401 | * Returns the URL to logout the user. 402 | * @param options Logout options. 403 | * @param options.redirectUri Specifies the uri to redirect to after logout. 404 | */ 405 | createLogoutUrl(options?: any): string; 406 | 407 | /** 408 | * Returns the URL to registration page. 409 | * @param options Supports same options as Keycloak#createLoginUrl but 410 | * `action` is set to `'register'`. 411 | */ 412 | createRegisterUrl(options?: KeycloakLoginOptions): string; 413 | 414 | /** 415 | * Returns the URL to the Account Management Console. 416 | */ 417 | createAccountUrl(): string; 418 | 419 | /** 420 | * Returns true if the token has less than `minValidity` seconds left before 421 | * it expires. 422 | * @param minValidity If not specified, `0` is used. 423 | */ 424 | isTokenExpired(minValidity?: number): boolean; 425 | 426 | /** 427 | * If the token expires within `minValidity` seconds, the token is refreshed. 428 | * If the session status iframe is enabled, the session status is also 429 | * checked. 430 | * @returns A promise to set functions that can be invoked if the token is 431 | * still valid, or if the token is no longer valid. 432 | * @example 433 | * ```js 434 | * keycloak.updateToken(5).success(function(refreshed) { 435 | * if (refreshed) { 436 | * alert('Token was successfully refreshed'); 437 | * } else { 438 | * alert('Token is still valid'); 439 | * } 440 | * }).error(function() { 441 | * alert('Failed to refresh the token, or the session has expired'); 442 | * }); 443 | */ 444 | updateToken(minValidity: number): KeycloakPromise; 445 | 446 | /** 447 | * Clears authentication state, including tokens. This can be useful if 448 | * the application has detected the session was expired, for example if 449 | * updating token fails. Invoking this results in Keycloak#onAuthLogout 450 | * callback listener being invoked. 451 | */ 452 | clearToken(): void; 453 | 454 | /** 455 | * Returns true if the token has the given realm role. 456 | * @param role A realm role name. 457 | */ 458 | hasRealmRole(role: string): boolean; 459 | 460 | /** 461 | * Returns true if the token has the given role for the resource. 462 | * @param role A role name. 463 | * @param resource If not specified, `clientId` is used. 464 | */ 465 | hasResourceRole(role: string, resource?: string): boolean; 466 | 467 | /** 468 | * Loads the user's profile. 469 | * @returns A promise to set functions to be invoked on success or error. 470 | */ 471 | loadUserProfile(): KeycloakPromise; 472 | 473 | /** 474 | * @private Undocumented. 475 | */ 476 | loadUserInfo(): KeycloakPromise<{}, void>; 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/keycloak/files/__sourceDir__/__appRoot__/keycloak-service/keycloak.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Red Hat, Inc. and/or its affiliates 3 | * and other contributors as indicated by the @author tags. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | (function( window, undefined ) { 19 | 20 | var Keycloak = function (config) { 21 | if (!(this instanceof Keycloak)) { 22 | return new Keycloak(config); 23 | } 24 | 25 | var kc = this; 26 | var adapter; 27 | var refreshQueue = []; 28 | var callbackStorage; 29 | 30 | var loginIframe = { 31 | enable: true, 32 | callbackList: [], 33 | interval: 5 34 | }; 35 | 36 | var scripts = document.getElementsByTagName('script'); 37 | for (var i = 0; i < scripts.length; i++) { 38 | if ((scripts[i].src.indexOf('keycloak.js') !== -1 || scripts[i].src.indexOf('keycloak.min.js') !== -1) && scripts[i].src.indexOf('version=') !== -1) { 39 | kc.iframeVersion = scripts[i].src.substring(scripts[i].src.indexOf('version=') + 8).split('&')[0]; 40 | } 41 | } 42 | 43 | kc.init = function (initOptions) { 44 | kc.authenticated = false; 45 | 46 | callbackStorage = createCallbackStorage(); 47 | 48 | if (initOptions && initOptions.adapter === 'cordova') { 49 | adapter = loadAdapter('cordova'); 50 | } else if (initOptions && initOptions.adapter === 'default') { 51 | adapter = loadAdapter(); 52 | } else { 53 | if (window.Cordova || window.cordova) { 54 | adapter = loadAdapter('cordova'); 55 | } else { 56 | adapter = loadAdapter(); 57 | } 58 | } 59 | 60 | if (initOptions) { 61 | if (typeof initOptions.checkLoginIframe !== 'undefined') { 62 | loginIframe.enable = initOptions.checkLoginIframe; 63 | } 64 | 65 | if (initOptions.checkLoginIframeInterval) { 66 | loginIframe.interval = initOptions.checkLoginIframeInterval; 67 | } 68 | 69 | if (initOptions.onLoad === 'login-required') { 70 | kc.loginRequired = true; 71 | } 72 | 73 | if (initOptions.responseMode) { 74 | if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') { 75 | kc.responseMode = initOptions.responseMode; 76 | } else { 77 | throw 'Invalid value for responseMode'; 78 | } 79 | } 80 | 81 | if (initOptions.flow) { 82 | switch (initOptions.flow) { 83 | case 'standard': 84 | kc.responseType = 'code'; 85 | break; 86 | case 'implicit': 87 | kc.responseType = 'id_token token'; 88 | break; 89 | case 'hybrid': 90 | kc.responseType = 'code id_token token'; 91 | break; 92 | default: 93 | throw 'Invalid value for flow'; 94 | } 95 | kc.flow = initOptions.flow; 96 | } 97 | 98 | if (initOptions.timeSkew != null) { 99 | kc.timeSkew = initOptions.timeSkew; 100 | } 101 | } 102 | 103 | if (!kc.responseMode) { 104 | kc.responseMode = 'fragment'; 105 | } 106 | if (!kc.responseType) { 107 | kc.responseType = 'code'; 108 | kc.flow = 'standard'; 109 | } 110 | 111 | var promise = createPromise(); 112 | 113 | var initPromise = createPromise(); 114 | initPromise.promise.success(function() { 115 | kc.onReady && kc.onReady(kc.authenticated); 116 | promise.setSuccess(kc.authenticated); 117 | }).error(function(errorData) { 118 | promise.setError(errorData); 119 | }); 120 | 121 | var configPromise = loadConfig(config); 122 | 123 | function onLoad() { 124 | var doLogin = function(prompt) { 125 | if (!prompt) { 126 | options.prompt = 'none'; 127 | } 128 | kc.login(options).success(function () { 129 | initPromise.setSuccess(); 130 | }).error(function () { 131 | initPromise.setError(); 132 | }); 133 | } 134 | 135 | var options = {}; 136 | switch (initOptions.onLoad) { 137 | case 'check-sso': 138 | if (loginIframe.enable) { 139 | setupCheckLoginIframe().success(function() { 140 | checkLoginIframe().success(function () { 141 | doLogin(false); 142 | }).error(function () { 143 | initPromise.setSuccess(); 144 | }); 145 | }); 146 | } else { 147 | doLogin(false); 148 | } 149 | break; 150 | case 'login-required': 151 | doLogin(true); 152 | break; 153 | default: 154 | throw 'Invalid value for onLoad'; 155 | } 156 | } 157 | 158 | function processInit() { 159 | var callback = parseCallback(window.location.href); 160 | 161 | if (callback) { 162 | return setupCheckLoginIframe().success(function() { 163 | window.history.replaceState({}, null, callback.newUrl); 164 | processCallback(callback, initPromise); 165 | }).error(function (e) { 166 | initPromise.setError(); 167 | }); 168 | } else if (initOptions) { 169 | if (initOptions.token && initOptions.refreshToken) { 170 | setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken); 171 | 172 | if (loginIframe.enable) { 173 | setupCheckLoginIframe().success(function() { 174 | checkLoginIframe().success(function () { 175 | kc.onAuthSuccess && kc.onAuthSuccess(); 176 | initPromise.setSuccess(); 177 | }).error(function () { 178 | setToken(null, null, null); 179 | initPromise.setSuccess(); 180 | }); 181 | }); 182 | } else { 183 | kc.updateToken(-1).success(function() { 184 | kc.onAuthSuccess && kc.onAuthSuccess(); 185 | initPromise.setSuccess(); 186 | }).error(function() { 187 | kc.onAuthError && kc.onAuthError(); 188 | if (initOptions.onLoad) { 189 | onLoad(); 190 | } else { 191 | initPromise.setError(); 192 | } 193 | }); 194 | } 195 | } else if (initOptions.onLoad) { 196 | onLoad(); 197 | } else { 198 | initPromise.setSuccess(); 199 | } 200 | } else { 201 | initPromise.setSuccess(); 202 | } 203 | } 204 | 205 | configPromise.success(processInit); 206 | configPromise.error(function() { 207 | promise.setError(); 208 | }); 209 | 210 | return promise.promise; 211 | } 212 | 213 | kc.login = function (options) { 214 | return adapter.login(options); 215 | } 216 | 217 | kc.createLoginUrl = function(options) { 218 | var state = createUUID(); 219 | var nonce = createUUID(); 220 | 221 | var redirectUri = adapter.redirectUri(options); 222 | 223 | var callbackState = { 224 | state: state, 225 | nonce: nonce, 226 | redirectUri: encodeURIComponent(redirectUri) 227 | } 228 | 229 | if (options && options.prompt) { 230 | callbackState.prompt = options.prompt; 231 | } 232 | 233 | callbackStorage.add(callbackState); 234 | 235 | var action = 'auth'; 236 | if (options && options.action == 'register') { 237 | action = 'registrations'; 238 | } 239 | 240 | var scope = (options && options.scope) ? "openid " + options.scope : "openid"; 241 | 242 | var url = getRealmUrl() 243 | + '/protocol/openid-connect/' + action 244 | + '?client_id=' + encodeURIComponent(kc.clientId) 245 | + '&redirect_uri=' + encodeURIComponent(redirectUri) 246 | + '&state=' + encodeURIComponent(state) 247 | + '&nonce=' + encodeURIComponent(nonce) 248 | + '&response_mode=' + encodeURIComponent(kc.responseMode) 249 | + '&response_type=' + encodeURIComponent(kc.responseType) 250 | + '&scope=' + encodeURIComponent(scope); 251 | 252 | if (options && options.prompt) { 253 | url += '&prompt=' + encodeURIComponent(options.prompt); 254 | } 255 | 256 | if (options && options.maxAge) { 257 | url += '&max_age=' + encodeURIComponent(options.maxAge); 258 | } 259 | 260 | if (options && options.loginHint) { 261 | url += '&login_hint=' + encodeURIComponent(options.loginHint); 262 | } 263 | 264 | if (options && options.idpHint) { 265 | url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint); 266 | } 267 | 268 | if (options && options.locale) { 269 | url += '&ui_locales=' + encodeURIComponent(options.locale); 270 | } 271 | 272 | return url; 273 | } 274 | 275 | kc.logout = function(options) { 276 | return adapter.logout(options); 277 | } 278 | 279 | kc.createLogoutUrl = function(options) { 280 | var url = getRealmUrl() 281 | + '/protocol/openid-connect/logout' 282 | + '?redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false)); 283 | 284 | return url; 285 | } 286 | 287 | kc.register = function (options) { 288 | return adapter.register(options); 289 | } 290 | 291 | kc.createRegisterUrl = function(options) { 292 | if (!options) { 293 | options = {}; 294 | } 295 | options.action = 'register'; 296 | return kc.createLoginUrl(options); 297 | } 298 | 299 | kc.createAccountUrl = function(options) { 300 | var url = getRealmUrl() 301 | + '/account' 302 | + '?referrer=' + encodeURIComponent(kc.clientId) 303 | + '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options)); 304 | 305 | return url; 306 | } 307 | 308 | kc.accountManagement = function() { 309 | return adapter.accountManagement(); 310 | } 311 | 312 | kc.hasRealmRole = function (role) { 313 | var access = kc.realmAccess; 314 | return !!access && access.roles.indexOf(role) >= 0; 315 | } 316 | 317 | kc.hasResourceRole = function(role, resource) { 318 | if (!kc.resourceAccess) { 319 | return false; 320 | } 321 | 322 | var access = kc.resourceAccess[resource || kc.clientId]; 323 | return !!access && access.roles.indexOf(role) >= 0; 324 | } 325 | 326 | kc.loadUserProfile = function() { 327 | var url = getRealmUrl() + '/account'; 328 | var req = new XMLHttpRequest(); 329 | req.open('GET', url, true); 330 | req.setRequestHeader('Accept', 'application/json'); 331 | req.setRequestHeader('Authorization', 'bearer ' + kc.token); 332 | 333 | var promise = createPromise(); 334 | 335 | req.onreadystatechange = function () { 336 | if (req.readyState == 4) { 337 | if (req.status == 200) { 338 | kc.profile = JSON.parse(req.responseText); 339 | promise.setSuccess(kc.profile); 340 | } else { 341 | promise.setError(); 342 | } 343 | } 344 | } 345 | 346 | req.send(); 347 | 348 | return promise.promise; 349 | } 350 | 351 | kc.loadUserInfo = function() { 352 | var url = getRealmUrl() + '/protocol/openid-connect/userinfo'; 353 | var req = new XMLHttpRequest(); 354 | req.open('GET', url, true); 355 | req.setRequestHeader('Accept', 'application/json'); 356 | req.setRequestHeader('Authorization', 'bearer ' + kc.token); 357 | 358 | var promise = createPromise(); 359 | 360 | req.onreadystatechange = function () { 361 | if (req.readyState == 4) { 362 | if (req.status == 200) { 363 | kc.userInfo = JSON.parse(req.responseText); 364 | promise.setSuccess(kc.userInfo); 365 | } else { 366 | promise.setError(); 367 | } 368 | } 369 | } 370 | 371 | req.send(); 372 | 373 | return promise.promise; 374 | } 375 | 376 | kc.isTokenExpired = function(minValidity) { 377 | if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit' )) { 378 | throw 'Not authenticated'; 379 | } 380 | 381 | if (kc.timeSkew == null) { 382 | console.info('[KEYCLOAK] Unable to determine if token is expired as timeskew is not set'); 383 | return true; 384 | } 385 | 386 | var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew; 387 | if (minValidity) { 388 | expiresIn -= minValidity; 389 | } 390 | return expiresIn < 0; 391 | } 392 | 393 | kc.updateToken = function(minValidity) { 394 | var promise = createPromise(); 395 | 396 | if (!kc.refreshToken) { 397 | promise.setError(); 398 | return promise.promise; 399 | } 400 | 401 | minValidity = minValidity || 5; 402 | 403 | var exec = function() { 404 | var refreshToken = false; 405 | if (minValidity == -1) { 406 | refreshToken = true; 407 | console.info('[KEYCLOAK] Refreshing token: forced refresh'); 408 | } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) { 409 | refreshToken = true; 410 | console.info('[KEYCLOAK] Refreshing token: token expired'); 411 | } 412 | 413 | if (!refreshToken) { 414 | promise.setSuccess(false); 415 | } else { 416 | var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken; 417 | var url = getRealmUrl() + '/protocol/openid-connect/token'; 418 | 419 | refreshQueue.push(promise); 420 | 421 | if (refreshQueue.length == 1) { 422 | var req = new XMLHttpRequest(); 423 | req.open('POST', url, true); 424 | req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 425 | req.withCredentials = true; 426 | 427 | if (kc.clientId && kc.clientSecret) { 428 | req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret)); 429 | } else { 430 | params += '&client_id=' + encodeURIComponent(kc.clientId); 431 | } 432 | 433 | var timeLocal = new Date().getTime(); 434 | 435 | req.onreadystatechange = function () { 436 | if (req.readyState == 4) { 437 | if (req.status == 200) { 438 | console.info('[KEYCLOAK] Token refreshed'); 439 | 440 | timeLocal = (timeLocal + new Date().getTime()) / 2; 441 | 442 | var tokenResponse = JSON.parse(req.responseText); 443 | 444 | setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], timeLocal); 445 | 446 | kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess(); 447 | for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { 448 | p.setSuccess(true); 449 | } 450 | } else { 451 | console.warn('[KEYCLOAK] Failed to refresh token'); 452 | 453 | kc.onAuthRefreshError && kc.onAuthRefreshError(); 454 | for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { 455 | p.setError(true); 456 | } 457 | } 458 | } 459 | }; 460 | 461 | req.send(params); 462 | } 463 | } 464 | } 465 | 466 | if (loginIframe.enable) { 467 | var iframePromise = checkLoginIframe(); 468 | iframePromise.success(function() { 469 | exec(); 470 | }).error(function() { 471 | promise.setError(); 472 | }); 473 | } else { 474 | exec(); 475 | } 476 | 477 | return promise.promise; 478 | } 479 | 480 | kc.clearToken = function() { 481 | if (kc.token) { 482 | setToken(null, null, null); 483 | kc.onAuthLogout && kc.onAuthLogout(); 484 | if (kc.loginRequired) { 485 | kc.login(); 486 | } 487 | } 488 | } 489 | 490 | function getRealmUrl() { 491 | if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') { 492 | return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm); 493 | } else { 494 | return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm); 495 | } 496 | } 497 | 498 | function getOrigin() { 499 | if (!window.location.origin) { 500 | return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: ''); 501 | } else { 502 | return window.location.origin; 503 | } 504 | } 505 | 506 | function processCallback(oauth, promise) { 507 | var code = oauth.code; 508 | var error = oauth.error; 509 | var prompt = oauth.prompt; 510 | 511 | var timeLocal = new Date().getTime(); 512 | 513 | if (error) { 514 | if (prompt != 'none') { 515 | var errorData = { error: error, error_description: oauth.error_description }; 516 | kc.onAuthError && kc.onAuthError(errorData); 517 | promise && promise.setError(errorData); 518 | } else { 519 | promise && promise.setSuccess(); 520 | } 521 | return; 522 | } else if ((kc.flow != 'standard') && (oauth.access_token || oauth.id_token)) { 523 | authSuccess(oauth.access_token, null, oauth.id_token, true); 524 | } 525 | 526 | if ((kc.flow != 'implicit') && code) { 527 | var params = 'code=' + code + '&grant_type=authorization_code'; 528 | var url = getRealmUrl() + '/protocol/openid-connect/token'; 529 | 530 | var req = new XMLHttpRequest(); 531 | req.open('POST', url, true); 532 | req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 533 | 534 | if (kc.clientId && kc.clientSecret) { 535 | req.setRequestHeader('Authorization', 'Basic ' + btoa(kc.clientId + ':' + kc.clientSecret)); 536 | } else { 537 | params += '&client_id=' + encodeURIComponent(kc.clientId); 538 | } 539 | 540 | params += '&redirect_uri=' + oauth.redirectUri; 541 | 542 | req.withCredentials = true; 543 | 544 | req.onreadystatechange = function() { 545 | if (req.readyState == 4) { 546 | if (req.status == 200) { 547 | 548 | var tokenResponse = JSON.parse(req.responseText); 549 | authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard'); 550 | } else { 551 | kc.onAuthError && kc.onAuthError(); 552 | promise && promise.setError(); 553 | } 554 | } 555 | }; 556 | 557 | req.send(params); 558 | } 559 | 560 | function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) { 561 | timeLocal = (timeLocal + new Date().getTime()) / 2; 562 | 563 | setToken(accessToken, refreshToken, idToken, timeLocal); 564 | 565 | if ((kc.tokenParsed && kc.tokenParsed.nonce != oauth.storedNonce) || 566 | (kc.refreshTokenParsed && kc.refreshTokenParsed.nonce != oauth.storedNonce) || 567 | (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) { 568 | 569 | console.info('[KEYCLOAK] Invalid nonce, clearing token'); 570 | kc.clearToken(); 571 | promise && promise.setError(); 572 | } else { 573 | if (fulfillPromise) { 574 | kc.onAuthSuccess && kc.onAuthSuccess(); 575 | promise && promise.setSuccess(); 576 | } 577 | } 578 | } 579 | 580 | } 581 | 582 | function loadConfig(url) { 583 | var promise = createPromise(); 584 | var configUrl; 585 | 586 | if (!config) { 587 | configUrl = 'keycloak.json'; 588 | } else if (typeof config === 'string') { 589 | configUrl = config; 590 | } 591 | 592 | if (configUrl) { 593 | var req = new XMLHttpRequest(); 594 | req.open('GET', configUrl, true); 595 | req.setRequestHeader('Accept', 'application/json'); 596 | 597 | req.onreadystatechange = function () { 598 | if (req.readyState == 4) { 599 | if (req.status == 200 || fileLoaded(req)) { 600 | var config = JSON.parse(req.responseText); 601 | 602 | kc.authServerUrl = config['auth-server-url']; 603 | kc.realm = config['realm']; 604 | kc.clientId = config['resource']; 605 | kc.clientSecret = (config['credentials'] || {})['secret']; 606 | 607 | promise.setSuccess(); 608 | } else { 609 | promise.setError(); 610 | } 611 | } 612 | }; 613 | 614 | req.send(); 615 | } else { 616 | if (!config['url']) { 617 | var scripts = document.getElementsByTagName('script'); 618 | for (var i = 0; i < scripts.length; i++) { 619 | if (scripts[i].src.match(/.*keycloak\.js/)) { 620 | config.url = scripts[i].src.substr(0, scripts[i].src.indexOf('/js/keycloak.js')); 621 | break; 622 | } 623 | } 624 | } 625 | 626 | if (!config.realm) { 627 | throw 'realm missing'; 628 | } 629 | 630 | if (!config.clientId) { 631 | throw 'clientId missing'; 632 | } 633 | 634 | kc.authServerUrl = config.url; 635 | kc.realm = config.realm; 636 | kc.clientId = config.clientId; 637 | kc.clientSecret = (config.credentials || {}).secret; 638 | 639 | promise.setSuccess(); 640 | } 641 | 642 | return promise.promise; 643 | } 644 | 645 | function fileLoaded(xhr) { 646 | return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:'); 647 | } 648 | 649 | function setToken(token, refreshToken, idToken, timeLocal) { 650 | if (kc.tokenTimeoutHandle) { 651 | clearTimeout(kc.tokenTimeoutHandle); 652 | kc.tokenTimeoutHandle = null; 653 | } 654 | 655 | if (refreshToken) { 656 | kc.refreshToken = refreshToken; 657 | kc.refreshTokenParsed = decodeToken(refreshToken); 658 | } else { 659 | delete kc.refreshToken; 660 | delete kc.refreshTokenParsed; 661 | } 662 | 663 | if (idToken) { 664 | kc.idToken = idToken; 665 | kc.idTokenParsed = decodeToken(idToken); 666 | } else { 667 | delete kc.idToken; 668 | delete kc.idTokenParsed; 669 | } 670 | 671 | if (token) { 672 | kc.token = token; 673 | kc.tokenParsed = decodeToken(token); 674 | kc.sessionId = kc.tokenParsed.session_state; 675 | kc.authenticated = true; 676 | kc.subject = kc.tokenParsed.sub; 677 | kc.realmAccess = kc.tokenParsed.realm_access; 678 | kc.resourceAccess = kc.tokenParsed.resource_access; 679 | 680 | if (timeLocal) { 681 | kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat; 682 | } 683 | 684 | if (kc.timeSkew != null) { 685 | console.info('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds'); 686 | 687 | if (kc.onTokenExpired) { 688 | var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000; 689 | console.info('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s'); 690 | if (expiresIn <= 0) { 691 | kc.onTokenExpired(); 692 | } else { 693 | kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn); 694 | } 695 | } 696 | } 697 | } else { 698 | delete kc.token; 699 | delete kc.tokenParsed; 700 | delete kc.subject; 701 | delete kc.realmAccess; 702 | delete kc.resourceAccess; 703 | 704 | kc.authenticated = false; 705 | } 706 | } 707 | 708 | function decodeToken(str) { 709 | str = str.split('.')[1]; 710 | 711 | str = str.replace('/-/g', '+'); 712 | str = str.replace('/_/g', '/'); 713 | switch (str.length % 4) 714 | { 715 | case 0: 716 | break; 717 | case 2: 718 | str += '=='; 719 | break; 720 | case 3: 721 | str += '='; 722 | break; 723 | default: 724 | throw 'Invalid token'; 725 | } 726 | 727 | str = (str + '===').slice(0, str.length + (str.length % 4)); 728 | str = str.replace(/-/g, '+').replace(/_/g, '/'); 729 | 730 | str = decodeURIComponent(escape(atob(str))); 731 | 732 | str = JSON.parse(str); 733 | return str; 734 | } 735 | 736 | function createUUID() { 737 | var s = []; 738 | var hexDigits = '0123456789abcdef'; 739 | for (var i = 0; i < 36; i++) { 740 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); 741 | } 742 | s[14] = '4'; 743 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); 744 | s[8] = s[13] = s[18] = s[23] = '-'; 745 | var uuid = s.join(''); 746 | return uuid; 747 | } 748 | 749 | kc.callback_id = 0; 750 | 751 | function createCallbackId() { 752 | var id = ''; 753 | return id; 754 | 755 | } 756 | 757 | function parseCallback(url) { 758 | var oauth = new CallbackParser(url, kc.responseMode).parseUri(); 759 | var oauthState = callbackStorage.get(oauth.state); 760 | 761 | if (oauthState && (oauth.code || oauth.error || oauth.access_token || oauth.id_token)) { 762 | oauth.redirectUri = oauthState.redirectUri; 763 | oauth.storedNonce = oauthState.nonce; 764 | oauth.prompt = oauthState.prompt; 765 | 766 | if (oauth.fragment) { 767 | oauth.newUrl += '#' + oauth.fragment; 768 | } 769 | 770 | return oauth; 771 | } 772 | } 773 | 774 | function createPromise() { 775 | var p = { 776 | setSuccess: function(result) { 777 | p.success = true; 778 | p.result = result; 779 | if (p.successCallback) { 780 | p.successCallback(result); 781 | } 782 | }, 783 | 784 | setError: function(result) { 785 | p.error = true; 786 | p.result = result; 787 | if (p.errorCallback) { 788 | p.errorCallback(result); 789 | } 790 | }, 791 | 792 | promise: { 793 | success: function(callback) { 794 | if (p.success) { 795 | callback(p.result); 796 | } else if (!p.error) { 797 | p.successCallback = callback; 798 | } 799 | return p.promise; 800 | }, 801 | error: function(callback) { 802 | if (p.error) { 803 | callback(p.result); 804 | } else if (!p.success) { 805 | p.errorCallback = callback; 806 | } 807 | return p.promise; 808 | } 809 | } 810 | } 811 | return p; 812 | } 813 | 814 | function setupCheckLoginIframe() { 815 | var promise = createPromise(); 816 | 817 | if (!loginIframe.enable) { 818 | promise.setSuccess(); 819 | return promise.promise; 820 | } 821 | 822 | if (loginIframe.iframe) { 823 | promise.setSuccess(); 824 | return promise.promise; 825 | } 826 | 827 | var iframe = document.createElement('iframe'); 828 | loginIframe.iframe = iframe; 829 | 830 | iframe.onload = function() { 831 | var realmUrl = getRealmUrl(); 832 | if (realmUrl.charAt(0) === '/') { 833 | loginIframe.iframeOrigin = getOrigin(); 834 | } else { 835 | loginIframe.iframeOrigin = realmUrl.substring(0, realmUrl.indexOf('/', 8)); 836 | } 837 | promise.setSuccess(); 838 | 839 | setTimeout(check, loginIframe.interval * 1000); 840 | } 841 | 842 | var src = getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html'; 843 | if (kc.iframeVersion) { 844 | src = src + '?version=' + kc.iframeVersion; 845 | } 846 | 847 | iframe.setAttribute('src', src ); 848 | iframe.setAttribute('title', 'keycloak-session-iframe' ); 849 | iframe.style.display = 'none'; 850 | document.body.appendChild(iframe); 851 | 852 | var messageCallback = function(event) { 853 | if ((event.origin !== loginIframe.iframeOrigin) || (loginIframe.iframe.contentWindow !== event.source)) { 854 | return; 855 | } 856 | 857 | if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) { 858 | return; 859 | } 860 | 861 | 862 | if (event.data != 'unchanged') { 863 | kc.clearToken(); 864 | } 865 | 866 | var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length); 867 | 868 | for (var i = callbacks.length - 1; i >= 0; --i) { 869 | var promise = callbacks[i]; 870 | if (event.data == 'unchanged') { 871 | promise.setSuccess(); 872 | } else { 873 | promise.setError(); 874 | } 875 | } 876 | }; 877 | 878 | window.addEventListener('message', messageCallback, false); 879 | 880 | var check = function() { 881 | checkLoginIframe(); 882 | if (kc.token) { 883 | setTimeout(check, loginIframe.interval * 1000); 884 | } 885 | }; 886 | 887 | return promise.promise; 888 | } 889 | 890 | function checkLoginIframe() { 891 | var promise = createPromise(); 892 | 893 | if (loginIframe.iframe && loginIframe.iframeOrigin ) { 894 | var msg = kc.clientId + ' ' + kc.sessionId; 895 | loginIframe.callbackList.push(promise); 896 | var origin = loginIframe.iframeOrigin; 897 | if (loginIframe.callbackList.length == 1) { 898 | loginIframe.iframe.contentWindow.postMessage(msg, origin); 899 | } 900 | } else { 901 | promise.setSuccess(); 902 | } 903 | 904 | return promise.promise; 905 | } 906 | 907 | function loadAdapter(type) { 908 | if (!type || type == 'default') { 909 | return { 910 | login: function(options) { 911 | window.location.href = kc.createLoginUrl(options); 912 | return createPromise().promise; 913 | }, 914 | 915 | logout: function(options) { 916 | window.location.href = kc.createLogoutUrl(options); 917 | return createPromise().promise; 918 | }, 919 | 920 | register: function(options) { 921 | window.location.href = kc.createRegisterUrl(options); 922 | return createPromise().promise; 923 | }, 924 | 925 | accountManagement : function() { 926 | window.location.href = kc.createAccountUrl(); 927 | return createPromise().promise; 928 | }, 929 | 930 | redirectUri: function(options, encodeHash) { 931 | if (arguments.length == 1) { 932 | encodeHash = true; 933 | } 934 | 935 | if (options && options.redirectUri) { 936 | return options.redirectUri; 937 | } else if (kc.redirectUri) { 938 | return kc.redirectUri; 939 | } else { 940 | var redirectUri = location.href; 941 | if (location.hash && encodeHash) { 942 | redirectUri = redirectUri.substring(0, location.href.indexOf('#')); 943 | redirectUri += (redirectUri.indexOf('?') == -1 ? '?' : '&') + 'redirect_fragment=' + encodeURIComponent(location.hash.substring(1)); 944 | } 945 | return redirectUri; 946 | } 947 | } 948 | }; 949 | } 950 | 951 | if (type == 'cordova') { 952 | loginIframe.enable = false; 953 | var cordovaOpenWindowWrapper = function(loginUrl, target, options) { 954 | if (window.cordova && window.cordova.InAppBrowser) { 955 | // Use inappbrowser for IOS and Android if available 956 | return window.cordova.InAppBrowser.open(loginUrl, target, options); 957 | } else { 958 | return window.open(loginUrl, target, options); 959 | } 960 | }; 961 | return { 962 | login: function(options) { 963 | var promise = createPromise(); 964 | 965 | var o = 'location=no'; 966 | if (options && options.prompt == 'none') { 967 | o += ',hidden=yes'; 968 | } 969 | 970 | var loginUrl = kc.createLoginUrl(options); 971 | var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', o); 972 | var completed = false; 973 | 974 | ref.addEventListener('loadstart', function(event) { 975 | if (event.url.indexOf('http://localhost') == 0) { 976 | var callback = parseCallback(event.url); 977 | processCallback(callback, promise); 978 | ref.close(); 979 | completed = true; 980 | } 981 | }); 982 | 983 | ref.addEventListener('loaderror', function(event) { 984 | if (!completed) { 985 | if (event.url.indexOf('http://localhost') == 0) { 986 | var callback = parseCallback(event.url); 987 | processCallback(callback, promise); 988 | ref.close(); 989 | completed = true; 990 | } else { 991 | promise.setError(); 992 | ref.close(); 993 | } 994 | } 995 | }); 996 | 997 | return promise.promise; 998 | }, 999 | 1000 | logout: function(options) { 1001 | var promise = createPromise(); 1002 | 1003 | var logoutUrl = kc.createLogoutUrl(options); 1004 | var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes'); 1005 | 1006 | var error; 1007 | 1008 | ref.addEventListener('loadstart', function(event) { 1009 | if (event.url.indexOf('http://localhost') == 0) { 1010 | ref.close(); 1011 | } 1012 | }); 1013 | 1014 | ref.addEventListener('loaderror', function(event) { 1015 | if (event.url.indexOf('http://localhost') == 0) { 1016 | ref.close(); 1017 | } else { 1018 | error = true; 1019 | ref.close(); 1020 | } 1021 | }); 1022 | 1023 | ref.addEventListener('exit', function(event) { 1024 | if (error) { 1025 | promise.setError(); 1026 | } else { 1027 | kc.clearToken(); 1028 | promise.setSuccess(); 1029 | } 1030 | }); 1031 | 1032 | return promise.promise; 1033 | }, 1034 | 1035 | register : function() { 1036 | var registerUrl = kc.createRegisterUrl(); 1037 | var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', 'location=no'); 1038 | ref.addEventListener('loadstart', function(event) { 1039 | if (event.url.indexOf('http://localhost') == 0) { 1040 | ref.close(); 1041 | } 1042 | }); 1043 | }, 1044 | 1045 | accountManagement : function() { 1046 | var accountUrl = kc.createAccountUrl(); 1047 | var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no'); 1048 | ref.addEventListener('loadstart', function(event) { 1049 | if (event.url.indexOf('http://localhost') == 0) { 1050 | ref.close(); 1051 | } 1052 | }); 1053 | }, 1054 | 1055 | redirectUri: function(options) { 1056 | return 'http://localhost'; 1057 | } 1058 | } 1059 | } 1060 | 1061 | throw 'invalid adapter type: ' + type; 1062 | } 1063 | 1064 | var LocalStorage = function() { 1065 | if (!(this instanceof LocalStorage)) { 1066 | return new LocalStorage(); 1067 | } 1068 | 1069 | localStorage.setItem('kc-test', 'test'); 1070 | localStorage.removeItem('kc-test'); 1071 | 1072 | var cs = this; 1073 | 1074 | function clearExpired() { 1075 | var time = new Date().getTime(); 1076 | for (var i = 0; i < localStorage.length; i++) { 1077 | var key = localStorage.key(i); 1078 | if (key && key.indexOf('kc-callback-') == 0) { 1079 | var value = localStorage.getItem(key); 1080 | if (value) { 1081 | try { 1082 | var expires = JSON.parse(value).expires; 1083 | if (!expires || expires < time) { 1084 | localStorage.removeItem(key); 1085 | } 1086 | } catch (err) { 1087 | localStorage.removeItem(key); 1088 | } 1089 | } 1090 | } 1091 | } 1092 | } 1093 | 1094 | cs.get = function(state) { 1095 | if (!state) { 1096 | return; 1097 | } 1098 | 1099 | var key = 'kc-callback-' + state; 1100 | var value = localStorage.getItem(key); 1101 | if (value) { 1102 | localStorage.removeItem(key); 1103 | value = JSON.parse(value); 1104 | } 1105 | 1106 | clearExpired(); 1107 | return value; 1108 | }; 1109 | 1110 | cs.add = function(state) { 1111 | clearExpired(); 1112 | 1113 | var key = 'kc-callback-' + state.state; 1114 | state.expires = new Date().getTime() + (60 * 60 * 1000); 1115 | localStorage.setItem(key, JSON.stringify(state)); 1116 | }; 1117 | }; 1118 | 1119 | var CookieStorage = function() { 1120 | if (!(this instanceof CookieStorage)) { 1121 | return new CookieStorage(); 1122 | } 1123 | 1124 | var cs = this; 1125 | 1126 | cs.get = function(state) { 1127 | if (!state) { 1128 | return; 1129 | } 1130 | 1131 | var value = getCookie('kc-callback-' + state); 1132 | setCookie('kc-callback-' + state, '', cookieExpiration(-100)); 1133 | if (value) { 1134 | return JSON.parse(value); 1135 | } 1136 | }; 1137 | 1138 | cs.add = function(state) { 1139 | setCookie('kc-callback-' + state.state, JSON.stringify(state), cookieExpiration(60)); 1140 | }; 1141 | 1142 | cs.removeItem = function(key) { 1143 | setCookie(key, '', cookieExpiration(-100)); 1144 | }; 1145 | 1146 | var cookieExpiration = function (minutes) { 1147 | var exp = new Date(); 1148 | exp.setTime(exp.getTime() + (minutes*60*1000)); 1149 | return exp; 1150 | }; 1151 | 1152 | var getCookie = function (key) { 1153 | var name = key + '='; 1154 | var ca = document.cookie.split(';'); 1155 | for (var i = 0; i < ca.length; i++) { 1156 | var c = ca[i]; 1157 | while (c.charAt(0) == ' ') { 1158 | c = c.substring(1); 1159 | } 1160 | if (c.indexOf(name) == 0) { 1161 | return c.substring(name.length, c.length); 1162 | } 1163 | } 1164 | return ''; 1165 | }; 1166 | 1167 | var setCookie = function (key, value, expirationDate) { 1168 | var cookie = key + '=' + value + '; ' 1169 | + 'expires=' + expirationDate.toUTCString() + '; '; 1170 | document.cookie = cookie; 1171 | } 1172 | }; 1173 | 1174 | function createCallbackStorage() { 1175 | try { 1176 | return new LocalStorage(); 1177 | } catch (err) { 1178 | } 1179 | 1180 | return new CookieStorage(); 1181 | } 1182 | 1183 | var CallbackParser = function(uriToParse, responseMode) { 1184 | if (!(this instanceof CallbackParser)) { 1185 | return new CallbackParser(uriToParse, responseMode); 1186 | } 1187 | var parser = this; 1188 | 1189 | var initialParse = function() { 1190 | var baseUri = null; 1191 | var queryString = null; 1192 | var fragmentString = null; 1193 | 1194 | var questionMarkIndex = uriToParse.indexOf("?"); 1195 | var fragmentIndex = uriToParse.indexOf("#", questionMarkIndex + 1); 1196 | if (questionMarkIndex == -1 && fragmentIndex == -1) { 1197 | baseUri = uriToParse; 1198 | } else if (questionMarkIndex != -1) { 1199 | baseUri = uriToParse.substring(0, questionMarkIndex); 1200 | queryString = uriToParse.substring(questionMarkIndex + 1); 1201 | if (fragmentIndex != -1) { 1202 | fragmentIndex = queryString.indexOf("#"); 1203 | fragmentString = queryString.substring(fragmentIndex + 1); 1204 | queryString = queryString.substring(0, fragmentIndex); 1205 | } 1206 | } else { 1207 | baseUri = uriToParse.substring(0, fragmentIndex); 1208 | fragmentString = uriToParse.substring(fragmentIndex + 1); 1209 | } 1210 | 1211 | return { baseUri: baseUri, queryString: queryString, fragmentString: fragmentString }; 1212 | } 1213 | 1214 | var parseParams = function(paramString) { 1215 | var result = {}; 1216 | var params = paramString.split('&'); 1217 | for (var i = 0; i < params.length; i++) { 1218 | var p = params[i].split('='); 1219 | var paramName = decodeURIComponent(p[0]); 1220 | var paramValue = decodeURIComponent(p[1]); 1221 | result[paramName] = paramValue; 1222 | } 1223 | return result; 1224 | } 1225 | 1226 | var handleQueryParam = function(paramName, paramValue, oauth) { 1227 | var supportedOAuthParams = [ 'code', 'state', 'error', 'error_description' ]; 1228 | 1229 | for (var i = 0 ; i< supportedOAuthParams.length ; i++) { 1230 | if (paramName === supportedOAuthParams[i]) { 1231 | oauth[paramName] = paramValue; 1232 | return true; 1233 | } 1234 | } 1235 | return false; 1236 | } 1237 | 1238 | 1239 | parser.parseUri = function() { 1240 | var parsedUri = initialParse(); 1241 | 1242 | var queryParams = {}; 1243 | if (parsedUri.queryString) { 1244 | queryParams = parseParams(parsedUri.queryString); 1245 | } 1246 | 1247 | var oauth = { newUrl: parsedUri.baseUri }; 1248 | for (var param in queryParams) { 1249 | switch (param) { 1250 | case 'redirect_fragment': 1251 | oauth.fragment = queryParams[param]; 1252 | break; 1253 | default: 1254 | if (responseMode != 'query' || !handleQueryParam(param, queryParams[param], oauth)) { 1255 | oauth.newUrl += (oauth.newUrl.indexOf('?') == -1 ? '?' : '&') + param + '=' + encodeURIComponent(queryParams[param]); 1256 | } 1257 | break; 1258 | } 1259 | } 1260 | 1261 | if (responseMode === 'fragment') { 1262 | var fragmentParams = {}; 1263 | if (parsedUri.fragmentString) { 1264 | fragmentParams = parseParams(parsedUri.fragmentString); 1265 | } 1266 | for (var param in fragmentParams) { 1267 | oauth[param] = fragmentParams[param]; 1268 | } 1269 | } 1270 | 1271 | return oauth; 1272 | } 1273 | } 1274 | 1275 | } 1276 | 1277 | if ( typeof module === "object" && module && typeof module.exports === "object" ) { 1278 | module.exports = Keycloak; 1279 | } else { 1280 | window.Keycloak = Keycloak; 1281 | 1282 | if ( typeof define === "function" && define.amd ) { 1283 | define( "keycloak", [], function () { return Keycloak; } ); 1284 | } 1285 | } 1286 | })( window ); 1287 | --------------------------------------------------------------------------------