├── .gitignore ├── src ├── capacitor │ ├── index.ts │ ├── capacitor-browser.ts │ └── capcitor-storage.ts ├── auth-session.ts ├── index.ts ├── auth-configuration.ts ├── cordova │ ├── index.ts │ ├── cordova-requestor.ts │ ├── cordova-browser.ts │ └── cordova-secure-storage.ts ├── auth-browser.ts ├── user-info-request-handler.ts ├── end-session-request.ts ├── end-session-request-handler.ts ├── implicit-request.ts ├── auth-action.ts ├── authorization-request-handler.ts ├── implicit-request-handler.ts └── ionic-auth.ts ├── tslint.json ├── tsconfig.json ├── LICENSE ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib -------------------------------------------------------------------------------- /src/capacitor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './capacitor-browser'; 2 | export * from './capcitor-storage'; 3 | 4 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-duplicate-variable": true, 4 | "no-unused-variable": [ 5 | true 6 | ] 7 | }, 8 | "rulesDirectory": [ 9 | "node_modules/tslint-eslint-rules/dist/rules" 10 | ] 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "target": "es5", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": true 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "**/__tests__/*"], 12 | } -------------------------------------------------------------------------------- /src/auth-session.ts: -------------------------------------------------------------------------------- 1 | import { TokenResponse } from "@openid/appauth"; 2 | 3 | export interface IAuthSession { 4 | isAuthenticated : boolean; 5 | token?: TokenResponse; 6 | user?: any; 7 | } 8 | 9 | export class DefaultAuthSession implements IAuthSession { 10 | isAuthenticated: boolean = false; 11 | token?: TokenResponse | undefined = undefined; 12 | user?: any = undefined; 13 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-browser' 2 | export * from './auth-configuration' 3 | export * from './authorization-request-handler' 4 | export * from './end-session-request' 5 | export * from './end-session-request-handler' 6 | export * from './ionic-auth' 7 | export * from './auth-action' 8 | export * from './user-info-request-handler' 9 | export * from './implicit-request' 10 | export * from './implicit-request-handler' 11 | -------------------------------------------------------------------------------- /src/auth-configuration.ts: -------------------------------------------------------------------------------- 1 | import { StringMap } from "@openid/appauth"; 2 | 3 | export enum AuthenticationType { 4 | Token = "token", 5 | AuthorizationCode = "code", 6 | IdToken = "id_token" 7 | } 8 | 9 | export interface IAuthConfig { 10 | identity_client: string, 11 | identity_server: string, 12 | redirect_url: string, 13 | end_session_redirect_url: string, 14 | scopes: string, 15 | usePkce : boolean, 16 | response_type?: string, 17 | auth_extras?: StringMap 18 | } -------------------------------------------------------------------------------- /src/cordova/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cordova-browser'; 2 | export * from './cordova-secure-storage'; 3 | export * from './cordova-requestor'; 4 | 5 | export class CordovaDocument { 6 | 7 | public static ready(f ?: Function) : Promise{ 8 | return new Promise(resolve => { 9 | document.addEventListener('deviceready', () => { 10 | if(f != undefined){ 11 | f(); 12 | } 13 | resolve(); 14 | }); 15 | }) 16 | } 17 | } -------------------------------------------------------------------------------- /src/auth-browser.ts: -------------------------------------------------------------------------------- 1 | export abstract class Browser { 2 | protected onCloseFunction : Function = () => {}; 3 | 4 | abstract showWindow(url : string, callbackUrl?: string) : string | undefined | Promise; 5 | abstract closeWindow(): void | Promise; 6 | 7 | addCloseBrowserEvent(closeBrowserEvent : Function){ 8 | this.onCloseFunction = closeBrowserEvent; 9 | } 10 | } 11 | 12 | export class DefaultBrowser extends Browser { 13 | public showWindow(url: string) : string | undefined { 14 | window.open(url, "_self"); 15 | return; 16 | } 17 | 18 | public closeWindow(): void { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/capacitor/capacitor-browser.ts: -------------------------------------------------------------------------------- 1 | import { Browser } from '../auth-browser'; 2 | import { Plugins, BrowserOpenOptions } from '@capacitor/core'; 3 | 4 | export class CapacitorBrowser extends Browser { 5 | public closeWindow(): void | Promise { 6 | if(!Plugins.Browser) 7 | throw new Error("Capacitor Browser Is Undefined!"); 8 | 9 | Plugins.Browser.close(); 10 | } 11 | 12 | 13 | public async showWindow(url: string, callbackUrl?: string): Promise { 14 | let options : BrowserOpenOptions = { 15 | url : url, 16 | windowName: '_self' 17 | }; 18 | 19 | if(!Plugins.Browser) 20 | throw new Error("Capacitor Browser Is Undefined!"); 21 | 22 | Plugins.Browser.open(options); 23 | 24 | return ; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/user-info-request-handler.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizationServiceConfiguration, TokenResponse, Requestor } from '@openid/appauth'; 2 | 3 | export interface UserInfoHandler { 4 | performUserInfoRequest(configuration: AuthorizationServiceConfiguration, token : TokenResponse): Promise 5 | } 6 | 7 | export class IonicUserInfoHandler implements UserInfoHandler { 8 | 9 | constructor( 10 | private requestor : Requestor 11 | ) {} 12 | 13 | public async performUserInfoRequest(configuration: AuthorizationServiceConfiguration, token : TokenResponse): Promise { 14 | let settings : JQueryAjaxSettings = { 15 | url : configuration.userInfoEndpoint, 16 | dataType: 'json', 17 | method: 'GET', 18 | headers : { 19 | "Authorization": `${(token.tokenType == 'bearer') ? 'Bearer' : token.tokenType} ${token.accessToken}`, 20 | "Content-Type": "application/json" 21 | } 22 | } 23 | 24 | return this.requestor.xhr(settings); 25 | } 26 | } -------------------------------------------------------------------------------- /src/capacitor/capcitor-storage.ts: -------------------------------------------------------------------------------- 1 | import { StorageBackend } from '@openid/appauth'; 2 | import { Plugins } from '@capacitor/core'; 3 | 4 | export class CapacitorStorage implements StorageBackend { 5 | 6 | async getItem(name: string): Promise { 7 | if(!Plugins.Storage) 8 | throw new Error("Capacitor Storage Is Undefined!"); 9 | 10 | let returned = await Plugins.Storage.get({ key: name }); 11 | return returned.value; 12 | } 13 | 14 | removeItem(name: string): Promise { 15 | if(!Plugins.Storage) 16 | throw new Error("Capacitor Storage Is Undefined!"); 17 | 18 | return Plugins.Storage.remove({ key: name }); 19 | } 20 | 21 | clear(): Promise { 22 | if(!Plugins.Storage) 23 | throw new Error("Capacitor Storage Is Undefined!"); 24 | 25 | return Plugins.Storage.clear(); 26 | } 27 | 28 | setItem(name: string, value: string): Promise { 29 | if(!Plugins.Storage) 30 | throw new Error("Capacitor Storage Is Undefined!"); 31 | 32 | return Plugins.Storage.set({ key: name, value: value }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/end-session-request.ts: -------------------------------------------------------------------------------- 1 | import { Crypto, DefaultCrypto } from '@openid/appauth/built/crypto_utils'; 2 | 3 | export interface EndSessionRequestJson { 4 | idTokenHint: string; 5 | postLogoutRedirectURI: string; 6 | state?: string; 7 | } 8 | 9 | const BYTES_LENGTH = 10; 10 | const newState = function(crypto: Crypto): string { 11 | return crypto.generateRandom(BYTES_LENGTH); 12 | }; 13 | 14 | 15 | export class EndSessionRequest { 16 | 17 | state: string; 18 | idTokenHint: string; 19 | postLogoutRedirectURI: string; 20 | 21 | constructor( 22 | request: EndSessionRequestJson, 23 | crypto : Crypto = new DefaultCrypto()) { 24 | this.state = request.state || newState(crypto); 25 | this.idTokenHint = request.idTokenHint; 26 | this.postLogoutRedirectURI = request.postLogoutRedirectURI; 27 | } 28 | 29 | toJson(): EndSessionRequestJson { 30 | let json: EndSessionRequestJson = {idTokenHint: this.idTokenHint, postLogoutRedirectURI : this.postLogoutRedirectURI }; 31 | 32 | if (this.state) { 33 | json['state'] = this.state; 34 | } 35 | 36 | return json; 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wi3land 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | 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 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ionic App Auth 2 | Ionic App Auth is a implementation of the [AppAuth-JS](https://github.com/openid/AppAuth-JS) for Ionic Users. 3 | It includes code extensions for core cordova plugins to run the Ionic app such as [Advanced HTTP](https://github.com/silkimen/cordova-plugin-advanced-http) and [SafariViewController](https://github.com/EddyVerbruggen/cordova-plugin-safariviewcontroller). 4 | 5 | The cordova plugins are optional and can be replaced with Angular/React/Vue http handlers and/or Capacitor Plugins. 6 | This library is intended to be as flexiable with compatiablity as ionic V4 is attempting to be. 7 | 8 | ## Installation 9 | Run following command to install Ionic App Auth in your project. 10 | 11 | ```bash 12 | npm install ionic-appauth --save 13 | ``` 14 | 15 | ## Examples 16 | React/Capacitor : https://github.com/wi3land/ionic-appauth-react-demo - Still In Progress
17 | Angular/Cordova : https://github.com/wi3land/ionic-appauth-ng-demo
18 | Angular/Capacitor : https://github.com/wi3land/ionic-appauth-capacitor-demo 19 | 20 | ## Contributing 21 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 22 | 23 | ## License 24 | 25 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 26 | 27 | ## Acknowledgments 28 | 29 | * [AppAuth-JS](https://github.com/openid/AppAuth-JS) 30 | * [AppAuth-Ionic](https://github.com/Belicosus/AppAuth-Ionic) 31 | -------------------------------------------------------------------------------- /src/end-session-request-handler.ts: -------------------------------------------------------------------------------- 1 | import { EndSessionRequest } from './end-session-request'; 2 | import { AuthorizationServiceConfiguration, StringMap, BasicQueryStringUtils } from "@openid/appauth"; 3 | import { Browser } from './auth-browser' 4 | 5 | export interface EndSessionHandler { 6 | performEndSessionRequest(configuration: AuthorizationServiceConfiguration, request : EndSessionRequest): Promise; 7 | } 8 | 9 | export class IonicEndSessionHandler implements EndSessionHandler { 10 | 11 | constructor( 12 | private browser: Browser, 13 | private utils = new BasicQueryStringUtils() 14 | ) {} 15 | 16 | public async performEndSessionRequest(configuration: AuthorizationServiceConfiguration, request : EndSessionRequest): Promise { 17 | let url = this.buildRequestUrl(configuration, request); 18 | return this.browser.showWindow(url, request.postLogoutRedirectURI); 19 | } 20 | 21 | private buildRequestUrl(configuration: AuthorizationServiceConfiguration,request: EndSessionRequest) { 22 | let requestMap: StringMap = { 23 | 'id_token_hint': request.idTokenHint, 24 | 'post_logout_redirect_uri': request.postLogoutRedirectURI, 25 | 'state': request.state, 26 | }; 27 | 28 | let query = this.utils.stringify(requestMap); 29 | let baseUrl = configuration.endSessionEndpoint; 30 | let url = `${baseUrl}?${query}`; 31 | return url; 32 | } 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-appauth", 3 | "version": "0.4.4", 4 | "description": "Intergration for OpenId/AppAuth-JS into Ionic V3/4", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc", 9 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", 10 | "lint": "tslint -p tsconfig.json" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:wi3land/ionic-appauth.git" 15 | }, 16 | "homepage": "https://github.com/wi3land/ionic-appauth#readme", 17 | "keywords": [ 18 | "OAuth", 19 | "AppAuth", 20 | "JavaScript", 21 | "Ionic", 22 | "Cordova", 23 | "OpenId", 24 | "Angular", 25 | "React" 26 | ], 27 | "author": { 28 | "name": "wi3land" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/wi3land/ionic-appauth/issues" 32 | }, 33 | "license": "MIT", 34 | "devDependencies": { 35 | "tslint": "^5.14.0", 36 | "tslint-eslint-rules": "^5.4.0", 37 | "typescript": "^3.3.4000", 38 | "rxjs": "^6.3.0" 39 | }, 40 | "files": [ 41 | "lib/**/*" 42 | ], 43 | "types": "lib/index.d.ts", 44 | "dependencies": { 45 | "@openid/appauth": "^1.2.2" 46 | }, 47 | "peerDependencies": { 48 | "rxjs": "^6.3.0" 49 | }, 50 | "optionalDependencies": { 51 | "@capacitor/core": "^1.0.0-beta.19", 52 | "@ionic-native/core": "^5.3.0", 53 | "@ionic-native/http": "^5.3.0", 54 | "@ionic-native/in-app-browser": "^5.3.0", 55 | "@ionic-native/safari-view-controller": "^5.3.0", 56 | "@ionic-native/secure-storage": "^5.3.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/cordova/cordova-requestor.ts: -------------------------------------------------------------------------------- 1 | import { CordovaDocument } from './index'; 2 | import { Requestor } from '@openid/appauth'; 3 | import { HTTP, HTTPResponse } from '@ionic-native/http' 4 | 5 | export interface XhrSettings { 6 | url: string, 7 | dataType?: string, 8 | method?: "GET" | "POST" | "PUT" | "DELETE", 9 | data?: any, 10 | headers?: any// {key : string, value: any} 11 | } 12 | 13 | // REQUIRES CORDOVA PLUGINS 14 | // cordova-plugin-advanced-http 15 | export class CordovaRequestor extends Requestor { 16 | 17 | constructor(){ 18 | CordovaDocument.ready(() => HTTP.setDataSerializer('utf8')); 19 | super(); 20 | } 21 | 22 | public async xhr(settings: XhrSettings) : Promise { 23 | if(!settings.method) 24 | settings.method = "GET"; 25 | 26 | await CordovaDocument.ready(); 27 | 28 | switch(settings.method){ 29 | case "GET": 30 | return this.get(settings.url, settings.headers); 31 | case "POST": 32 | return this.post(settings.url, settings.data, settings.headers); 33 | case "PUT": 34 | return this.put(settings.url, settings.data, settings.headers); 35 | case "DELETE": 36 | return this.delete(settings.url, settings.headers); 37 | } 38 | } 39 | 40 | private async get(url : string, headers: any) { 41 | return HTTP.get(url, undefined, headers).then((response: HTTPResponse) => JSON.parse(response.data) as T); 42 | } 43 | 44 | private async post(url : string, data: any, headers: any){ 45 | return HTTP.post(url, data, headers).then((response: HTTPResponse) => JSON.parse(response.data) as T); 46 | } 47 | 48 | private async put(url : string, data: any, headers: any){ 49 | return HTTP.put(url, data, headers).then((response: HTTPResponse) => JSON.parse(response.data) as T); 50 | } 51 | 52 | private async delete(url : string, headers: any){ 53 | return HTTP.delete(url,undefined,headers).then((response: HTTPResponse) => JSON.parse(response.data) as T); 54 | } 55 | } -------------------------------------------------------------------------------- /src/cordova/cordova-browser.ts: -------------------------------------------------------------------------------- 1 | import { CordovaDocument } from './index'; 2 | import { Browser } from '../auth-browser' 3 | import { SafariViewController } from '@ionic-native/safari-view-controller' 4 | import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser' 5 | 6 | // REQUIRES CORDOVA PLUGINS 7 | // cordova-plugin-safariviewcontroller 8 | // cordova-plugin-customurlscheme 9 | // cordova-plugin-inappbrowser FROM https://github.com/Onegini/cordova-plugin-inappbrowser.git 10 | export class CordovaBrowser extends Browser { 11 | 12 | private inAppBrowserRef : InAppBrowserObject | undefined; 13 | 14 | public async closeWindow(): Promise { 15 | await CordovaDocument.ready(); 16 | 17 | if(await SafariViewController.isAvailable()){ 18 | SafariViewController.hide(); 19 | }else{ 20 | if(this.inAppBrowserRef != undefined) 21 | this.inAppBrowserRef.close(); 22 | } 23 | } 24 | 25 | public async showWindow(url: string) : Promise { 26 | await CordovaDocument.ready(); 27 | 28 | if(await SafariViewController.isAvailable()){ 29 | let optionSafari: any = { 30 | url: url, 31 | showDefaultShareMenuItem: false, 32 | toolbarColor: '#ffffff' 33 | } 34 | SafariViewController.show(optionSafari).subscribe((result : any) => { 35 | if (result.event === 'closed') { 36 | this.onCloseFunction(); 37 | } 38 | }); 39 | }else{ 40 | let options: any = { 41 | location: 'no', 42 | zoom: 'no', 43 | clearcache: 'yes', 44 | clearsessioncache: 'yes', 45 | } 46 | 47 | this.inAppBrowserRef = InAppBrowser.create(url, '_self', options); 48 | 49 | if(this.inAppBrowserRef != undefined) 50 | this.inAppBrowserRef.on('exit').subscribe((event) => this.onCloseFunction); 51 | 52 | } 53 | return; 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /src/implicit-request.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultCrypto, 3 | Crypto, 4 | StringMap, 5 | } from "@openid/appauth"; 6 | 7 | export enum ImplicitResponseType { 8 | Token = "token", 9 | IdToken = "id_token", 10 | IdTokenToken = "id_token token" 11 | } 12 | 13 | /** 14 | * Represents an ImplicitRequest as JSON. 15 | */ 16 | export interface ImplicitRequestJson { 17 | response_type: string; 18 | client_id: string; 19 | redirect_uri: string; 20 | scope: string; 21 | state?: string; 22 | extras?: StringMap; 23 | nonce?: string; 24 | } 25 | 26 | /** 27 | * Generates a cryptographically random new state. Useful for CSRF protection. 28 | */ 29 | const SIZE = 10; // 10 bytes 30 | const newState = function(crypto: Crypto): string { 31 | return crypto.generateRandom(SIZE); 32 | }; 33 | 34 | /** 35 | * Represents the AuthorizationRequest. 36 | * For more information look at 37 | * https://tools.ietf.org/html/rfc6749#section-4.1.1 38 | */ 39 | export class ImplicitRequest { 40 | static RESPONSE_TYPE_TOKEN = 'token'; 41 | static RESPONSE_TYPE_CODE = 'code'; 42 | 43 | // NOTE: 44 | // Both redirect_uri and state are actually optional. 45 | // However AppAuth is more opionionated, and requires you to use both. 46 | 47 | clientId: string; 48 | redirectUri: string; 49 | scope: string; 50 | responseType: string; 51 | state: string; 52 | nonce: string; 53 | extras?: StringMap; 54 | /** 55 | * Constructs a new ImplicitRequest. 56 | * Use a `undefined` value for the `state` parameter, to generate a random 57 | * state for CSRF protection. 58 | */ 59 | constructor( 60 | request: ImplicitRequestJson, 61 | private crypto: Crypto = new DefaultCrypto()) { 62 | this.clientId = request.client_id; 63 | this.redirectUri = request.redirect_uri; 64 | this.scope = request.scope; 65 | this.responseType = request.response_type || ImplicitResponseType.IdTokenToken; 66 | this.state = request.state || newState(crypto); 67 | this.nonce = newState(crypto); 68 | this.extras = request.extras; 69 | } 70 | 71 | 72 | /** 73 | * Serializes the ImplicitRequest to a JavaScript Object. 74 | */ 75 | toJson(): ImplicitRequestJson { 76 | return { 77 | response_type: this.responseType, 78 | client_id: this.clientId, 79 | redirect_uri: this.redirectUri, 80 | scope: this.scope, 81 | state: this.state, 82 | extras: this.extras, 83 | nonce: this.nonce 84 | }; 85 | } 86 | } -------------------------------------------------------------------------------- /src/auth-action.ts: -------------------------------------------------------------------------------- 1 | import { IAuthAction } from './auth-action'; 2 | import { TokenResponse } from '@openid/appauth'; 3 | 4 | export enum AuthActions { 5 | Default = "Default", 6 | SignInSuccess = "Sign In Success", 7 | SignInFailed = "Sign In Failed", 8 | SignOutSuccess = "Sign Out Success", 9 | SignOutFailed = "Sign Out Failed", 10 | RefreshSuccess = "Refresh Success", 11 | RefreshFailed = "Refesh Failed", 12 | AutoSignInFailed = "Auto Sign In Failed", 13 | AutoSignInSuccess = "Auto Sign In Success", 14 | } 15 | 16 | export interface IAuthAction { 17 | action : AuthActions, 18 | tokenResponse ?: TokenResponse 19 | error ?: string; 20 | } 21 | 22 | export class AuthActionBuilder { 23 | public static Default() : IAuthAction{ 24 | return { 25 | action : AuthActions.Default, 26 | } 27 | } 28 | 29 | public static SignOutSuccess() : IAuthAction{ 30 | return { 31 | action : AuthActions.SignOutSuccess, 32 | } 33 | } 34 | 35 | public static SignOutFailed(error : any) : IAuthAction{ 36 | return { 37 | action : AuthActions.SignOutFailed, 38 | error : JSON.stringify(error) 39 | } 40 | } 41 | 42 | public static RefreshSuccess(token : TokenResponse) : IAuthAction{ 43 | return { 44 | action : AuthActions.RefreshSuccess, 45 | tokenResponse : token 46 | } 47 | } 48 | 49 | public static RefreshFailed(error : any) : IAuthAction{ 50 | return { 51 | action : AuthActions.RefreshFailed, 52 | error : JSON.stringify(error) 53 | } 54 | } 55 | 56 | public static SignInSuccess(token : TokenResponse) : IAuthAction{ 57 | return { 58 | action : AuthActions.SignInSuccess, 59 | tokenResponse : token 60 | } 61 | } 62 | 63 | public static SignInFailed(error : any) : IAuthAction{ 64 | return { 65 | action : AuthActions.AutoSignInFailed, 66 | error : JSON.stringify(error) 67 | } 68 | } 69 | 70 | public static AutoSignInSuccess(token : TokenResponse) : IAuthAction{ 71 | return { 72 | action : AuthActions.AutoSignInSuccess, 73 | tokenResponse : token 74 | } 75 | } 76 | 77 | public static AutoSignInFailed(error : any) : IAuthAction{ 78 | return { 79 | action : AuthActions.AutoSignInFailed, 80 | error : JSON.stringify(error) 81 | } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/cordova/cordova-secure-storage.ts: -------------------------------------------------------------------------------- 1 | import { CordovaDocument } from './index'; 2 | import { StorageBackend } from '@openid/appauth'; 3 | import { SecureStorage, SecureStorageObject } from '@ionic-native/secure-storage' 4 | 5 | // REQUIRES CORDOVA PLUGINS 6 | // cordova-plugin-secure-storage 7 | export class CordovaSecureStorage extends StorageBackend { 8 | 9 | private localData: any = {}; 10 | private KEYSTORE: string = "SecretStore"; 11 | 12 | public async SecureStorageExists() : Promise{ 13 | await CordovaDocument.ready(); 14 | return SecureStorage.create(this.KEYSTORE).then(() => true, () => false); 15 | } 16 | 17 | public async hasRecord(store: SecureStorageObject, key: string){ 18 | let keys : string[] = await store.keys(); 19 | return (keys.indexOf(key) > -1) 20 | } 21 | 22 | public async getItem(name: string): Promise { 23 | await CordovaDocument.ready(); 24 | return SecureStorage.create(this.KEYSTORE).then((store) => { 25 | return store.get(name); 26 | }) 27 | .catch(() => { 28 | return this.getTemp(name); 29 | }); 30 | } 31 | 32 | public async removeItem(name: string): Promise { 33 | await CordovaDocument.ready(); 34 | return SecureStorage.create(this.KEYSTORE).then((store) => { 35 | store.remove(name); 36 | }) 37 | .catch(() => { 38 | this.removeTemp(name); 39 | }); 40 | } 41 | 42 | public async setItem(name: string, value: string): Promise { 43 | await CordovaDocument.ready(); 44 | return SecureStorage.create(this.KEYSTORE).then((store) => { 45 | store.set(name,value); 46 | }) 47 | .catch(() => { 48 | this.setTemp(name, value); 49 | }); 50 | } 51 | 52 | public async clear(): Promise { 53 | await CordovaDocument.ready(); 54 | return SecureStorage.create(this.KEYSTORE).then((store) => { 55 | store.clear(); 56 | }) 57 | .catch(() => { 58 | this.clearTemp(); 59 | }); 60 | } 61 | 62 | private getTemp(key: string) : string | null { 63 | if (this.localData[key]) 64 | return this.localData[key]; 65 | else 66 | return null; 67 | } 68 | 69 | private setTemp(key: string, data: string) : void { 70 | this.localData[key] = data; 71 | } 72 | 73 | private removeTemp(key: string) : void { 74 | if (this.localData[key]){ 75 | delete this.localData[key] 76 | } 77 | } 78 | 79 | private clearTemp() : void { 80 | this.localData = {}; 81 | } 82 | } -------------------------------------------------------------------------------- /src/authorization-request-handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthorizationRequestHandler, 3 | AuthorizationRequest, 4 | AuthorizationServiceConfiguration, 5 | AuthorizationRequestResponse, 6 | BasicQueryStringUtils, 7 | AuthorizationResponse, 8 | AuthorizationError, 9 | AuthorizationErrorJson, 10 | AuthorizationResponseJson, 11 | DefaultCrypto, 12 | StorageBackend, 13 | StringMap 14 | } from "@openid/appauth"; 15 | 16 | import { Browser } from "./auth-browser"; 17 | 18 | /** key for authorization request. */ 19 | const authorizationRequestKey = 20 | (handle: string) => { 21 | return `${handle}_appauth_authorization_request`; 22 | } 23 | 24 | /** key in local storage which represents the current authorization request. */ 25 | const AUTHORIZATION_REQUEST_HANDLE_KEY = 'appauth_current_authorization_request'; 26 | export const AUTHORIZATION_RESPONSE_KEY = "auth_response"; 27 | 28 | export class IonicAuthorizationRequestHandler extends AuthorizationRequestHandler { 29 | 30 | constructor( 31 | private browser : Browser, 32 | private storage : StorageBackend, 33 | utils = new BasicQueryStringUtils(), 34 | private generateRandom = new DefaultCrypto(), 35 | ) { 36 | super(utils, generateRandom); 37 | } 38 | 39 | public async performAuthorizationRequest(configuration: AuthorizationServiceConfiguration, request: AuthorizationRequest) : Promise { 40 | let handle = this.generateRandom.generateRandom(10); 41 | this.storage.setItem(AUTHORIZATION_REQUEST_HANDLE_KEY, handle); 42 | this.storage.setItem(authorizationRequestKey(handle), JSON.stringify(await request.toJson())); 43 | let url = this.buildRequestUrl(configuration, request); 44 | let returnedUrl : string | undefined = await this.browser.showWindow(url, request.redirectUri); 45 | 46 | //callback may come from showWindow or via another method 47 | if(returnedUrl != undefined){ 48 | await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, url); 49 | this.completeAuthorizationRequest(); 50 | } 51 | } 52 | 53 | protected async completeAuthorizationRequest(): Promise { 54 | let handle = await this.storage.getItem(AUTHORIZATION_REQUEST_HANDLE_KEY); 55 | 56 | if (!handle) { 57 | throw new Error("Handle Not Available"); 58 | } 59 | 60 | let request : AuthorizationRequest = this.getAuthorizationRequest(await this.storage.getItem(authorizationRequestKey(handle))); 61 | let queryParams = this.getQueryParams(await this.storage.getItem(AUTHORIZATION_RESPONSE_KEY)); 62 | this.removeItemsFromStorage(handle); 63 | 64 | let state: string | undefined = queryParams['state']; 65 | let error: string | undefined = queryParams['error']; 66 | 67 | if (state !== request.state) { 68 | throw new Error("State Does Not Match"); 69 | } 70 | 71 | return { 72 | request: request, 73 | response: (!error) ? this.getAuthorizationResponse(queryParams) : undefined, 74 | error: (error) ? this.getAuthorizationError(queryParams) : undefined 75 | } 76 | 77 | } 78 | 79 | private getAuthorizationRequest(authRequest : string | null): AuthorizationRequest { 80 | if(authRequest == null){ 81 | throw new Error("No Auth Request Available"); 82 | } 83 | 84 | return new AuthorizationRequest(JSON.parse(authRequest)); 85 | } 86 | 87 | private getAuthorizationError(queryParams : StringMap): AuthorizationError { 88 | let authorizationErrorJSON : AuthorizationErrorJson = { 89 | error: queryParams['error'], 90 | error_description: queryParams['error_description'], 91 | error_uri: undefined, 92 | state: queryParams['state'] 93 | } 94 | return new AuthorizationError(authorizationErrorJSON); 95 | } 96 | 97 | private getAuthorizationResponse(queryParams : StringMap): AuthorizationResponse { 98 | let authorizationResponseJSON : AuthorizationResponseJson = { 99 | code: queryParams['code'], 100 | state: queryParams['state'] 101 | } 102 | return new AuthorizationResponse(authorizationResponseJSON); 103 | } 104 | 105 | private removeItemsFromStorage(handle : string) : void { 106 | this.storage.removeItem(AUTHORIZATION_REQUEST_HANDLE_KEY); 107 | this.storage.removeItem(authorizationRequestKey(handle)); 108 | this.storage.removeItem(AUTHORIZATION_RESPONSE_KEY); 109 | } 110 | 111 | private getQueryParams(authResponse: string | null) : StringMap { 112 | if(authResponse != null){ 113 | let parts = authResponse.split('?'); 114 | if (parts.length !== 2) throw new Error("Invalid auth response string"); 115 | let hash = parts[1]; 116 | return this.utils.parseQueryString(hash); 117 | }else{ 118 | return {}; 119 | } 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /src/implicit-request-handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthorizationServiceConfiguration, 3 | BasicQueryStringUtils, 4 | DefaultCrypto, 5 | Crypto, 6 | StorageBackend, 7 | StringMap, 8 | QueryStringUtils, 9 | TokenError, 10 | TokenResponse, 11 | TokenResponseJson, 12 | TokenErrorJson, 13 | ErrorType, 14 | TokenType, 15 | log, 16 | } from "@openid/appauth"; 17 | 18 | import { Browser } from "./auth-browser"; 19 | import { ImplicitRequest } from './implicit-request'; 20 | 21 | /** key for implicit request. */ 22 | const implicitRequestKey = 23 | (handle: string) => { 24 | return `${handle}_appauth_implicit_request`; 25 | } 26 | 27 | /** key in local storage which represents the current implicit request. */ 28 | const IMPLICIT_REQUEST_HANDLE_KEY = 'appauth_current_implicit_request'; 29 | export const IMPLICIT_RESPONSE_KEY = "implicit_response"; 30 | 31 | /** 32 | * This type represents a lambda that can take an ImplicitRequest, 33 | * and an TokenResponse as arguments. 34 | */ 35 | export type ImplicitListener = 36 | (request: ImplicitRequest, 37 | response: TokenResponse|null, 38 | error: TokenError|null) => void; 39 | 40 | /** 41 | * Represents a structural type holding both Implicit request and response. 42 | */ 43 | export interface ImplicitRequestResponse { 44 | request: ImplicitRequest; 45 | response: TokenResponse|null; 46 | error: TokenError|null; 47 | } 48 | 49 | /** 50 | * Implicit Service notifier. 51 | * This manages the communication of the TokenResponse to the 3p client. 52 | */ 53 | export class ImplicitNotifier { 54 | private listener: ImplicitListener|null = null; 55 | 56 | setImplicitListener(listener: ImplicitListener) { 57 | this.listener = listener; 58 | } 59 | 60 | /** 61 | * The Implicit complete callback. 62 | */ 63 | onImplicitComplete( 64 | request: ImplicitRequest, 65 | response: TokenResponse|null, 66 | error: TokenError|null): void { 67 | if (this.listener) { 68 | // complete Implicit request 69 | this.listener(request, response, error); 70 | } 71 | } 72 | } 73 | // TODO(rahulrav@): add more built in parameters. 74 | /* built in parameters. */ 75 | export const BUILT_IN_PARAMETERS = ['redirect_uri', 'client_id', 'response_type', 'state', 'scope']; 76 | 77 | /** 78 | * Defines the interface which is capable of handling an Implicit request 79 | * using various methods (iframe / popup / different process etc.). 80 | */ 81 | export abstract class ImplicitRequestHandler { 82 | constructor(public utils: QueryStringUtils, protected crypto: Crypto) {} 83 | 84 | // notifier send the response back to the client. 85 | protected notifier: ImplicitNotifier|null = null; 86 | 87 | /** 88 | * A utility method to be able to build the Implicit request URL. 89 | */ 90 | protected buildRequestUrl( 91 | configuration: AuthorizationServiceConfiguration, 92 | request: ImplicitRequest) { 93 | // build the query string 94 | // coerce to any type for convenience 95 | let requestMap: StringMap = { 96 | 'redirect_uri': request.redirectUri, 97 | 'client_id': request.clientId, 98 | 'response_type': request.responseType, 99 | 'state': request.state, 100 | 'scope': request.scope, 101 | 'nonce': request.nonce 102 | }; 103 | 104 | // copy over extras 105 | if (request.extras) { 106 | for (let extra in request.extras) { 107 | if (request.extras.hasOwnProperty(extra)) { 108 | // check before inserting to requestMap 109 | if (BUILT_IN_PARAMETERS.indexOf(extra) < 0) { 110 | requestMap[extra] = request.extras[extra]; 111 | } 112 | } 113 | } 114 | } 115 | 116 | let query = this.utils.stringify(requestMap); 117 | let baseUrl = configuration.authorizationEndpoint; 118 | let url = `${baseUrl}?${query}`; 119 | return url; 120 | } 121 | 122 | 123 | /** 124 | * Completes the Implicit request if necessary & when possible. 125 | */ 126 | completeImplicitRequestIfPossible(): Promise { 127 | // call complete Implicit if possible to see there might 128 | // be a response that needs to be delivered. 129 | log(`Checking to see if there is an Implicit response to be delivered.`); 130 | if (!this.notifier) { 131 | log(`Notifier is not present on ImplicitRequest handler. 132 | No delivery of result will be possible`) 133 | } 134 | return this.completeImplicitRequest().then(result => { 135 | if (!result) { 136 | log(`No result is available yet.`); 137 | } 138 | if (result && this.notifier) { 139 | this.notifier.onImplicitComplete(result.request, result.response, result.error); 140 | } 141 | }); 142 | } 143 | 144 | /** 145 | * Sets the default Implicit Service notifier. 146 | */ 147 | setImplicitNotifier(notifier: ImplicitNotifier): ImplicitRequestHandler { 148 | this.notifier = notifier; 149 | return this; 150 | }; 151 | 152 | /** 153 | * Makes an Implicit request. 154 | */ 155 | abstract performImplicitRequest( 156 | configuration: AuthorizationServiceConfiguration, 157 | request: ImplicitRequest): void; 158 | 159 | /** 160 | * Checks if an Implicit flow can be completed, and completes it. 161 | * The handler returns a `Promise` if ready, or a `Promise` 162 | * if not ready. 163 | */ 164 | protected abstract completeImplicitRequest(): Promise; 165 | } 166 | 167 | export class IonicImplicitRequestHandler extends ImplicitRequestHandler { 168 | 169 | constructor( 170 | private browser : Browser, 171 | private storage : StorageBackend, 172 | utils = new BasicQueryStringUtils(), 173 | private generateRandom = new DefaultCrypto(), 174 | ) { 175 | super(utils, generateRandom); 176 | } 177 | 178 | public async performImplicitRequest(configuration: AuthorizationServiceConfiguration, request: ImplicitRequest) : Promise { 179 | let handle = this.generateRandom.generateRandom(10); 180 | this.storage.setItem(IMPLICIT_REQUEST_HANDLE_KEY, handle); 181 | this.storage.setItem(implicitRequestKey(handle), JSON.stringify(await request.toJson())); 182 | let url = this.buildRequestUrl(configuration, request); 183 | let returnedUrl : string | undefined = await this.browser.showWindow(url, request.redirectUri); 184 | 185 | //callback may come from showWindow or via another method 186 | if(returnedUrl != undefined){ 187 | await this.storage.setItem(IMPLICIT_RESPONSE_KEY, url); 188 | this.completeImplicitRequest(); 189 | } 190 | } 191 | 192 | protected async completeImplicitRequest(): Promise { 193 | let handle = await this.storage.getItem(IMPLICIT_REQUEST_HANDLE_KEY); 194 | 195 | if (!handle) { 196 | throw new Error("Handle Not Available"); 197 | } 198 | 199 | let request : ImplicitRequest = this.getImplicitRequest(await this.storage.getItem(implicitRequestKey(handle))); 200 | let queryParams = this.getQueryParams(await this.storage.getItem(IMPLICIT_RESPONSE_KEY)); 201 | this.removeItemsFromStorage(handle); 202 | 203 | let state: string | undefined = queryParams['state']; 204 | let error: string | undefined = queryParams['error']; 205 | 206 | if (state !== request.state) { 207 | throw new Error("State Does Not Match"); 208 | } 209 | 210 | return { 211 | request: request, 212 | response: (!error) ? this.getImplicitResponse(queryParams) : null, 213 | error: (error) ? this.getImplicitError(queryParams) : null 214 | } 215 | 216 | } 217 | 218 | private getImplicitRequest(authRequest : string | null): ImplicitRequest { 219 | if(authRequest == null){ 220 | throw new Error("No Auth Request Available"); 221 | } 222 | 223 | return new ImplicitRequest(JSON.parse(authRequest)); 224 | } 225 | 226 | private getImplicitError(queryParams : StringMap): TokenError { 227 | let implicitErrorJSON : TokenErrorJson = { 228 | error: this.convertToErrorType(queryParams['error']) , 229 | error_description: queryParams['error_description'], 230 | error_uri: undefined 231 | } 232 | return new TokenError(implicitErrorJSON); 233 | } 234 | 235 | private getImplicitResponse(queryParams : StringMap): TokenResponse { 236 | let implicitResponseJSON : TokenResponseJson = { 237 | access_token: queryParams['access_token'], 238 | token_type: this.convertToTokenType(queryParams['token_type']), 239 | expires_in: +queryParams['expires_in'], 240 | refresh_token: queryParams['refresh_token'], 241 | scope: queryParams['scope'], 242 | id_token: queryParams['id_token'], 243 | issued_at: +queryParams['issued_at'] 244 | } 245 | return new TokenResponse(implicitResponseJSON); 246 | } 247 | 248 | private convertToTokenType(type : string) : TokenType | undefined { 249 | return (type == 'bearer' || type == 'mac') ? type : undefined; 250 | } 251 | 252 | private convertToErrorType(type : string) : ErrorType { 253 | return (type == 'invalid_request' || type == 'invalid_client' || type == 'invalid_grant' || type == 'unauthorized_client' || type == 'unsupported_grant_type' || type == 'invalid_scope') ? type : 'invalid_request'; 254 | } 255 | 256 | private removeItemsFromStorage(handle : string) : void { 257 | this.storage.removeItem(IMPLICIT_REQUEST_HANDLE_KEY); 258 | this.storage.removeItem(implicitRequestKey(handle)); 259 | this.storage.removeItem(IMPLICIT_RESPONSE_KEY); 260 | } 261 | 262 | private getQueryParams(authResponse: string | null) : StringMap { 263 | if(authResponse != null){ 264 | let parts = authResponse.split('#'); 265 | if (parts.length !== 2) throw new Error("Invalid auth response string"); 266 | let hash = parts[1]; 267 | return this.utils.parseQueryString(hash); 268 | }else{ 269 | return {}; 270 | } 271 | } 272 | 273 | } 274 | -------------------------------------------------------------------------------- /src/ionic-auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizationRequestHandler, TokenError } from '@openid/appauth'; 2 | import { IAuthAction, AuthActionBuilder, AuthActions } from './auth-action'; 3 | import { IonicUserInfoHandler, UserInfoHandler } from './user-info-request-handler'; 4 | import { IonicEndSessionHandler, EndSessionHandler } from './end-session-request-handler'; 5 | import { IAuthConfig } from './auth-configuration'; 6 | import { IonicAuthorizationRequestHandler, AUTHORIZATION_RESPONSE_KEY } from './authorization-request-handler'; 7 | import { Browser, DefaultBrowser } from "./auth-browser"; 8 | import { StorageBackend, Requestor, BaseTokenRequestHandler, AuthorizationServiceConfiguration, AuthorizationNotifier, TokenResponse, AuthorizationRequestJson, AuthorizationRequest, DefaultCrypto, GRANT_TYPE_AUTHORIZATION_CODE, TokenRequestJson, TokenRequest, GRANT_TYPE_REFRESH_TOKEN, AuthorizationResponse, AuthorizationError, LocalStorageBackend, JQueryRequestor, TokenRequestHandler } from '@openid/appauth'; 9 | import { EndSessionRequestJson, EndSessionRequest } from './end-session-request'; 10 | import { Observable, BehaviorSubject } from 'rxjs'; 11 | import { take } from 'rxjs/operators'; 12 | import { ImplicitRequestHandler, ImplicitNotifier, IMPLICIT_RESPONSE_KEY } from './implicit-request-handler'; 13 | import { ImplicitRequest, ImplicitRequestJson, ImplicitResponseType } from './implicit-request'; 14 | 15 | const TOKEN_RESPONSE_KEY = "token_response"; 16 | const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds 17 | const IS_VALID_BUFFER_KEY = 'isValidBuffer'; 18 | 19 | export interface IIonicAuth { 20 | signIn(loginHint?: string): void; 21 | signOut(): void; 22 | getUserInfo(): Promise; 23 | startUpAsync(): Promise; 24 | AuthorizationCallBack(url: string): void; 25 | EndSessionCallBack(): void; 26 | requestRefreshToken(tokenResponse: TokenResponse): Promise; 27 | getValidToken(): void; 28 | } 29 | 30 | export class BaseIonicAuth implements IIonicAuth { 31 | signIn(loginHint?: string): void { 32 | throw new Error("Method not implemented."); 33 | } 34 | signOut(): void { 35 | throw new Error("Method not implemented."); 36 | } 37 | getUserInfo(): Promise { 38 | throw new Error("Method not implemented."); 39 | } 40 | startUpAsync(): Promise { 41 | throw new Error("Method not implemented."); 42 | } 43 | AuthorizationCallBack(url: string): void { 44 | throw new Error("Method not implemented."); 45 | } 46 | EndSessionCallBack(): void { 47 | throw new Error("Method not implemented."); 48 | } 49 | requestRefreshToken(tokenResponse: TokenResponse): Promise { 50 | throw new Error("Method not implemented."); 51 | } 52 | getValidToken(): void { 53 | throw new Error("Method not implemented."); 54 | } 55 | } 56 | 57 | export const NullIonicAuthObject : IIonicAuth = new BaseIonicAuth(); 58 | 59 | export class IonicAuth implements BaseIonicAuth { 60 | 61 | protected configuration: AuthorizationServiceConfiguration | undefined; 62 | protected authConfig: IAuthConfig | undefined; 63 | protected authSubject : BehaviorSubject = new BehaviorSubject(AuthActionBuilder.Default()); 64 | public authObservable : Observable = this.authSubject.asObservable(); 65 | 66 | 67 | constructor( 68 | protected browser : Browser = new DefaultBrowser(), 69 | protected storage : StorageBackend = new LocalStorageBackend(), 70 | protected requestor : Requestor = new JQueryRequestor(), 71 | protected tokenHandler: TokenRequestHandler = new BaseTokenRequestHandler(requestor), 72 | protected userInfoHandler: UserInfoHandler = new IonicUserInfoHandler(requestor), 73 | protected requestHandler : AuthorizationRequestHandler | ImplicitRequestHandler = new IonicAuthorizationRequestHandler(browser, storage), 74 | protected endSessionHandler : EndSessionHandler = new IonicEndSessionHandler(browser) 75 | ){ 76 | this.setupNotifier(); 77 | } 78 | 79 | protected getAuthConfig() : IAuthConfig { 80 | if(!this.authConfig) 81 | throw new Error("AuthConfig Not Defined"); 82 | 83 | return this.authConfig; 84 | } 85 | 86 | protected setupNotifier(){ 87 | if(this.requestHandler instanceof AuthorizationRequestHandler){ 88 | let notifier = new AuthorizationNotifier(); 89 | this.requestHandler.setAuthorizationNotifier(notifier); 90 | notifier.setAuthorizationListener((request, response, error) => this.onAuthorizationNotification(request, response, error)); 91 | }else{ 92 | let notifier = new ImplicitNotifier(); 93 | this.requestHandler.setImplicitNotifier(notifier); 94 | notifier.setImplicitListener((request, response, error) => this.onImplicitNotification(request, response, error)); 95 | } 96 | } 97 | 98 | protected async onImplicitNotification(request : ImplicitRequest , response : TokenResponse | null, error : TokenError | null){ 99 | if (response != null) { 100 | await this.storage.setItem(TOKEN_RESPONSE_KEY, JSON.stringify(response.toJson())); 101 | this.authSubject.next(AuthActionBuilder.SignInSuccess(response)); 102 | }else if(error != null){ 103 | throw new Error(error.errorDescription); 104 | }else{ 105 | throw new Error("Unknown Error With Authentication"); 106 | } 107 | } 108 | 109 | protected onAuthorizationNotification(request : AuthorizationRequest , response : AuthorizationResponse | null, error : AuthorizationError | null){ 110 | let codeVerifier : string | undefined = (request.internal != undefined && this.getAuthConfig().usePkce) ? request.internal.code_verifier : undefined; 111 | 112 | if (response != null) { 113 | this.requestAccessToken(response.code, codeVerifier); 114 | }else if(error != null){ 115 | throw new Error(error.errorDescription); 116 | }else{ 117 | throw new Error("Unknown Error With Authentication"); 118 | } 119 | } 120 | 121 | public async signIn(loginHint?: string) { 122 | await this.performAuthorizationRequest(loginHint).catch((response) => { 123 | this.authSubject.next(AuthActionBuilder.SignInFailed(response)); 124 | throw response; 125 | }) 126 | } 127 | 128 | public async signOut(){ 129 | await this.performEndSessionRequest().catch((response) => { 130 | this.authSubject.next(AuthActionBuilder.SignOutFailed(response)); 131 | throw response; 132 | }) 133 | } 134 | 135 | public async getUserInfo() : Promise{ 136 | let token : TokenResponse | undefined = await this.getValidToken(); 137 | 138 | if(token != undefined){ 139 | return this.userInfoHandler.performUserInfoRequest(await this.getConfiguration(), token); 140 | } 141 | else{ 142 | throw new Error("Unable To Obtain User Info - No Token Available"); 143 | } 144 | } 145 | 146 | public async startUpAsync(){ 147 | //subscribing to auth observable for event hooks 148 | this.authObservable.subscribe((action : IAuthAction) => this.authObservableEvents(action)); 149 | 150 | let token : TokenResponse | undefined; 151 | let tokenResponseString : string | null = await this.storage.getItem(TOKEN_RESPONSE_KEY); 152 | 153 | if(tokenResponseString != null){ 154 | token = new TokenResponse(JSON.parse(tokenResponseString)); 155 | if(token && !token.isValid()){ 156 | token = await this.requestNewToken(token); 157 | } 158 | } 159 | 160 | if(!token){ 161 | this.authSubject.next(AuthActionBuilder.AutoSignInFailed("No Token Available")); 162 | }else{ 163 | this.authSubject.next(AuthActionBuilder.AutoSignInSuccess(token)); 164 | } 165 | } 166 | 167 | public async AuthorizationCallBack(url: string){ 168 | this.browser.closeWindow(); 169 | 170 | if(this.requestHandler instanceof AuthorizationRequestHandler){ 171 | await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, url); 172 | this.requestHandler.completeAuthorizationRequestIfPossible(); 173 | }else{ 174 | await this.storage.setItem(IMPLICIT_RESPONSE_KEY, url); 175 | this.requestHandler.completeImplicitRequestIfPossible(); 176 | } 177 | } 178 | 179 | public async EndSessionCallBack(){ 180 | this.browser.closeWindow(); 181 | this.storage.removeItem(TOKEN_RESPONSE_KEY); 182 | this.authSubject.next(AuthActionBuilder.SignOutSuccess()); 183 | } 184 | 185 | protected async performEndSessionRequest() : Promise{ 186 | let token : TokenResponse | undefined = await this.getTokenFromObserver(); 187 | 188 | if(token != undefined){ 189 | let requestJson : EndSessionRequestJson = { 190 | postLogoutRedirectURI : this.getAuthConfig().end_session_redirect_url, 191 | idTokenHint: token.idToken || '' 192 | } 193 | 194 | let request : EndSessionRequest = new EndSessionRequest(requestJson); 195 | let returnedUrl : string | undefined = await this.endSessionHandler.performEndSessionRequest(await this.getConfiguration(), request); 196 | 197 | //callback may come from showWindow or via another method 198 | if(returnedUrl != undefined){ 199 | this.EndSessionCallBack(); 200 | } 201 | }else{ 202 | //if user has no token they should not be logged in in the first place 203 | this.EndSessionCallBack(); 204 | } 205 | } 206 | 207 | protected async performAuthorizationRequest(loginHint?: string) : Promise { 208 | if(this.requestHandler instanceof AuthorizationRequestHandler){ 209 | return this.requestHandler.performAuthorizationRequest(await this.getConfiguration(), await this.getAuthorizationRequest(loginHint)); 210 | }else{ 211 | return this.requestHandler.performImplicitRequest(await this.getConfiguration(), await this.getImplicitRequest(loginHint)); 212 | } 213 | } 214 | 215 | protected async getAuthorizationRequest(loginHint?: string){ 216 | let authConfig : IAuthConfig = this.getAuthConfig(); 217 | let requestJson : AuthorizationRequestJson = { 218 | response_type: authConfig.response_type || AuthorizationRequest.RESPONSE_TYPE_CODE, 219 | client_id: authConfig.identity_client, 220 | redirect_uri: authConfig.redirect_url, 221 | scope: authConfig.scopes, 222 | extras: authConfig.auth_extras 223 | } 224 | 225 | if(loginHint){ 226 | requestJson.extras = requestJson.extras || {}; 227 | requestJson.extras['login_hint'] = loginHint; 228 | } 229 | 230 | let request = new AuthorizationRequest(requestJson, new DefaultCrypto(), authConfig.usePkce); 231 | 232 | if(authConfig.usePkce) 233 | await request.setupCodeVerifier(); 234 | 235 | return request; 236 | } 237 | 238 | protected async getImplicitRequest(loginHint?: string){ 239 | let authConfig : IAuthConfig = this.getAuthConfig(); 240 | let requestJson : ImplicitRequestJson = { 241 | response_type: authConfig.response_type || ImplicitResponseType.IdTokenToken, 242 | client_id: authConfig.identity_client, 243 | redirect_uri: authConfig.redirect_url, 244 | scope: authConfig.scopes, 245 | extras: authConfig.auth_extras 246 | } 247 | 248 | if(loginHint){ 249 | requestJson.extras = requestJson.extras || {}; 250 | requestJson.extras['login_hint'] = loginHint; 251 | } 252 | 253 | return new ImplicitRequest(requestJson, new DefaultCrypto()); 254 | } 255 | 256 | protected async getConfiguration() : Promise{ 257 | if(!this.configuration){ 258 | this.configuration = await AuthorizationServiceConfiguration.fetchFromIssuer(this.getAuthConfig().identity_server,this.requestor).catch(()=> undefined); 259 | } 260 | 261 | if(this.configuration != undefined){ 262 | return this.configuration; 263 | }else{ 264 | throw new Error("Unable To Obtain Server Configuration"); 265 | } 266 | } 267 | 268 | protected async requestAccessToken(code : string, codeVerifier?: string) : Promise { 269 | let authConfig : IAuthConfig = this.getAuthConfig(); 270 | let requestJSON: TokenRequestJson = { 271 | grant_type: GRANT_TYPE_AUTHORIZATION_CODE, 272 | code: code, 273 | refresh_token: undefined, 274 | redirect_uri: authConfig.redirect_url, 275 | client_id: authConfig.identity_client, 276 | extras: (codeVerifier) ? { 277 | "code_verifier": codeVerifier 278 | } : {} 279 | 280 | } 281 | 282 | try{ 283 | let token : TokenResponse = await this.tokenHandler.performTokenRequest(await this.getConfiguration(), new TokenRequest(requestJSON)); 284 | await this.storage.setItem(TOKEN_RESPONSE_KEY, JSON.stringify(token.toJson())); 285 | this.authSubject.next(AuthActionBuilder.SignInSuccess(token)) 286 | }catch(error){ 287 | this.authSubject.next(AuthActionBuilder.SignInFailed(error)) 288 | throw error; 289 | } 290 | } 291 | 292 | public async requestRefreshToken(tokenResponse : TokenResponse) : Promise { 293 | let authConfig : IAuthConfig = this.getAuthConfig(); 294 | let requestJSON: TokenRequestJson = { 295 | grant_type: GRANT_TYPE_REFRESH_TOKEN, 296 | code: undefined, 297 | refresh_token: tokenResponse.refreshToken, 298 | redirect_uri: authConfig.redirect_url, 299 | client_id: authConfig.identity_client, 300 | } 301 | 302 | try{ 303 | let token : TokenResponse = await this.tokenHandler.performTokenRequest(await this.getConfiguration(), new TokenRequest(requestJSON)) 304 | await this.storage.setItem(TOKEN_RESPONSE_KEY, JSON.stringify(token.toJson())); 305 | this.authSubject.next(AuthActionBuilder.RefreshSuccess(token)); 306 | }catch(error){ 307 | this.storage.removeItem(TOKEN_RESPONSE_KEY); 308 | this.authSubject.next(AuthActionBuilder.RefreshFailed(error)) 309 | throw error; 310 | } 311 | } 312 | 313 | public async getValidToken(){ 314 | let token : TokenResponse | undefined = await this.getTokenFromObserver(); 315 | 316 | if(token == undefined) 317 | throw new Error("Unable To Obtain Token - No Token Available"); 318 | 319 | // The buffer parameter passed to token.isValid(). 320 | let isValidBuffer = AUTH_EXPIRY_BUFFER; 321 | 322 | const authConfig : IAuthConfig = this.getAuthConfig(); 323 | 324 | // See if a IS_VALID_BUFFER_KEY is specified in the config extras, 325 | // to specify a buffer parameter for token.isValid(). 326 | if (authConfig.auth_extras) { 327 | if (authConfig.auth_extras.hasOwnProperty(IS_VALID_BUFFER_KEY)) { 328 | isValidBuffer = parseInt(authConfig.auth_extras[IS_VALID_BUFFER_KEY], 10); 329 | } 330 | } 331 | 332 | if(!token.isValid(isValidBuffer)){ 333 | token = await this.requestNewToken(token); 334 | } 335 | 336 | return token; 337 | } 338 | 339 | protected async requestNewToken(token: TokenResponse) : Promise { 340 | await this.requestRefreshToken(token); 341 | return this.getTokenFromObserver(); 342 | } 343 | 344 | protected async getTokenFromObserver() : Promise { 345 | return this.authSubject.pipe(take(1)).toPromise().then((action : IAuthAction) => action.tokenResponse); 346 | } 347 | 348 | private authObservableEvents(action : IAuthAction) : IAuthAction { 349 | switch(action.action){ 350 | case AuthActions.Default: 351 | break; 352 | case AuthActions.SignInSuccess : 353 | case AuthActions.AutoSignInSuccess : 354 | this.onSignInSuccessful(action); 355 | break; 356 | case AuthActions.RefreshSuccess : 357 | this.onRefreshSuccessful(action); 358 | break; 359 | case AuthActions.SignOutSuccess : 360 | this.onSignOutSuccessful(action); 361 | break; 362 | case AuthActions.SignInFailed : 363 | case AuthActions.AutoSignInFailed : 364 | this.onSignInFailure(action); 365 | break; 366 | case AuthActions.RefreshFailed : 367 | this.onRefreshFailure(action); 368 | break; 369 | case AuthActions.SignOutFailed : 370 | this.onSignOutFailure(action); 371 | break; 372 | } 373 | return action; 374 | } 375 | 376 | //Auth Events To Be Overriden 377 | protected onSignInSuccessful(action: IAuthAction): void { 378 | } 379 | protected onSignOutSuccessful(action: IAuthAction): void { 380 | } 381 | protected onRefreshSuccessful(action: IAuthAction): void { 382 | } 383 | protected onSignInFailure(action: IAuthAction): void { 384 | } 385 | protected onSignOutFailure(action: IAuthAction): void { 386 | } 387 | protected onRefreshFailure(action: IAuthAction): void { 388 | } 389 | } 390 | 391 | --------------------------------------------------------------------------------