diff --git a/src/AccessTokenEvents.js b/src/AccessTokenEvents.ts similarity index 82% rename from src/AccessTokenEvents.js rename to src/AccessTokenEvents.ts index 95d37e2fd..6b2954ae9 100644 --- a/src/AccessTokenEvents.js +++ b/src/AccessTokenEvents.ts @@ -1,12 +1,18 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { Timer } from './Timer.js'; +import { Log } from './Log'; +import { Timer } from './Timer'; +import { User } from './User'; const DefaultAccessTokenExpiringNotificationTime = 60; // seconds +export type AccessTokenCallback = (...ev: any[]) => void; + export class AccessTokenEvents { + private _accessTokenExpiringNotificationTime: number + private _accessTokenExpiring: Timer + private _accessTokenExpired: Timer constructor({ accessTokenExpiringNotificationTime = DefaultAccessTokenExpiringNotificationTime, @@ -14,12 +20,11 @@ export class AccessTokenEvents { accessTokenExpiredTimer = new Timer("Access token expired") } = {}) { this._accessTokenExpiringNotificationTime = accessTokenExpiringNotificationTime; - this._accessTokenExpiring = accessTokenExpiringTimer; this._accessTokenExpired = accessTokenExpiredTimer; } - load(container) { + load(container: User) { // only register events if there's an access token and it has an expiration if (container.access_token && container.expires_in !== undefined) { let duration = container.expires_in; @@ -31,7 +36,7 @@ export class AccessTokenEvents { if (expiring <= 0){ expiring = 1; } - + Log.debug("AccessTokenEvents.load: registering expiring timer in:", expiring); this._accessTokenExpiring.init(expiring); } @@ -57,17 +62,17 @@ export class AccessTokenEvents { this._accessTokenExpired.cancel(); } - addAccessTokenExpiring(cb) { + addAccessTokenExpiring(cb: AccessTokenCallback) { this._accessTokenExpiring.addHandler(cb); } - removeAccessTokenExpiring(cb) { + removeAccessTokenExpiring(cb: AccessTokenCallback) { this._accessTokenExpiring.removeHandler(cb); } - addAccessTokenExpired(cb) { + addAccessTokenExpired(cb: AccessTokenCallback) { this._accessTokenExpired.addHandler(cb); } - removeAccessTokenExpired(cb) { + removeAccessTokenExpired(cb: AccessTokenCallback) { this._accessTokenExpired.removeHandler(cb); } } diff --git a/src/CheckSessionIFrame.js b/src/CheckSessionIFrame.ts similarity index 75% rename from src/CheckSessionIFrame.js rename to src/CheckSessionIFrame.ts index 3da0aa050..9329622c9 100644 --- a/src/CheckSessionIFrame.js +++ b/src/CheckSessionIFrame.ts @@ -1,17 +1,26 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; const DefaultInterval = 2000; export class CheckSessionIFrame { - constructor(callback, client_id, url, interval, stopOnError = true) { + private _callback: () => void; + private _client_id: string; + private _interval: number; + private _stopOnError: boolean; + private _frame_origin: string; + private _frame: HTMLIFrameElement; + private _boundMessageEvent: ((e: any) => void) | null; + private _timer: number | null; + private _session_state: any | null; + + constructor(callback: () => void, client_id: string, url: string, interval?: number, stopOnError?: boolean) { this._callback = callback; this._client_id = client_id; - this._url = url; this._interval = interval || DefaultInterval; - this._stopOnError = stopOnError; + this._stopOnError = stopOnError || true; var idx = url.indexOf("/", url.indexOf("//") + 2); this._frame_origin = url.substr(0, idx); @@ -22,13 +31,17 @@ export class CheckSessionIFrame { this._frame.style.visibility = "hidden"; this._frame.style.position = "absolute"; this._frame.style.display = "none"; - this._frame.width = 0; - this._frame.height = 0; - + this._frame.width = "0"; + this._frame.height = "0"; this._frame.src = url; + + this._boundMessageEvent = null; + this._timer = null; + } + load() { - return new Promise((resolve) => { + return new Promise((resolve) => { this._frame.onload = () => { resolve(); } @@ -38,7 +51,8 @@ export class CheckSessionIFrame { window.addEventListener("message", this._boundMessageEvent, false); }); } - _message(e) { + + _message(e: any) { if (e.origin === this._frame_origin && e.source === this._frame.contentWindow ) { @@ -58,7 +72,8 @@ export class CheckSessionIFrame { } } } - start(session_state) { + + start(session_state: any) { if (this._session_state !== session_state) { Log.debug("CheckSessionIFrame.start"); @@ -67,9 +82,10 @@ export class CheckSessionIFrame { this._session_state = session_state; let send = () => { + this._frame.contentWindow && this._frame.contentWindow.postMessage(this._client_id + " " + this._session_state, this._frame_origin); }; - + // trigger now send(); diff --git a/src/ClockService.js b/src/ClockService.ts similarity index 100% rename from src/ClockService.js rename to src/ClockService.ts diff --git a/src/CordovaIFrameNavigator.js b/src/CordovaIFrameNavigator.ts similarity index 63% rename from src/CordovaIFrameNavigator.js rename to src/CordovaIFrameNavigator.ts index 953ff7a48..2d9e59cd9 100644 --- a/src/CordovaIFrameNavigator.js +++ b/src/CordovaIFrameNavigator.ts @@ -1,11 +1,12 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { CordovaPopupWindow } from './CordovaPopupWindow.js'; +import { CordovaPopupWindow } from './CordovaPopupWindow'; +import { INavigator } from './INavigator'; -export class CordovaIFrameNavigator { +export class CordovaIFrameNavigator implements INavigator { - prepare(params) { + prepare(params: any) { params.popupWindowFeatures = 'hidden=yes'; let popup = new CordovaPopupWindow(params); return Promise.resolve(popup); diff --git a/src/CordovaPopupNavigator.js b/src/CordovaPopupNavigator.ts similarity index 59% rename from src/CordovaPopupNavigator.js rename to src/CordovaPopupNavigator.ts index 346486ed1..b43137b37 100644 --- a/src/CordovaPopupNavigator.js +++ b/src/CordovaPopupNavigator.ts @@ -1,11 +1,12 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { CordovaPopupWindow } from './CordovaPopupWindow.js'; +import { CordovaPopupWindow } from './CordovaPopupWindow'; +import { INavigator } from './INavigator'; -export class CordovaPopupNavigator { +export class CordovaPopupNavigator implements INavigator { - prepare(params) { + prepare(params: any) { let popup = new CordovaPopupWindow(params); return Promise.resolve(popup); } diff --git a/src/CordovaPopupWindow.js b/src/CordovaPopupWindow.ts similarity index 72% rename from src/CordovaPopupWindow.js rename to src/CordovaPopupWindow.ts index d4bd10f40..74bf3ce18 100644 --- a/src/CordovaPopupWindow.js +++ b/src/CordovaPopupWindow.ts @@ -1,14 +1,24 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; +import { IWindow } from './IWindow'; const DefaultPopupFeatures = 'location=no,toolbar=no,zoom=no'; const DefaultPopupTarget = "_blank"; -export class CordovaPopupWindow { +export class CordovaPopupWindow implements IWindow { + private _promise: Promise; + private _resolve!: (value: unknown) => void; + private _reject!: (reason?: any) => void; + private features: string; + private target: string; + private redirect_uri: string; + private _popup: any; + private _exitCallbackEvent?: (message: any) => void; + private _loadStartCallbackEvent?: (event: any) => void; - constructor(params) { + constructor(params: any) { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; @@ -16,36 +26,42 @@ export class CordovaPopupWindow { this.features = params.popupWindowFeatures || DefaultPopupFeatures; this.target = params.popupWindowTarget || DefaultPopupTarget; - + this.redirect_uri = params.startUrl; Log.debug("CordovaPopupWindow.ctor: redirect_uri: " + this.redirect_uri); } - _isInAppBrowserInstalled(cordovaMetadata) { + _isInAppBrowserInstalled(cordovaMetadata: any) { return ["cordova-plugin-inappbrowser", "cordova-plugin-inappbrowser.inappbrowser", "org.apache.cordova.inappbrowser"].some(function (name) { return cordovaMetadata.hasOwnProperty(name) }) } - - navigate(params) { + + navigate(params: any) { if (!params || !params.url) { this._error("No url provided"); } else { + // @ts-ignore if (!window.cordova) { - return this._error("cordova is undefined") + this._error("cordova is undefined"); + return this.promise; } - + + // @ts-ignore var cordovaMetadata = window.cordova.require("cordova/plugin_list").metadata; if (this._isInAppBrowserInstalled(cordovaMetadata) === false) { - return this._error("InAppBrowser plugin not found") + this._error("InAppBrowser plugin not found"); + return this.promise; } + + // @ts-ignore this._popup = cordova.InAppBrowser.open(params.url, this.target, this.features); if (this._popup) { Log.debug("CordovaPopupWindow.navigate: popup successfully created"); - - this._exitCallbackEvent = this._exitCallback.bind(this); + + this._exitCallbackEvent = this._exitCallback.bind(this); this._loadStartCallbackEvent = this._loadStartCallback.bind(this); - + this._popup.addEventListener("exit", this._exitCallbackEvent, false); this._popup.addEventListener("loadstart", this._loadStartCallbackEvent, false); } else { @@ -59,22 +75,22 @@ export class CordovaPopupWindow { return this._promise; } - _loadStartCallback(event) { + _loadStartCallback(event: any) { if (event.url.indexOf(this.redirect_uri) === 0) { this._success({ url: event.url }); - } + } } - _exitCallback(message) { - this._error(message); + _exitCallback(message: string) { + this._error(message); } - - _success(data) { + + _success(data: any) { this._cleanup(); Log.debug("CordovaPopupWindow: Successful response from cordova popup window"); this._resolve(data); } - _error(message) { + _error(message: string) { this._cleanup(); Log.error(message); diff --git a/src/ErrorResponse.js b/src/ErrorResponse.ts similarity index 61% rename from src/ErrorResponse.js rename to src/ErrorResponse.ts index fecb07a54..db1e826c8 100644 --- a/src/ErrorResponse.js +++ b/src/ErrorResponse.ts @@ -1,12 +1,22 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; export class ErrorResponse extends Error { - constructor({error, error_description, error_uri, state, session_state}={} - ) { - if (!error){ + public readonly name: string; + + public readonly error: string; + public readonly error_description: string; + public readonly error_uri: string; + + public readonly state: any; + public readonly session_state?: string; + + constructor({ + error, error_description, error_uri, state, session_state + }: any) { + if (!error) { Log.error("No error passed to ErrorResponse"); throw new Error("error"); } diff --git a/src/Event.js b/src/Event.ts similarity index 70% rename from src/Event.js rename to src/Event.ts index 9ac7c7520..382e9f997 100644 --- a/src/Event.js +++ b/src/Event.ts @@ -1,27 +1,29 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; export class Event { + protected _name: string; + private _callbacks: ((...ev: any[]) => void)[]; - constructor(name) { + constructor(name: string) { this._name = name; this._callbacks = []; } - addHandler(cb) { + addHandler(cb: (...ev: any[]) => void) { this._callbacks.push(cb); } - removeHandler(cb) { + removeHandler(cb: (...ev: any[]) => void) { var idx = this._callbacks.findIndex(item => item === cb); if (idx >= 0) { this._callbacks.splice(idx, 1); } } - raise(...params) { + raise(...params: any[]) { Log.debug("Event: Raising event: " + this._name); for (let i = 0; i < this._callbacks.length; i++) { this._callbacks[i](...params); diff --git a/src/Global.js b/src/Global.js deleted file mode 100644 index 9dcded8fb..000000000 --- a/src/Global.js +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -const timer = { - setInterval: function (cb, duration) { - return setInterval(cb, duration); - }, - clearInterval: function (handle) { - return clearInterval(handle); - } -}; - -let testing = false; -let request = null; - -export class Global { - - static _testing() { - testing = true; - } - - static get location() { - if (!testing) { - return location; - } - } - - static get localStorage() { - if (!testing && typeof window !== 'undefined') { - return localStorage; - } - } - - static get sessionStorage() { - if (!testing && typeof window !== 'undefined') { - return sessionStorage; - } - } - - static setXMLHttpRequest(newRequest) { - request = newRequest; - } - - static get XMLHttpRequest() { - if (!testing && typeof window !== 'undefined') { - return request || XMLHttpRequest; - } - } - - static get timer() { - if (!testing) { - return timer; - } - } -} diff --git a/src/Global.ts b/src/Global.ts new file mode 100644 index 000000000..427ba9628 --- /dev/null +++ b/src/Global.ts @@ -0,0 +1,64 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +const timer = { + setInterval: function (cb: (...args: any[]) => void, duration?: number): number { + // @ts-ignore + return setInterval(cb, duration); + }, + clearInterval: function (handle: number): void { + return clearInterval(handle); + } +}; + +/*TODO: port-TS +let testing = false; +let request: XMLHttpRequest | null = null; +*/ + +export class Global { + + /*TODO: port-TS static _testing() { + testing = true; + } + + static get location() { + if (!testing) { + return location; + } + }*/ + + static get localStorage() { + /*TODO: port-TS if (!testing && typeof window !== 'undefined') { + return localStorage; + }*/ + return localStorage; + } + + static get sessionStorage() { + /*TODO: port-TS if (!testing && typeof window !== 'undefined') { + return sessionStorage; + }*/ + return sessionStorage; + } + + /*TODO: port-TS + static setXMLHttpRequest(newRequest: XMLHttpRequest) { + request = newRequest; + } + */ + + static get XMLHttpRequest(): typeof XMLHttpRequest { + /*TODO: port-TS if (!testing && typeof window !== 'undefined') { + return request || XMLHttpRequest; + }*/ + return XMLHttpRequest; + } + + static get timer() { + /*TODO: port-TS if (!testing) { + return timer; + }*/ + return timer; + } +} diff --git a/src/IFrameNavigator.js b/src/IFrameNavigator.ts similarity index 68% rename from src/IFrameNavigator.js rename to src/IFrameNavigator.ts index 114ae39f8..d26a83d2e 100644 --- a/src/IFrameNavigator.js +++ b/src/IFrameNavigator.ts @@ -1,17 +1,18 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { IFrameWindow } from './IFrameWindow.js'; +import { Log } from './Log'; +import { IFrameWindow } from './IFrameWindow'; +import { INavigator } from './INavigator'; -export class IFrameNavigator { +export class IFrameNavigator implements INavigator { - prepare(params) { + prepare(params: any) { let frame = new IFrameWindow(params); return Promise.resolve(frame); } - callback(url) { + callback(url: string) { Log.debug("IFrameNavigator.callback"); try { diff --git a/src/IFrameWindow.js b/src/IFrameWindow.ts similarity index 77% rename from src/IFrameWindow.js rename to src/IFrameWindow.ts index bfb038b2d..fdd09ab21 100644 --- a/src/IFrameWindow.js +++ b/src/IFrameWindow.ts @@ -1,13 +1,20 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; +import { IWindow } from './IWindow'; const DefaultTimeout = 10000; -export class IFrameWindow { +export class IFrameWindow implements IWindow { + private _promise: Promise; + private _resolve!: (value: unknown) => void; + private _reject!: (reason?: any) => void; + private _boundMessageEvent: ((e: any) => void) | null; + private _frame: HTMLIFrameElement | null; + private _timer: number | null; - constructor(params) { + constructor(_params: any) { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; @@ -21,13 +28,15 @@ export class IFrameWindow { // shotgun approach this._frame.style.visibility = "hidden"; this._frame.style.position = "absolute"; - this._frame.width = 0; - this._frame.height = 0; + this._frame.width = "0"; + this._frame.height = "0"; window.document.body.appendChild(this._frame); + + this._timer = null; } - navigate(params) { + navigate(params: any) { if (!params || !params.url) { this._error("No url provided"); } @@ -35,7 +44,7 @@ export class IFrameWindow { let timeout = params.silentRequestTimeout || DefaultTimeout; Log.debug("IFrameWindow.navigate: Using timeout of:", timeout); this._timer = window.setTimeout(this._timeout.bind(this), timeout); - this._frame.src = params.url; + this._frame!.src = params.url; } return this.promise; @@ -45,13 +54,13 @@ export class IFrameWindow { return this._promise; } - _success(data) { + _success(data: any) { this._cleanup(); Log.debug("IFrameWindow: Successful response from frame window"); this._resolve(data); } - _error(message) { + _error(message: string) { this._cleanup(); Log.error(message); @@ -66,8 +75,8 @@ export class IFrameWindow { if (this._frame) { Log.debug("IFrameWindow: cleanup"); - window.removeEventListener("message", this._boundMessageEvent, false); - window.clearTimeout(this._timer); + window.removeEventListener("message", this._boundMessageEvent!, false); + window.clearTimeout(this._timer!); window.document.body.removeChild(this._frame); this._timer = null; @@ -81,10 +90,10 @@ export class IFrameWindow { this._error("Frame window timed out"); } - _message(e) { + _message(e: any) { Log.debug("IFrameWindow.message"); - if (this._timer && + if (this._timer && this._frame && e.origin === this._origin && e.source === this._frame.contentWindow && (typeof e.data === 'string' && (e.data.startsWith('http://') || e.data.startsWith('https://'))) @@ -103,7 +112,7 @@ export class IFrameWindow { return location.protocol + "//" + location.host; } - static notifyParent(url) { + static notifyParent(url: string) { Log.debug("IFrameWindow.notifyParent"); url = url || window.location.href; if (url) { diff --git a/src/INavigator.ts b/src/INavigator.ts new file mode 100644 index 000000000..4362835ec --- /dev/null +++ b/src/INavigator.ts @@ -0,0 +1,8 @@ +// Copyright (c) Patrick Ammann. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { IWindow } from './IWindow'; + +export interface INavigator { + prepare(params: any): Promise +} diff --git a/src/IWindow.ts b/src/IWindow.ts new file mode 100644 index 000000000..2465a4211 --- /dev/null +++ b/src/IWindow.ts @@ -0,0 +1,7 @@ +// Copyright (c) Patrick Ammann. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +export interface IWindow { + navigate(params: any): Promise; + close(): void; +} diff --git a/src/InMemoryWebStorage.js b/src/InMemoryWebStorage.ts similarity index 64% rename from src/InMemoryWebStorage.js rename to src/InMemoryWebStorage.ts index f6360d396..fb2c2a738 100644 --- a/src/InMemoryWebStorage.js +++ b/src/InMemoryWebStorage.ts @@ -1,24 +1,31 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; -export class InMemoryWebStorage{ - constructor(){ +export class InMemoryWebStorage implements Storage { + private _data: Record; + + constructor() { + this._data = {}; + } + + clear(): void { + Log.debug("InMemoryWebStorage.clear"); this._data = {}; } - getItem(key) { + getItem(key: string) { Log.debug("InMemoryWebStorage.getItem", key); return this._data[key]; } - setItem(key, value){ + setItem(key: string, value: any) { Log.debug("InMemoryWebStorage.setItem", key); this._data[key] = value; } - removeItem(key){ + removeItem(key: string) { Log.debug("InMemoryWebStorage.removeItem", key); delete this._data[key]; } @@ -27,7 +34,7 @@ export class InMemoryWebStorage{ return Object.getOwnPropertyNames(this._data).length; } - key(index) { + key(index: number) { return Object.getOwnPropertyNames(this._data)[index]; } } diff --git a/src/JoseUtil.js b/src/JoseUtil.ts similarity index 100% rename from src/JoseUtil.js rename to src/JoseUtil.ts diff --git a/src/JoseUtilImpl.js b/src/JoseUtilImpl.ts similarity index 85% rename from src/JoseUtilImpl.js rename to src/JoseUtilImpl.ts index e2cf383d4..1cadd2240 100644 --- a/src/JoseUtilImpl.js +++ b/src/JoseUtilImpl.ts @@ -1,12 +1,12 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; -export default function getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64tohex, AllowedSigningAlgs }) { +export default function getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64tohex, AllowedSigningAlgs }: any) { return class JoseUtil { - static parseJwt(jwt) { + static parseJwt(jwt: any) { Log.debug("JoseUtil.parseJwt"); try { var token = jws.JWS.parse(jwt); @@ -16,10 +16,11 @@ export default function getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64 } } catch (e) { Log.error(e); + return null; } } - static validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) { + static validateJwt(jwt: any, key: any, issuer: string, audience: string, clockSkew: number, now?: number, timeInsensitive = false) { Log.debug("JoseUtil.validateJwt"); try { @@ -52,16 +53,21 @@ export default function getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64 } } - static validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive) { + static validateJwtAttributes(jwt: any, issuer: string, audience: string, clockSkew: number, now?: number, timeInsensitive=false) { if (!clockSkew) { clockSkew = 0; } if (!now) { - now = parseInt(Date.now() / 1000); + now = Date.now() / 1000; } - var payload = JoseUtil.parseJwt(jwt).payload; + const parsedJwt = JoseUtil.parseJwt(jwt); + if (!parsedJwt || !parsedJwt.payload) { + return Promise.reject(new Error("Failed to parse token")); + } + + var payload = parsedJwt.payload; if (!payload.iss) { Log.error("JoseUtil._validateJwt: issuer was not provided"); @@ -117,7 +123,7 @@ export default function getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64 return Promise.resolve(payload); } - static _validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive) { + static _validateJwt(jwt: any, key: string, issuer: string, audience: string, clockSkew: number, now?: number, timeInsensitive = false) { return JoseUtil.validateJwtAttributes(jwt, issuer, audience, clockSkew, now, timeInsensitive).then(payload => { try { @@ -134,7 +140,7 @@ export default function getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64 }); } - static hashString(value, alg) { + static hashString(value: any, alg: string) { try { return crypto.Util.hashString(value, alg); } catch (e) { @@ -142,7 +148,7 @@ export default function getJoseUtil({ jws, KeyUtil, X509, crypto, hextob64u, b64 } } - static hexToBase64Url(value) { + static hexToBase64Url(value: any) { try { return hextob64u(value); } catch (e) { diff --git a/src/JoseUtilRsa.js b/src/JoseUtilRsa.ts similarity index 100% rename from src/JoseUtilRsa.js rename to src/JoseUtilRsa.ts diff --git a/src/JsonService.js b/src/JsonService.ts similarity index 85% rename from src/JsonService.js rename to src/JsonService.ts index eb76bda02..a3da48057 100644 --- a/src/JsonService.js +++ b/src/JsonService.ts @@ -1,14 +1,19 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { Global } from './Global.js'; +import { Log } from './Log'; +import { Global } from './Global'; +import { SigninResponse } from './SigninResponse'; export class JsonService { + private _contentTypes: string[]; + private _XMLHttpRequest: typeof XMLHttpRequest; + private _jwtHandler: any; + constructor( - additionalContentTypes = null, - XMLHttpRequestCtor = Global.XMLHttpRequest, - jwtHandler = null + additionalContentTypes: string[] | null = null, + XMLHttpRequestCtor = Global.XMLHttpRequest, + jwtHandler: any = null ) { if (additionalContentTypes && Array.isArray(additionalContentTypes)) { @@ -27,7 +32,7 @@ export class JsonService { this._jwtHandler = jwtHandler; } - getJson(url, token) { + getJson(url: string, token?: string): Promise { if (!url){ Log.error("JsonService.getJson: No url passed"); throw new Error("url"); @@ -47,15 +52,9 @@ export class JsonService { Log.debug("JsonService.getJson: HTTP response received, status", req.status); if (req.status === 200) { - - var contentType = req.getResponseHeader("Content-Type"); + const contentType = req.getResponseHeader("Content-Type"); if (contentType) { - - var found = allowedContentTypes.find(item=>{ - if (contentType.startsWith(item)) { - return true; - } - }); + var found = allowedContentTypes.find(item => contentType.startsWith(item)); if (found == "application/jwt") { jwtHandler(req).then(resolve, reject); @@ -96,7 +95,7 @@ export class JsonService { }); } - postForm(url, payload, basicAuth) { + postForm(url: string, payload: any, basicAuth?: string): Promise { if (!url){ Log.error("JsonService.postForm: No url passed"); throw new Error("url"); @@ -115,16 +114,9 @@ export class JsonService { Log.debug("JsonService.postForm: HTTP response received, status", req.status); if (req.status === 200) { - - var contentType = req.getResponseHeader("Content-Type"); + const contentType = req.getResponseHeader("Content-Type"); if (contentType) { - - var found = allowedContentTypes.find(item=>{ - if (contentType.startsWith(item)) { - return true; - } - }); - + var found = allowedContentTypes.find(item => contentType.startsWith(item)); if (found) { try { resolve(JSON.parse(req.responseText)); @@ -143,16 +135,9 @@ export class JsonService { } if (req.status === 400) { - - var contentType = req.getResponseHeader("Content-Type"); + const contentType = req.getResponseHeader("Content-Type"); if (contentType) { - - var found = allowedContentTypes.find(item=>{ - if (contentType.startsWith(item)) { - return true; - } - }); - + var found = allowedContentTypes.find(item => contentType.startsWith(item)); if (found) { try { var payload = JSON.parse(req.responseText); diff --git a/src/Log.js b/src/Log.ts similarity index 75% rename from src/Log.js rename to src/Log.ts index 5f59db5f1..8fd70b2ba 100644 --- a/src/Log.js +++ b/src/Log.ts @@ -1,7 +1,14 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -let nopLogger = { +export interface Logger { + error(...args: any[]): void; + info(...args: any[]): void; + debug(...args: any[]): void; + warn(...args: any[]): void; +} + +let nopLogger: Logger = { debug(){}, info(){}, warn(){}, @@ -14,8 +21,8 @@ const WARN = 2; const INFO = 3; const DEBUG = 4; -let logger; -let level; +let logger: Logger; +let level: number; export class Log { static get NONE() {return NONE}; @@ -23,16 +30,16 @@ export class Log { static get WARN() {return WARN}; static get INFO() {return INFO}; static get DEBUG() {return DEBUG}; - + static reset(){ level = INFO; logger = nopLogger; } - + static get level(){ return level; } - static set level(value){ + static set level(value: number) { if (NONE <= value && value <= DEBUG){ level = value; } @@ -40,11 +47,12 @@ export class Log { throw new Error("Invalid log level"); } } - - static get logger(){ + + static get logger() { return logger; } - static set logger(value){ + static set logger(value) { + /* TODO port-ts if (!value.debug && value.info) { // just to stay backwards compat. can remove in 2.0 value.debug = value.info; @@ -55,25 +63,26 @@ export class Log { } else { throw new Error("Invalid logger"); - } + }*/ + logger = value; } - - static debug(...args){ + + static debug(...args: any[]) { if (level >= DEBUG){ logger.debug.apply(logger, Array.from(args)); } } - static info(...args){ + static info(...args: any[]) { if (level >= INFO){ logger.info.apply(logger, Array.from(args)); } } - static warn(...args){ + static warn(...args: any[]) { if (level >= WARN){ logger.warn.apply(logger, Array.from(args)); } } - static error(...args){ + static error(...args: any[]) { if (level >= ERROR){ logger.error.apply(logger, Array.from(args)); } diff --git a/src/MetadataService.js b/src/MetadataService.ts similarity index 72% rename from src/MetadataService.js rename to src/MetadataService.ts index 630662a3e..4dd3598dd 100644 --- a/src/MetadataService.js +++ b/src/MetadataService.ts @@ -1,13 +1,19 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { JsonService } from './JsonService.js'; +import { Log } from './Log'; +import { JsonService } from './JsonService'; +import { OidcClientSettings } from './OidcClientSettings'; +import { OidcMetadata } from './OidcMetadata'; const OidcMetadataUrlPath = '.well-known/openid-configuration'; export class MetadataService { - constructor(settings, JsonServiceCtor = JsonService) { + private _settings: OidcClientSettings + private _jsonService: JsonService + private _metadataUrl: string | undefined + + constructor(settings: OidcClientSettings, JsonServiceCtor = JsonService) { if (!settings) { Log.error("MetadataService: No settings passed to MetadataService"); throw new Error("settings"); @@ -15,9 +21,10 @@ export class MetadataService { this._settings = settings; this._jsonService = new JsonServiceCtor(['application/jwk-set+json']); + this._metadataUrl = undefined } - get metadataUrl() { + get metadataUrl(): string { if (!this._metadataUrl) { if (this._settings.metadataUrl) { this._metadataUrl = this._settings.metadataUrl; @@ -34,15 +41,15 @@ export class MetadataService { } } - return this._metadataUrl; + return this._metadataUrl || ""; } resetSigningKeys() { - this._settings = this._settings || {} + // TODO: port-TS this._settings = this._settings || {} this._settings.signingKeys = undefined } - getMetadata() { + getMetadata(): Promise> { if (this._settings.metadata) { Log.debug("MetadataService.getMetadata: Returning metadata from settings"); return Promise.resolve(this._settings.metadata); @@ -58,53 +65,52 @@ export class MetadataService { return this._jsonService.getJson(this.metadataUrl) .then(metadata => { Log.debug("MetadataService.getMetadata: json received"); - + var seed = this._settings.metadataSeed || {}; - this._settings.metadata = Object.assign({}, seed, metadata); + this._settings.metadata = Object.assign({}, seed, metadata) as Partial; return this._settings.metadata; }); } - getIssuer() { - return this._getMetadataProperty("issuer"); + getIssuer(): Promise { + return this._getMetadataProperty("issuer") as Promise; } - getAuthorizationEndpoint() { - return this._getMetadataProperty("authorization_endpoint"); + getAuthorizationEndpoint(): Promise { + return this._getMetadataProperty("authorization_endpoint") as Promise; } - getUserInfoEndpoint() { - return this._getMetadataProperty("userinfo_endpoint"); + getUserInfoEndpoint(): Promise { + return this._getMetadataProperty("userinfo_endpoint") as Promise; } - getTokenEndpoint(optional=true) { - return this._getMetadataProperty("token_endpoint", optional); + getTokenEndpoint(optional=true): Promise { + return this._getMetadataProperty("token_endpoint", optional) as Promise; } - getCheckSessionIframe() { - return this._getMetadataProperty("check_session_iframe", true); + getCheckSessionIframe(): Promise { + return this._getMetadataProperty("check_session_iframe", true) as Promise; } - getEndSessionEndpoint() { - return this._getMetadataProperty("end_session_endpoint", true); + getEndSessionEndpoint(): Promise { + return this._getMetadataProperty("end_session_endpoint", true) as Promise; } - getRevocationEndpoint() { - return this._getMetadataProperty("revocation_endpoint", true); + getRevocationEndpoint(): Promise { + return this._getMetadataProperty("revocation_endpoint", true) as Promise; } - getKeysEndpoint() { - return this._getMetadataProperty("jwks_uri", true); + getKeysEndpoint(): Promise { + return this._getMetadataProperty("jwks_uri", true) as Promise; } - _getMetadataProperty(name, optional=false) { + _getMetadataProperty(name: keyof OidcMetadata, optional=false) { Log.debug("MetadataService.getMetadataProperty for: " + name); return this.getMetadata().then(metadata => { Log.debug("MetadataService.getMetadataProperty: metadata recieved"); if (metadata[name] === undefined) { - if (optional === true) { Log.warn("MetadataService.getMetadataProperty: Metadata does not contain optional property " + name); return undefined; @@ -128,7 +134,7 @@ export class MetadataService { return this._getMetadataProperty("jwks_uri").then(jwks_uri => { Log.debug("MetadataService.getSigningKeys: jwks_uri received", jwks_uri); - return this._jsonService.getJson(jwks_uri).then(keySet => { + return this._jsonService.getJson(jwks_uri as string /*TODO: port-TS*/).then(keySet => { Log.debug("MetadataService.getSigningKeys: key set received", keySet); if (!keySet.keys) { diff --git a/src/OidcClient.js b/src/OidcClient.ts similarity index 78% rename from src/OidcClient.js rename to src/OidcClient.ts index 717f69b2d..7d639e469 100644 --- a/src/OidcClient.js +++ b/src/OidcClient.ts @@ -1,18 +1,21 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { OidcClientSettings } from './OidcClientSettings.js'; -import { ErrorResponse } from './ErrorResponse.js'; -import { SigninRequest } from './SigninRequest.js'; -import { SigninResponse } from './SigninResponse.js'; -import { SignoutRequest } from './SignoutRequest.js'; -import { SignoutResponse } from './SignoutResponse.js'; -import { SigninState } from './SigninState.js'; -import { State } from './State.js'; +import { Log } from './Log'; +import { OidcClientSettings } from './OidcClientSettings'; +import { ErrorResponse } from './ErrorResponse'; +import { SigninRequest } from './SigninRequest'; +import { SigninResponse } from './SigninResponse'; +import { SignoutRequest } from './SignoutRequest'; +import { SignoutResponse } from './SignoutResponse'; +import { SigninState } from './SigninState'; +import { StateStore } from './StateStore'; +import { State } from './State'; export class OidcClient { - constructor(settings = {}) { + protected _settings: OidcClientSettings + + constructor(settings: any = {}) { if (settings instanceof OidcClientSettings) { this._settings = settings; } @@ -39,13 +42,14 @@ export class OidcClient { } createSigninRequest({ - response_type, scope, redirect_uri, - // data was meant to be the place a caller could indicate the data to - // have round tripped, but people were getting confused, so i added state (since that matches the spec) - // and so now if data is not passed, but state is then state will be used - data, state, prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values, - resource, request, request_uri, response_mode, extraQueryParams, extraTokenParams, request_type, skipUserInfo } = {}, - stateStore + response_type, scope, redirect_uri, + // data was meant to be the place a caller could indicate the data to + // have round tripped, but people were getting confused, so i added state (since that matches the spec) + // and so now if data is not passed, but state is then state will be used + data, state, prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values, + resource, request, request_uri, response_mode, extraQueryParams, extraTokenParams, request_type, skipUserInfo + }: any = {}, + stateStore?: StateStore ) { Log.debug("OidcClient.createSigninRequest"); @@ -71,7 +75,7 @@ export class OidcClient { return Promise.reject(new Error("OpenID Connect hybrid flow is not supported")); } - return this._metadataService.getAuthorizationEndpoint().then(url => { + return this._metadataService.getAuthorizationEndpoint().then((url) => { Log.debug("OidcClient.createSigninRequest: Received authorization endpoint", url); let signinRequest = new SigninRequest({ @@ -97,11 +101,12 @@ export class OidcClient { }); } - readSigninResponseState(url, stateStore, removeState = false) { + readSigninResponseState(url: string, stateStore: StateStore | null = null, removeState = false) { Log.debug("OidcClient.readSigninResponseState"); - let useQuery = this._settings.response_mode === "query" || - (!this._settings.response_mode && SigninRequest.isCode(this._settings.response_type)); + let useQuery = this._settings.response_mode === "query" || + (!this._settings.response_mode && + this._settings.response_type && SigninRequest.isCode(this._settings.response_type)); let delimiter = useQuery ? "?" : "#"; var response = new SigninResponse(url, delimiter); @@ -126,7 +131,7 @@ export class OidcClient { }); } - processSigninResponse(url, stateStore) { + processSigninResponse(url: string, stateStore: StateStore | null = null) { Log.debug("OidcClient.processSigninResponse"); return this.readSigninResponseState(url, stateStore, true).then(({state, response}) => { @@ -135,8 +140,10 @@ export class OidcClient { }); } - createSignoutRequest({id_token_hint, data, state, post_logout_redirect_uri, extraQueryParams, request_type } = {}, - stateStore + createSignoutRequest({ + id_token_hint, data, state, post_logout_redirect_uri, extraQueryParams, request_type + }: any = {}, + stateStore: StateStore | null = null ) { Log.debug("OidcClient.createSignoutRequest"); @@ -172,7 +179,8 @@ export class OidcClient { }); } - readSignoutResponseState(url, stateStore, removeState = false) { + readSignoutResponseState(url: string, stateStore: StateStore | null = null, removeState = false) + : Promise<{ state: undefined | State, response: SignoutResponse }> { Log.debug("OidcClient.readSignoutResponseState"); var response = new SignoutResponse(url); @@ -204,7 +212,7 @@ export class OidcClient { }); } - processSignoutResponse(url, stateStore) { + processSignoutResponse(url: string, stateStore: StateStore | null = null) { Log.debug("OidcClient.processSignoutResponse"); return this.readSignoutResponseState(url, stateStore, true).then(({state, response}) => { @@ -219,7 +227,7 @@ export class OidcClient { }); } - clearStaleState(stateStore) { + clearStaleState(stateStore: StateStore | null = null) { Log.debug("OidcClient.clearStaleState"); stateStore = stateStore || this._stateStore; diff --git a/src/OidcClientSettings.js b/src/OidcClientSettings.ts similarity index 62% rename from src/OidcClientSettings.js rename to src/OidcClientSettings.ts index 530d4c9e1..e6d1d802c 100644 --- a/src/OidcClientSettings.js +++ b/src/OidcClientSettings.ts @@ -1,11 +1,13 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { ClockService } from './ClockService.js'; -import { WebStorageStateStore } from './WebStorageStateStore.js'; -import { ResponseValidator } from './ResponseValidator.js'; -import { MetadataService } from './MetadataService.js'; +import { Log } from './Log'; +import { ClockService } from './ClockService'; +import { WebStorageStateStore } from './WebStorageStateStore'; +import { ResponseValidator } from './ResponseValidator'; +import { MetadataService } from './MetadataService'; +import { OidcMetadata } from './OidcMetadata' +import { StateStore } from './StateStore' const OidcMetadataUrlPath = '.well-known/openid-configuration'; @@ -15,19 +17,109 @@ const DefaultClientAuthentication = "client_secret_post" // The default value mu const DefaultStaleStateAge = 60 * 15; // seconds const DefaultClockSkewInSeconds = 60 * 5; +export interface OidcClientArgs { + /** The URL of the OIDC/OAuth2 provider */ + authority?: string, + metadataUrl?: string, + /** Provide metadata when authority server does not allow CORS on the metadata endpoint */ + metadata?: Partial, + /** Can be used to seed or add additional values to the results of the discovery request */ + metadataSeed?: Partial, + /** Provide signingKeys when authority server does not allow CORS on the jwks uri */ + signingKeys?: any[], + + /** Your client application's identifier as registered with the OIDC/OAuth2 */ + client_id?: string, + client_secret?: string, + /** The type of response desired from the OIDC/OAuth2 provider (default: 'id_token') */ + response_type?: string, + /** The scope being requested from the OIDC/OAuth2 provider (default: 'openid') */ + scope?: string, + /** The redirect URI of your client application to receive a response from the OIDC/OAuth2 provider */ + redirect_uri?: string, + /** The OIDC/OAuth2 post-logout redirect URI */ + post_logout_redirect_uri?: string, + client_authentication?: string, + + prompt?: string; + display?: string; + max_age?: number; + ui_locales?: string; + acr_values?: string; + resource?: string; + response_mode?: string; + + /** Should OIDC protocol claims be removed from profile (default: true) */ + filterProtocolClaims?: boolean; + /** Flag to control if additional identity data is loaded from the user info endpoint in order to populate the user's profile (default: true) */ + loadUserInfo?: boolean; + /** Number (in seconds) indicating the age of state entries in storage for authorize requests that are considered abandoned and thus can be cleaned up (default: 300) */ + staleStateAge?: number; + /** The window of time (in seconds) to allow the current time to deviate when validating id_token's iat, nbf, and exp values (default: 300) */ + clockSkew?: number; + clockService?: ClockService; + userInfoJwtIssuer?: 'ANY' | 'OP' | string; + mergeClaims?: boolean; + + stateStore?: StateStore; + ResponseValidatorCtor?: typeof ResponseValidator; + MetadataServiceCtor?: typeof MetadataService; + + /** An object containing additional query string parameters to be including in the authorization request */ + extraQueryParams?: Record; + extraTokenParams?: Record; +} + export class OidcClientSettings { + private _authority?: string; + private _metadataUrl?: string; + private _metadata?: Partial; + private _metadataSeed?: Partial; + private _signingKeys?: any[]; + + private _client_id: string; + private _client_secret?: string; + private _response_type: string; + private _scope: string; + private _redirect_uri?: string; + private _post_logout_redirect_uri?: string; + private _client_authentication?: string; + + private _prompt?: string; + private _display?: string; + private _max_age?: number; + private _ui_locales?: string; + private _acr_values?: string; + private _resource?: string; + private _response_mode?: string; + + private _filterProtocolClaims?: boolean; + private _loadUserInfo?: boolean; + private _staleStateAge: number; + private _clockSkew: number; + private _clockService: ClockService; + private _userInfoJwtIssuer?: 'ANY' | 'OP' | string; + private _mergeClaims?: boolean; + + private _stateStore: StateStore; + private _validator: ResponseValidator; + private _metadataService: MetadataService; + + private _extraQueryParams?: Record; + private _extraTokenParams?: Record; + constructor({ // metadata related authority, metadataUrl, metadata, signingKeys, metadataSeed, // client related - client_id, client_secret, response_type = DefaultResponseType, scope = DefaultScope, + client_id = "", client_secret, response_type = DefaultResponseType, scope = DefaultScope, redirect_uri, post_logout_redirect_uri, client_authentication = DefaultClientAuthentication, // optional protocol prompt, display, max_age, ui_locales, acr_values, resource, response_mode, // behavior flags filterProtocolClaims = true, loadUserInfo = true, - staleStateAge = DefaultStaleStateAge, + staleStateAge = DefaultStaleStateAge, clockSkew = DefaultClockSkewInSeconds, clockService = new ClockService(), userInfoJwtIssuer = 'OP', @@ -39,7 +131,7 @@ export class OidcClientSettings { // extra query params extraQueryParams = {}, extraTokenParams = {} - } = {}) { + }: OidcClientArgs = {}) { this._authority = authority; this._metadataUrl = metadataUrl; @@ -111,7 +203,7 @@ export class OidcClientSettings { get client_authentication() { return this._client_authentication; } - + // optional protocol params get prompt() { @@ -206,7 +298,7 @@ export class OidcClientSettings { get mergeClaims() { return this._mergeClaims; } - + get stateStore() { return this._stateStore; } diff --git a/src/OidcMetadata.ts b/src/OidcMetadata.ts new file mode 100644 index 000000000..39accfed5 --- /dev/null +++ b/src/OidcMetadata.ts @@ -0,0 +1,39 @@ +export interface OidcMetadata { + issuer: string; + authorization_endpoint: string; + token_endpoint: string; + token_endpoint_auth_methods_supported: string[]; + token_endpoint_auth_signing_alg_values_supported: string[]; + userinfo_endpoint: string; + check_session_iframe: string; + end_session_endpoint: string; + jwks_uri: string; + registration_endpoint: string; + scopes_supported: string[]; + response_types_supported: string[]; + acr_values_supported: string[]; + subject_types_supported: string[]; + userinfo_signing_alg_values_supported: string[]; + userinfo_encryption_alg_values_supported: string[]; + userinfo_encryption_enc_values_supported: string[]; + id_token_signing_alg_values_supported: string[]; + id_token_encryption_alg_values_supported: string[]; + id_token_encryption_enc_values_supported: string[]; + request_object_signing_alg_values_supported: string[]; + display_values_supported: string[]; + claim_types_supported: string[]; + claims_supported: string[]; + claims_parameter_supported: boolean; + service_documentation: string; + ui_locales_supported: string[]; + + revocation_endpoint: string; + introspection_endpoint: string; + frontchannel_logout_supported: boolean; + frontchannel_logout_session_supported: boolean; + backchannel_logout_supported: boolean; + backchannel_logout_session_supported: boolean; + grant_types_supported: string[]; + response_modes_supported: string[]; + code_challenge_methods_supported: string[]; +} diff --git a/src/PopupNavigator.js b/src/PopupNavigator.ts similarity index 66% rename from src/PopupNavigator.js rename to src/PopupNavigator.ts index e2a665365..3681a2388 100644 --- a/src/PopupNavigator.js +++ b/src/PopupNavigator.ts @@ -1,17 +1,18 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { PopupWindow } from './PopupWindow.js'; +import { Log } from './Log'; +import { PopupWindow } from './PopupWindow'; +import { INavigator } from './INavigator'; -export class PopupNavigator { +export class PopupNavigator implements INavigator { - prepare(params) { + prepare(params: any) { let popup = new PopupWindow(params); return Promise.resolve(popup); } - callback(url, keepOpen, delimiter) { + callback(url: string, keepOpen: boolean, delimiter: string) { Log.debug("PopupNavigator.callback"); try { diff --git a/src/PopupWindow.js b/src/PopupWindow.ts similarity index 81% rename from src/PopupWindow.js rename to src/PopupWindow.ts index d69dc49f5..4eecc4cd3 100644 --- a/src/PopupWindow.js +++ b/src/PopupWindow.ts @@ -1,8 +1,9 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { UrlUtility } from './UrlUtility.js'; +import { Log } from './Log'; +import { UrlUtility } from './UrlUtility'; +import { IWindow } from './IWindow'; const CheckForPopupClosedInterval = 500; const DefaultPopupFeatures = 'location=no,toolbar=no,width=500,height=500,left=100,top=100;'; @@ -10,9 +11,15 @@ const DefaultPopupFeatures = 'location=no,toolbar=no,width=500,height=500,left=1 const DefaultPopupTarget = "_blank"; -export class PopupWindow { +export class PopupWindow implements IWindow { + private _promise: Promise; + private _resolve!: (value: unknown) => void; + private _reject!: (reason?: any) => void; + private _popup: Window | null; + private _checkForPopupClosedTimer: number | null; + private _id: any; - constructor(params) { + constructor(params: any) { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; @@ -22,6 +29,7 @@ export class PopupWindow { let features = params.popupWindowFeatures || DefaultPopupFeatures; this._popup = window.open('', target, features); + this._checkForPopupClosedTimer = null; if (this._popup) { Log.debug("PopupWindow.ctor: popup successfully created"); this._checkForPopupClosedTimer = window.setInterval(this._checkForPopupClosed.bind(this), CheckForPopupClosedInterval); @@ -32,7 +40,7 @@ export class PopupWindow { return this._promise; } - navigate(params) { + navigate(params: any) { if (!this._popup) { this._error("PopupWindow.navigate: Error opening popup window"); } @@ -45,6 +53,7 @@ export class PopupWindow { this._id = params.id; if (this._id) { + // @ts-ignore window["popupCallback_" + params.id] = this._callback.bind(this); } @@ -55,15 +64,15 @@ export class PopupWindow { return this.promise; } - _success(data) { + _success(data: any) { Log.debug("PopupWindow.callback: Successful response from popup window"); this._cleanup(); this._resolve(data); } - _error(message) { + _error(message: string) { Log.error("PopupWindow.error: ", message); - + this._cleanup(); this._reject(new Error(message)); } @@ -72,12 +81,13 @@ export class PopupWindow { this._cleanup(false); } - _cleanup(keepOpen) { + _cleanup(keepOpen?: boolean) { Log.debug("PopupWindow.cleanup"); - window.clearInterval(this._checkForPopupClosedTimer); + window.clearInterval(this._checkForPopupClosedTimer!); this._checkForPopupClosedTimer = null; + // @ts-ignore delete window["popupCallback_" + this._id]; if (this._popup && !keepOpen) { @@ -92,7 +102,7 @@ export class PopupWindow { } } - _callback(url, keepOpen) { + _callback(url: string, keepOpen: boolean) { this._cleanup(keepOpen); if (url) { @@ -105,7 +115,7 @@ export class PopupWindow { } } - static notifyOpener(url, keepOpen, delimiter) { + static notifyOpener(url: string, keepOpen: boolean, delimiter: string) { if (window.opener) { url = url || window.location.href; if (url) { @@ -113,6 +123,7 @@ export class PopupWindow { if (data.state) { var name = "popupCallback_" + data.state; + // @ts-ignore var callback = window.opener[name]; if (callback) { Log.debug("PopupWindow.notifyOpener: passing url message to opener"); diff --git a/src/RedirectNavigator.js b/src/RedirectNavigator.ts similarity index 73% rename from src/RedirectNavigator.js rename to src/RedirectNavigator.ts index fa3824438..e253cc6da 100644 --- a/src/RedirectNavigator.js +++ b/src/RedirectNavigator.ts @@ -1,15 +1,17 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; +import { INavigator } from './INavigator'; +import { IWindow } from './IWindow'; -export class RedirectNavigator { +export class RedirectNavigator implements INavigator, IWindow { - prepare() { + prepare(_params: any) { return Promise.resolve(this); } - navigate(params) { + navigate(params: any) { if (!params || !params.url) { Log.error("RedirectNavigator.navigate: No url provided"); return Promise.reject(new Error("No url provided")); @@ -28,4 +30,7 @@ export class RedirectNavigator { get url() { return window.location.href; } + + close() { + } } diff --git a/src/ResponseValidator.js b/src/ResponseValidator.ts similarity index 89% rename from src/ResponseValidator.js rename to src/ResponseValidator.ts index 2d56f9ad0..23632e304 100644 --- a/src/ResponseValidator.js +++ b/src/ResponseValidator.ts @@ -1,20 +1,30 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { MetadataService } from './MetadataService.js'; -import { UserInfoService } from './UserInfoService.js'; -import { TokenClient } from './TokenClient.js'; -import { ErrorResponse } from './ErrorResponse.js'; -import { JoseUtil } from './JoseUtil.js'; +import { Log } from './Log'; +import { MetadataService } from './MetadataService'; +import { UserInfoService } from './UserInfoService'; +import { TokenClient } from './TokenClient'; +import { ErrorResponse } from './ErrorResponse'; +import { JoseUtil } from './JoseUtil'; +import { OidcClientSettings } from './OidcClientSettings'; +import { SigninState } from './SigninState'; +import { SigninResponse } from './SigninResponse'; +import { State } from './State'; +import { SignoutResponse } from './SignoutResponse'; const ProtocolClaims = ["nonce", "at_hash", "iat", "nbf", "exp", "aud", "iss", "c_hash"]; export class ResponseValidator { + private _settings: OidcClientSettings; + private _metadataService: MetadataService; + private _userInfoService: UserInfoService; + private _joseUtil: typeof JoseUtil; + private _tokenClient: TokenClient; - constructor(settings, + constructor(settings: OidcClientSettings, MetadataServiceCtor = MetadataService, - UserInfoServiceCtor = UserInfoService, + UserInfoServiceCtor = UserInfoService, joseUtil = JoseUtil, TokenClientCtor = TokenClient) { if (!settings) { @@ -29,7 +39,7 @@ export class ResponseValidator { this._tokenClient = new TokenClientCtor(this._settings); } - validateSigninResponse(state, response) { + validateSigninResponse(state: SigninState, response: SigninResponse) { Log.debug("ResponseValidator.validateSigninResponse"); return this._processSigninParams(state, response).then(response => { @@ -44,7 +54,7 @@ export class ResponseValidator { }); } - validateSignoutResponse(state, response) { + validateSignoutResponse(state: State, response: SignoutResponse) { if (state.id !== response.state) { Log.error("ResponseValidator.validateSignoutResponse: State does not match"); return Promise.reject(new Error("State does not match")); @@ -64,7 +74,7 @@ export class ResponseValidator { return Promise.resolve(response); } - _processSigninParams(state, response) { + _processSigninParams(state: SigninState, response: SigninResponse) { if (state.id !== response.state) { Log.error("ResponseValidator._processSigninParams: State does not match"); return Promise.reject(new Error("State does not match")); @@ -138,7 +148,7 @@ export class ResponseValidator { return Promise.resolve(response); } - _processClaims(state, response) { + _processClaims(state: SigninState, response: SigninResponse): Promise { if (response.isOpenIdConnect) { Log.debug("ResponseValidator._processClaims: response is OIDC, processing claims"); @@ -172,7 +182,7 @@ export class ResponseValidator { return Promise.resolve(response); } - _mergeClaims(claims1, claims2) { + _mergeClaims(claims1: any, claims2: any) { var result = Object.assign({}, claims1); for (let name in claims2) { @@ -205,12 +215,12 @@ export class ResponseValidator { return result; } - _filterProtocolClaims(claims) { + _filterProtocolClaims(claims: any) { Log.debug("ResponseValidator._filterProtocolClaims, incoming claims:", claims); var result = Object.assign({}, claims); - if (this._settings._filterProtocolClaims) { + if (this._settings.filterProtocolClaims) { ProtocolClaims.forEach(type => { delete result[type]; }); @@ -224,7 +234,7 @@ export class ResponseValidator { return result; } - _validateTokens(state, response) { + _validateTokens(state: SigninState, response: SigninResponse) { if (response.code) { Log.debug("ResponseValidator._validateTokens: Validating code"); return this._processCode(state, response); @@ -244,7 +254,7 @@ export class ResponseValidator { return Promise.resolve(response); } - _processCode(state, response) { + _processCode(state: SigninState, response: SigninResponse): Promise { var request = { client_id: state.client_id, client_secret: state.client_secret, @@ -256,12 +266,13 @@ export class ResponseValidator { if (state.extraTokenParams && typeof(state.extraTokenParams) === 'object') { Object.assign(request, state.extraTokenParams); } - + return this._tokenClient.exchangeCode(request).then(tokenResponse => { - - for(var key in tokenResponse) { + + /*TODO: port-TS for(var key in tokenResponse) { response[key] = tokenResponse[key]; - } + }*/ + response = tokenResponse if (response.id_token) { Log.debug("ResponseValidator._processCode: token response successful, processing id_token"); @@ -270,12 +281,12 @@ export class ResponseValidator { else { Log.debug("ResponseValidator._processCode: token response successful, returning response"); } - + return response; }); } - _validateIdTokenAttributes(state, response) { + _validateIdTokenAttributes(state: SigninState, response: SigninResponse) { return this._metadataService.getIssuer().then(issuer => { let audience = state.client_id; @@ -284,17 +295,17 @@ export class ResponseValidator { return this._settings.getEpochTime().then(now => { return this._joseUtil.validateJwtAttributes(response.id_token, issuer, audience, clockSkewInSeconds, now).then(payload => { - + if (state.nonce && state.nonce !== payload.nonce) { Log.error("ResponseValidator._validateIdTokenAttributes: Invalid nonce in id_token"); return Promise.reject(new Error("Invalid nonce in id_token")); } - + if (!payload.sub) { Log.error("ResponseValidator._validateIdTokenAttributes: No sub present in id_token"); return Promise.reject(new Error("No sub present in id_token")); } - + response.profile = payload; return response; }); @@ -302,13 +313,13 @@ export class ResponseValidator { }); } - _validateIdTokenAndAccessToken(state, response) { + _validateIdTokenAndAccessToken(state: SigninState, response: SigninResponse) { return this._validateIdToken(state, response).then(response => { return this._validateAccessToken(response); }); } - _getSigningKeyForJwt(jwt) { + _getSigningKeyForJwt(jwt: any) { return this._metadataService.getSigningKeys().then(keys => { const kid = jwt.header.kid; if (!keys) { @@ -338,7 +349,7 @@ export class ResponseValidator { }); } - _getSigningKeyForJwtWithSingleRetry(jwt) { + _getSigningKeyForJwtWithSingleRetry(jwt: any) { return this._getSigningKeyForJwt(jwt).then(key => { // Refreshing signingKeys if no suitable verification key is present for given jwt header. if (!key) { @@ -351,13 +362,13 @@ export class ResponseValidator { }); } - _validateIdToken(state, response) { + _validateIdToken(state: SigninState, response: SigninResponse): Promise { if (!state.nonce) { Log.error("ResponseValidator._validateIdToken: No nonce on state"); return Promise.reject(new Error("No nonce on state")); } - let jwt = this._joseUtil.parseJwt(response.id_token); + const jwt = this._joseUtil.parseJwt(response.id_token); if (!jwt || !jwt.header || !jwt.payload) { Log.error("ResponseValidator._validateIdToken: Failed to parse id_token", jwt); return Promise.reject(new Error("Failed to parse id_token")); @@ -390,15 +401,14 @@ export class ResponseValidator { } response.profile = jwt.payload; - return response; }); }); }); } - _filterByAlg(keys, alg){ - var kty = null; + _filterByAlg(keys: any[], alg: string){ + var kty: any = null; if (alg.startsWith("RS")) { kty = "RSA"; } @@ -424,7 +434,7 @@ export class ResponseValidator { return keys; } - _validateAccessToken(response) { + _validateAccessToken(response: SigninResponse) { if (!response.profile) { Log.error("ResponseValidator._validateAccessToken: No profile loaded from id_token"); return Promise.reject(new Error("No profile loaded from id_token")); @@ -440,7 +450,7 @@ export class ResponseValidator { return Promise.reject(new Error("No id_token")); } - let jwt = this._joseUtil.parseJwt(response.id_token); + const jwt = this._joseUtil.parseJwt(response.id_token); if (!jwt || !jwt.header) { Log.error("ResponseValidator._validateAccessToken: Failed to parse id_token", jwt); return Promise.reject(new Error("Failed to parse id_token")); diff --git a/src/SessionMonitor.js b/src/SessionMonitor.ts similarity index 81% rename from src/SessionMonitor.js rename to src/SessionMonitor.ts index b9e07c2b1..a2b64c76e 100644 --- a/src/SessionMonitor.js +++ b/src/SessionMonitor.ts @@ -1,13 +1,21 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { CheckSessionIFrame } from './CheckSessionIFrame.js'; -import { Global } from './Global.js'; +import { Log } from './Log'; +import { CheckSessionIFrame } from './CheckSessionIFrame'; +import { Global } from './Global'; +import { UserManager } from './UserManager'; +import { IntervalTimer } from './Timer'; export class SessionMonitor { - - constructor(userManager, CheckSessionIFrameCtor = CheckSessionIFrame, timer = Global.timer) { + private _userManager: UserManager; + private _CheckSessionIFrameCtor: typeof CheckSessionIFrame; + private _timer: IntervalTimer; + private _sub: any; + private _sid: any; + private _checkSessionIFrame?: CheckSessionIFrame; + + constructor(userManager: UserManager, CheckSessionIFrameCtor = CheckSessionIFrame, timer = Global.timer) { if (!userManager) { Log.error("SessionMonitor.ctor: No user manager passed to SessionMonitor"); throw new Error("userManager"); @@ -21,30 +29,28 @@ export class SessionMonitor { this._userManager.events.addUserUnloaded(this._stop.bind(this)); Promise.resolve(this._userManager.getUser().then(user => { - // doing this manually here since calling getUser + // doing this manually here since calling getUser // doesn't trigger load event. if (user) { this._start(user); } else if (this._settings.monitorAnonymousSession) { - this._userManager.querySessionStatus().then(session => { + this._userManager.querySessionStatus().then((session: any) => { let tmpUser = { - session_state : session.session_state - }; - if (session.sub && session.sid) { - tmpUser.profile = { + session_state: session.session_state, + profile: session.sub && session.sid ? { sub: session.sub, sid: session.sid - }; - } + } : null + }; this._start(tmpUser); }) - .catch(err => { + .catch((err: Error) => { // catch to suppress errors since we're in a ctor Log.error("SessionMonitor ctor: error from querySessionStatus:", err.message); }); } - }).catch(err => { + }).catch((err: Error) => { // catch to suppress errors since we're in a ctor Log.error("SessionMonitor ctor: error from getUser:", err.message); })); @@ -66,7 +72,7 @@ export class SessionMonitor { return this._settings.stopCheckSessionOnError; } - _start(user) { + _start(user: any) { let session_state = user.session_state; if (session_state) { @@ -92,6 +98,7 @@ export class SessionMonitor { this._checkSessionIFrame = new this._CheckSessionIFrameCtor(this._callback.bind(this), client_id, url, interval, stopOnError); this._checkSessionIFrame.load().then(() => { + this._checkSessionIFrame && this._checkSessionIFrame.start(session_state); }); } @@ -123,19 +130,17 @@ export class SessionMonitor { let timerHandle = this._timer.setInterval(()=>{ this._timer.clearInterval(timerHandle); - this._userManager.querySessionStatus().then(session => { + this._userManager.querySessionStatus().then((session: any) => { let tmpUser = { - session_state : session.session_state - }; - if (session.sub && session.sid) { - tmpUser.profile = { + session_state: session.session_state, + profile: session.sub && session.sid ? { sub: session.sub, sid: session.sid - }; - } + } : null + }; this._start(tmpUser); }) - .catch(err => { + .catch((err: Error) => { // catch to suppress errors since we're in a callback Log.error("SessionMonitor: error from querySessionStatus:", err.message); }); @@ -145,10 +150,10 @@ export class SessionMonitor { } _callback() { - this._userManager.querySessionStatus().then(session => { + this._userManager.querySessionStatus().then((session: any) => { var raiseEvent = true; - if (session) { + if (session && this._checkSessionIFrame) { if (session.sub === this._sub) { raiseEvent = false; this._checkSessionIFrame.start(session.session_state); @@ -179,7 +184,7 @@ export class SessionMonitor { this._userManager.events._raiseUserSignedIn(); } } - }).catch(err => { + }).catch((err: Error) => { if (this._sub) { Log.debug("SessionMonitor._callback: Error calling queryCurrentSigninSession; raising signed out event", err.message); this._userManager.events._raiseUserSignedOut(); diff --git a/src/SigninRequest.js b/src/SigninRequest.ts similarity index 83% rename from src/SigninRequest.js rename to src/SigninRequest.ts index 67c04c672..c95cfc240 100644 --- a/src/SigninRequest.js +++ b/src/SigninRequest.ts @@ -1,18 +1,21 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { UrlUtility } from './UrlUtility.js'; -import { SigninState } from './SigninState.js'; +import { Log } from './Log'; +import { UrlUtility } from './UrlUtility'; +import { SigninState } from './SigninState'; export class SigninRequest { + public readonly url: string; + public readonly state: any; + constructor({ // mandatory url, client_id, redirect_uri, response_type, scope, authority, // optional data, prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values, resource, response_mode, request, request_uri, extraQueryParams, request_type, client_secret, extraTokenParams, skipUserInfo - }) { + }: any) { if (!url) { Log.error("SigninRequest.ctor: No url passed"); throw new Error("url"); @@ -45,9 +48,9 @@ export class SigninRequest { response_mode = SigninRequest.isCode(response_type) ? "query" : null; } - this.state = new SigninState({ nonce: oidc, - data, client_id, authority, redirect_uri, - code_verifier: code, + this.state = new SigninState({ nonce: oidc, + data, client_id, authority, redirect_uri, + code_verifier: code, request_type, response_mode, client_secret, scope, extraTokenParams, skipUserInfo }); @@ -65,7 +68,7 @@ export class SigninRequest { url = UrlUtility.addQueryParam(url, "code_challenge_method", "S256"); } - var optional = { prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values, resource, request, request_uri, response_mode }; + var optional: any = { prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values, resource, request, request_uri, response_mode }; for(let key in optional){ if (optional[key]) { url = UrlUtility.addQueryParam(url, key, optional[key]); @@ -79,21 +82,21 @@ export class SigninRequest { this.url = url; } - static isOidc(response_type) { + static isOidc(response_type: string) { var result = response_type.split(/\s+/g).filter(function(item) { return item === "id_token"; }); return !!(result[0]); } - static isOAuth(response_type) { + static isOAuth(response_type: string) { var result = response_type.split(/\s+/g).filter(function(item) { return item === "token"; }); return !!(result[0]); } - - static isCode(response_type) { + + static isCode(response_type: string) { var result = response_type.split(/\s+/g).filter(function(item) { return item === "code"; }); diff --git a/src/SigninResponse.js b/src/SigninResponse.ts similarity index 61% rename from src/SigninResponse.js rename to src/SigninResponse.ts index 5dea43560..b41f80587 100644 --- a/src/SigninResponse.js +++ b/src/SigninResponse.ts @@ -1,12 +1,27 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { UrlUtility } from './UrlUtility.js'; +import { UrlUtility } from './UrlUtility'; const OidcScope = "openid"; export class SigninResponse { - constructor(url, delimiter = "#") { + public readonly error: string; + public readonly error_description: string; + public readonly error_uri: string; + + public readonly code: string; + public state: string; + public readonly id_token: string; + public readonly session_state: string; + public readonly access_token: string; + public readonly token_type: string; + public scope: string; + public profile: any | undefined; // will be set from ResponseValidator + + public readonly expires_at: number | undefined + + constructor(url: string, delimiter = "#") { var values = UrlUtility.parseUrlFragment(url, delimiter); @@ -23,23 +38,21 @@ export class SigninResponse { this.scope = values.scope; this.profile = undefined; // will be set from ResponseValidator - this.expires_in = values.expires_in; + this.expires_at = undefined + let expires_in = parseInt(values.expires_in); + if (expires_in > 0) { + let now = Math.floor(Date.now() / 1000); + this.expires_at = now + expires_in; + } } - get expires_in() { + get expires_in(): number | undefined { if (this.expires_at) { - let now = parseInt(Date.now() / 1000); + let now = Math.floor(Date.now() / 1000); return this.expires_at - now; } return undefined; } - set expires_in(value){ - let expires_in = parseInt(value); - if (typeof expires_in === 'number' && expires_in > 0) { - let now = parseInt(Date.now() / 1000); - this.expires_at = now + expires_in; - } - } get expired() { let expires_in = this.expires_in; diff --git a/src/SigninState.js b/src/SigninState.ts similarity index 79% rename from src/SigninState.js rename to src/SigninState.ts index 72a190eb7..e1cb67c2b 100644 --- a/src/SigninState.js +++ b/src/SigninState.ts @@ -1,13 +1,29 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { State } from './State.js'; -import { JoseUtil } from './JoseUtil.js'; -import random from './random.js'; +import { Log } from './Log'; +import { State } from './State'; +import { JoseUtil } from './JoseUtil'; +import random from './random'; export class SigninState extends State { - constructor({nonce, authority, client_id, redirect_uri, code_verifier, response_mode, client_secret, scope, extraTokenParams, skipUserInfo} = {}) { + private _nonce: any; + private _code_verifier: any; + private _code_challenge: any; + private _redirect_uri: any; + private _authority: any; + private _client_id: any; + private _response_mode: any; + private _client_secret: any; + private _scope: any; + private _extraTokenParams: any; + private _skipUserInfo: any; + + constructor({ + nonce, authority, client_id, + redirect_uri, code_verifier, response_mode, client_secret, + scope, extraTokenParams, skipUserInfo + }: any = {}) { super(arguments[0]); if (nonce === true) { @@ -24,7 +40,7 @@ export class SigninState extends State { else if (code_verifier) { this._code_verifier = code_verifier; } - + if (this.code_verifier) { let hash = JoseUtil.hashString(this.code_verifier, "SHA256"); this._code_challenge = JoseUtil.hexToBase64Url(hash); @@ -73,7 +89,7 @@ export class SigninState extends State { get skipUserInfo() { return this._skipUserInfo; } - + toStorageString() { Log.debug("SigninState.toStorageString"); return JSON.stringify({ @@ -94,7 +110,7 @@ export class SigninState extends State { }); } - static fromStorageString(storageString) { + static fromStorageString(storageString: string) { Log.debug("SigninState.fromStorageString"); var data = JSON.parse(storageString); return new SigninState(data); diff --git a/src/SignoutRequest.js b/src/SignoutRequest.ts similarity index 76% rename from src/SignoutRequest.js rename to src/SignoutRequest.ts index 2ed2951e4..1baa14a88 100644 --- a/src/SignoutRequest.js +++ b/src/SignoutRequest.ts @@ -1,12 +1,17 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { UrlUtility } from './UrlUtility.js'; -import { State } from './State.js'; +import { Log } from './Log'; +import { UrlUtility } from './UrlUtility'; +import { State } from './State'; export class SignoutRequest { - constructor({url, id_token_hint, post_logout_redirect_uri, data, extraQueryParams, request_type}) { + public readonly url: string + public readonly state?: State + + constructor({ + url, id_token_hint, post_logout_redirect_uri, data, extraQueryParams, request_type + }: any) { if (!url) { Log.error("SignoutRequest.ctor: No url passed"); throw new Error("url"); diff --git a/src/SignoutResponse.js b/src/SignoutResponse.ts similarity index 69% rename from src/SignoutResponse.js rename to src/SignoutResponse.ts index ee76482c9..385b034f8 100644 --- a/src/SignoutResponse.js +++ b/src/SignoutResponse.ts @@ -1,11 +1,15 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { UrlUtility } from './UrlUtility.js'; +import { UrlUtility } from './UrlUtility'; export class SignoutResponse { - constructor(url) { + public error?: string; + public error_description?: string; + public error_uri?: string; + public state?: any; + constructor(url: string) { var values = UrlUtility.parseUrlFragment(url, "?"); this.error = values.error; diff --git a/src/SilentRenewService.js b/src/SilentRenewService.ts similarity index 79% rename from src/SilentRenewService.js rename to src/SilentRenewService.ts index 13d2bcf87..0373c92d6 100644 --- a/src/SilentRenewService.js +++ b/src/SilentRenewService.ts @@ -1,11 +1,14 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; +import { UserManager } from './UserManager'; export class SilentRenewService { + private _userManager: UserManager; + private _callback: any; - constructor(userManager) { + constructor(userManager: UserManager) { this._userManager = userManager; } @@ -15,9 +18,9 @@ export class SilentRenewService { this._userManager.events.addAccessTokenExpiring(this._callback); // this will trigger loading of the user so the expiring events can be initialized - this._userManager.getUser().then(user=>{ + this._userManager.getUser().then(_user => { // deliberate nop - }).catch(err=>{ + }).catch(err => { // catch to suppress errors since we're in a ctor Log.error("SilentRenewService.start: Error from getUser:", err.message); }); @@ -32,7 +35,7 @@ export class SilentRenewService { } _tokenExpiring() { - this._userManager.signinSilent().then(user => { + this._userManager.signinSilent().then(_user => { Log.debug("SilentRenewService._tokenExpiring: Silent token renewal successful"); }, err => { Log.error("SilentRenewService._tokenExpiring: Error from signinSilent:", err.message); diff --git a/src/State.js b/src/State.ts similarity index 83% rename from src/State.js rename to src/State.ts index e156e2b60..a42b74fdc 100644 --- a/src/State.js +++ b/src/State.ts @@ -1,11 +1,19 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import random from './random.js'; +import { Log } from './Log'; +import random from './random'; +import { StateStore } from './StateStore'; export class State { - constructor({id, data, created, request_type} = {}) { + private _id: any; + private _data: any; + private _created: number; + private _request_type: any; + + constructor({ + id, data, created, request_type + }: any = {}) { this._id = id || random(); this._data = data; @@ -13,7 +21,7 @@ export class State { this._created = created; } else { - this._created = parseInt(Date.now() / 1000); + this._created = Date.now() / 1000; } this._request_type = request_type; } @@ -41,12 +49,12 @@ export class State { }); } - static fromStorageString(storageString) { + static fromStorageString(storageString: string) { Log.debug("State.fromStorageString"); return new State(JSON.parse(storageString)); } - static clearStaleState(storage, age) { + static clearStaleState(storage: StateStore, age: number) { var cutoff = Date.now() / 1000 - age; @@ -81,7 +89,7 @@ export class State { if (remove) { Log.debug("State.clearStaleState: removed item for key: ", key); - return storage.remove(key); + storage.remove(key); } }); diff --git a/src/StateStore.ts b/src/StateStore.ts new file mode 100644 index 000000000..a622b6626 --- /dev/null +++ b/src/StateStore.ts @@ -0,0 +1,6 @@ +export interface StateStore { + set(key: string, value: any): Promise; + get(key: string): Promise; + remove(key: string): Promise; + getAllKeys(): Promise; +} diff --git a/src/Timer.js b/src/Timer.ts similarity index 77% rename from src/Timer.js rename to src/Timer.ts index 421f56a27..b023d9f90 100644 --- a/src/Timer.js +++ b/src/Timer.ts @@ -1,15 +1,21 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { Global } from './Global.js'; -import { Event } from './Event.js'; +import { Log } from './Log'; +import { Global } from './Global'; +import { Event } from './Event'; const TimerDuration = 5; // seconds +export type IntervalTimer = { setInterval: (cb: (...args: any[]) => void, duration?: number | undefined) => number; clearInterval: (handle: number) => void; }; + export class Timer extends Event { + private _timer: IntervalTimer; + private _nowFunc: () => number; + private _timerHandle: number | null; + private _expiration: number; - constructor(name, timer = Global.timer, nowFunc = undefined) { + constructor(name: string, timer = Global.timer, nowFunc?: (() => number)) { super(name); this._timer = timer; @@ -19,17 +25,19 @@ export class Timer extends Event { else { this._nowFunc = () => Date.now() / 1000; } + + this._timerHandle = null; + this._expiration = 0; } get now() { - return parseInt(this._nowFunc()); + return this._nowFunc(); } - init(duration) { + init(duration: number) { if (duration <= 0) { duration = 1; } - duration = parseInt(duration); var expiration = this.now + duration; if (this.expiration === expiration && this._timerHandle) { @@ -52,7 +60,7 @@ export class Timer extends Event { } this._timerHandle = this._timer.setInterval(this._callback.bind(this), timerDuration * 1000); } - + get expiration() { return this._expiration; } diff --git a/src/TokenClient.js b/src/TokenClient.ts similarity index 83% rename from src/TokenClient.js rename to src/TokenClient.ts index 1fce56259..58116613a 100644 --- a/src/TokenClient.js +++ b/src/TokenClient.ts @@ -1,12 +1,18 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { JsonService } from './JsonService.js'; -import { MetadataService } from './MetadataService.js'; -import { Log } from './Log.js'; +import { JsonService } from './JsonService'; +import { MetadataService } from './MetadataService'; +import { Log } from './Log'; +import { OidcClientSettings } from './OidcClientSettings'; +import { SigninResponse } from './SigninResponse'; export class TokenClient { - constructor(settings, JsonServiceCtor = JsonService, MetadataServiceCtor = MetadataService) { + private _settings: OidcClientSettings; + private _jsonService: JsonService; + private _metadataService: MetadataService; + + constructor(settings: OidcClientSettings, JsonServiceCtor = JsonService, MetadataServiceCtor = MetadataService) { if (!settings) { Log.error("TokenClient.ctor: No settings passed"); throw new Error("settings"); @@ -17,7 +23,7 @@ export class TokenClient { this._metadataService = new MetadataServiceCtor(this._settings); } - exchangeCode(args = {}) { + exchangeCode(args:any = {}): Promise { args = Object.assign({}, args); args.grant_type = args.grant_type || "authorization_code"; @@ -25,8 +31,8 @@ export class TokenClient { args.client_secret = args.client_secret || this._settings.client_secret; args.redirect_uri = args.redirect_uri || this._settings.redirect_uri; - var basicAuth = undefined; - var client_authentication = args._client_authentication || this._settings._client_authentication; + var basicAuth: string | undefined = undefined; + var client_authentication = args._client_authentication || this._settings.client_authentication; delete args._client_authentication; if (!args.code) { @@ -67,15 +73,15 @@ export class TokenClient { }); } - exchangeRefreshToken(args = {}) { + exchangeRefreshToken(args: any = {}) { args = Object.assign({}, args); args.grant_type = args.grant_type || "refresh_token"; args.client_id = args.client_id || this._settings.client_id; args.client_secret = args.client_secret || this._settings.client_secret; - var basicAuth = undefined; - var client_authentication = args._client_authentication || this._settings._client_authentication; + var basicAuth: string | undefined = undefined; + var client_authentication = args._client_authentication || this._settings.client_authentication; delete args._client_authentication; if (!args.refresh_token) { diff --git a/src/TokenRevocationClient.js b/src/TokenRevocationClient.ts similarity index 79% rename from src/TokenRevocationClient.js rename to src/TokenRevocationClient.ts index eeaf3aa2c..df5ed786f 100644 --- a/src/TokenRevocationClient.js +++ b/src/TokenRevocationClient.ts @@ -1,15 +1,19 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { MetadataService } from './MetadataService.js'; -import { Global } from './Global.js'; +import { Log } from './Log'; +import { MetadataService } from './MetadataService'; +import { OidcClientSettings } from './OidcClientSettings'; const AccessTokenTypeHint = "access_token"; const RefreshTokenTypeHint = "refresh_token"; export class TokenRevocationClient { - constructor(settings, XMLHttpRequestCtor = Global.XMLHttpRequest, MetadataServiceCtor = MetadataService) { + private _settings: OidcClientSettings + private _XMLHttpRequestCtor: typeof XMLHttpRequest; + private _metadataService: MetadataService; + + constructor(settings: OidcClientSettings, XMLHttpRequestCtor = /*TODO: port-ts Global.*/XMLHttpRequest, MetadataServiceCtor = MetadataService) { if (!settings) { Log.error("TokenRevocationClient.ctor: No settings provided"); throw new Error("No settings provided."); @@ -20,7 +24,7 @@ export class TokenRevocationClient { this._metadataService = new MetadataServiceCtor(this._settings); } - revoke(token, required, type = "access_token") { + revoke(token: string, required: boolean, type = "access_token") { if (!token) { Log.error("TokenRevocationClient.revoke: No token provided"); throw new Error("No token provided."); @@ -49,9 +53,9 @@ export class TokenRevocationClient { }); } - _revoke(url, client_id, client_secret, token, type) { + _revoke(url: string, client_id: string, client_secret: string | undefined, token: string, type: string) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { var xhr = new this._XMLHttpRequestCtor(); xhr.open("POST", url); @@ -66,7 +70,7 @@ export class TokenRevocationClient { reject(Error(xhr.statusText + " (" + xhr.status + ")")); } }; - xhr.onerror = () => { + xhr.onerror = () => { Log.debug("TokenRevocationClient.revoke: Network Error.") reject("Network Error"); }; diff --git a/src/UrlUtility.js b/src/UrlUtility.ts similarity index 80% rename from src/UrlUtility.js rename to src/UrlUtility.ts index 7af42e2a2..920fb5094 100644 --- a/src/UrlUtility.js +++ b/src/UrlUtility.ts @@ -1,11 +1,10 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { Global } from './Global.js'; +import { Log } from './Log'; export class UrlUtility { - static addQueryParam(url, name, value) { + static addQueryParam(url: string, name: string, value: string) { if (url.indexOf('?') < 0) { url += "?"; } @@ -21,10 +20,11 @@ export class UrlUtility { return url; } - static parseUrlFragment(value, delimiter = "#", global = Global) { + static parseUrlFragment(value: string, delimiter = "#"/*, global = Global*/) { + /* TODO: port-ts if (typeof value !== 'string'){ value = global.location.href; - } + }*/ var idx = value.lastIndexOf(delimiter); if (idx >= 0) { @@ -39,8 +39,8 @@ export class UrlUtility { } } - var params = {}, - regex = /([^&=]+)=([^&]*)/g, + var params: { [name: string]: string } = {} + var regex = /([^&=]+)=([^&]*)/g, m; var counter = 0; @@ -54,10 +54,6 @@ export class UrlUtility { } } - for (var prop in params) { - return params; - } - - return {}; + return params; } } diff --git a/src/User.js b/src/User.ts similarity index 70% rename from src/User.js rename to src/User.ts index 6864e928c..6fc091201 100644 --- a/src/User.js +++ b/src/User.ts @@ -1,10 +1,22 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; +import { Log } from './Log'; export class User { - constructor({id_token, session_state, access_token, refresh_token, token_type, scope, profile, expires_at, state}) { + public id_token: string; + public session_state: any; + public access_token: any; + public refresh_token: string; + public token_type: string; + public scope: any; + public profile: any; + public state: any; + public expires_at: number; + + constructor({ + id_token, session_state, access_token, refresh_token, token_type, scope, profile, expires_at, state + }: any) { this.id_token = id_token; this.session_state = session_state; this.access_token = access_token; @@ -12,21 +24,21 @@ export class User { this.token_type = token_type; this.scope = scope; this.profile = profile; - this.expires_at = expires_at; this.state = state; + this.expires_at = expires_at; } get expires_in() { if (this.expires_at) { - let now = parseInt(Date.now() / 1000); + let now = Math.floor(Date.now() / 1000); return this.expires_at - now; } return undefined; } - set expires_in(value) { - let expires_in = parseInt(value); + set expires_in(value: number | undefined) { + let expires_in = value; if (typeof expires_in === 'number' && expires_in > 0) { - let now = parseInt(Date.now() / 1000); + let now = Math.floor(Date.now() / 1000); this.expires_at = now + expires_in; } } @@ -57,7 +69,7 @@ export class User { }); } - static fromStorageString(storageString) { + static fromStorageString(storageString: string) { Log.debug("User.fromStorageString"); return new User(JSON.parse(storageString)); } diff --git a/src/UserInfoService.js b/src/UserInfoService.ts similarity index 87% rename from src/UserInfoService.js rename to src/UserInfoService.ts index d2a6f361d..3747c5529 100644 --- a/src/UserInfoService.js +++ b/src/UserInfoService.ts @@ -1,16 +1,22 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { JsonService } from './JsonService.js'; -import { MetadataService } from './MetadataService.js'; -import { Log } from './Log.js'; -import { JoseUtil } from './JoseUtil.js'; +import { JsonService } from './JsonService'; +import { MetadataService } from './MetadataService'; +import { Log } from './Log'; +import { JoseUtil } from './JoseUtil'; +import { OidcClientSettings } from './OidcClientSettings'; export class UserInfoService { + private _settings: OidcClientSettings; + private _jsonService: JsonService; + private _metadataService: MetadataService; + private _joseUtil: typeof JoseUtil; + constructor( - settings, - JsonServiceCtor = JsonService, - MetadataServiceCtor = MetadataService, + settings: OidcClientSettings, + JsonServiceCtor = JsonService, + MetadataServiceCtor = MetadataService, joseUtil = JoseUtil ) { if (!settings) { @@ -24,7 +30,7 @@ export class UserInfoService { this._joseUtil = joseUtil; } - getClaims(token) { + getClaims(token?: string) { if (!token) { Log.error("UserInfoService.getClaims: No token passed"); return Promise.reject(new Error("A token is required")); @@ -40,9 +46,9 @@ export class UserInfoService { }); } - _getClaimsFromJwt(req) { + _getClaimsFromJwt(req: any) { try { - let jwt = this._joseUtil.parseJwt(req.responseText); + const jwt = this._joseUtil.parseJwt(req.responseText); if (!jwt || !jwt.header || !jwt.payload) { Log.error("UserInfoService._getClaimsFromJwt: Failed to parse JWT", jwt); return Promise.reject(new Error("Failed to parse id_token")); @@ -109,17 +115,15 @@ export class UserInfoService { }); }); }); - return; } catch (e) { Log.error("UserInfoService._getClaimsFromJwt: Error parsing JWT response", e.message); - reject(e); - return; + return Promise.reject(e); } } - _filterByAlg(keys, alg) { - var kty = null; + _filterByAlg(keys: any[], alg: string) { + var kty: any = null; if (alg.startsWith("RS")) { kty = "RSA"; } diff --git a/src/UserManager.js b/src/UserManager.ts similarity index 85% rename from src/UserManager.js rename to src/UserManager.ts index 62a5f65d5..cb7275a0f 100644 --- a/src/UserManager.js +++ b/src/UserManager.ts @@ -1,28 +1,40 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { OidcClient } from './OidcClient.js'; -import { UserManagerSettings } from './UserManagerSettings.js'; -import { User } from './User.js'; -import { UserManagerEvents } from './UserManagerEvents.js'; -import { SilentRenewService } from './SilentRenewService.js'; -import { SessionMonitor } from './SessionMonitor.js'; +import { Log } from './Log'; +import { OidcClient } from './OidcClient'; +import { UserManagerSettings } from './UserManagerSettings'; +import { User } from './User'; +import { UserManagerEvents } from './UserManagerEvents'; +import { SilentRenewService } from './SilentRenewService'; +import { SessionMonitor } from './SessionMonitor'; import { SigninRequest } from "./SigninRequest"; -import { TokenRevocationClient } from './TokenRevocationClient.js'; -import { TokenClient } from './TokenClient.js'; -import { JoseUtil } from './JoseUtil.js'; - +import { TokenRevocationClient } from './TokenRevocationClient'; +import { TokenClient } from './TokenClient'; +import { JoseUtil } from './JoseUtil'; +import { INavigator } from './INavigator'; +import { IFrameNavigator } from './IFrameNavigator'; +import { PopupNavigator } from './PopupNavigator'; export class UserManager extends OidcClient { - constructor(settings = {}, + protected _settings!: UserManagerSettings /* TODO: port-ts */ + + private readonly _events: UserManagerEvents; + private readonly _silentRenewService: SilentRenewService; + // @ts-ignore + private readonly _sessionMonitor: SessionMonitor | null; + private readonly _tokenRevocationClient: TokenRevocationClient; + private readonly _tokenClient: TokenClient; + private readonly _joseUtil: typeof JoseUtil; + + constructor( + settings: any = {}, SilentRenewServiceCtor = SilentRenewService, SessionMonitorCtor = SessionMonitor, TokenRevocationClientCtor = TokenRevocationClient, TokenClientCtor = TokenClient, joseUtil = JoseUtil ) { - if (!(settings instanceof UserManagerSettings)) { settings = new UserManagerSettings(settings); } @@ -37,6 +49,7 @@ export class UserManager extends OidcClient { this.startSilentRenew(); } + this._sessionMonitor = null; if (this.settings.monitorSession) { Log.debug("UserManager.ctor: monitorSession is configured, setting up session monitor"); this._sessionMonitor = new SessionMonitorCtor(this); @@ -47,6 +60,10 @@ export class UserManager extends OidcClient { this._joseUtil = joseUtil; } + get settings(): UserManagerSettings { + return this._settings; + } + get _redirectNavigator() { return this.settings.redirectNavigator; } @@ -87,7 +104,7 @@ export class UserManager extends OidcClient { }); } - signinRedirect(args = {}) { + signinRedirect(args: any = {}) { args = Object.assign({}, args); args.request_type = "si:r"; @@ -98,7 +115,7 @@ export class UserManager extends OidcClient { Log.info("UserManager.signinRedirect: successful"); }); } - signinRedirectCallback(url) { + signinRedirectCallback(url: string) { return this._signinEnd(url || this._redirectNavigator.url).then(user => { if (user.profile && user.profile.sub) { Log.info("UserManager.signinRedirectCallback: successful, signed in sub: ", user.profile.sub); @@ -111,7 +128,7 @@ export class UserManager extends OidcClient { }); } - signinPopup(args = {}) { + signinPopup(args: any = {}) { args = Object.assign({}, args); args.request_type = "si:p"; @@ -141,7 +158,7 @@ export class UserManager extends OidcClient { return user; }); } - signinPopupCallback(url) { + signinPopupCallback(url: string) { return this._signinCallback(url, this._popupNavigator).then(user => { if (user) { if (user.profile && user.profile.sub) { @@ -158,7 +175,7 @@ export class UserManager extends OidcClient { }); } - signinSilent(args = {}) { + signinSilent(args: any = {}) { args = Object.assign({}, args); // first determine if we have a refresh token, or need to use iframe @@ -179,7 +196,7 @@ export class UserManager extends OidcClient { }); } - _useRefreshToken(args = {}) { + _useRefreshToken(args: any = {}) { return this._tokenClient.exchangeRefreshToken(args).then(result => { if (!result) { Log.error("UserManager._useRefreshToken: No response returned from token endpoint"); @@ -201,7 +218,7 @@ export class UserManager extends OidcClient { Log.debug("UserManager._useRefreshToken: refresh token response success"); user.id_token = result.id_token || user.id_token; user.access_token = result.access_token; - user.refresh_token = result.refresh_token || user.refresh_token; + user.refresh_token = /* TODO: port-TS result.refresh_token ||*/ user.refresh_token; user.expires_in = result.expires_in; return this.storeUser(user).then(()=>{ @@ -217,7 +234,7 @@ export class UserManager extends OidcClient { }); } - _validateIdTokenFromTokenRefreshToken(profile, id_token) { + _validateIdTokenFromTokenRefreshToken(profile: any, id_token: string) { return this._metadataService.getIssuer().then(issuer => { return this.settings.getEpochTime().then(now => { return this._joseUtil.validateJwtAttributes(id_token, issuer, this._settings.client_id, this._settings.clockSkew, now).then(payload => { @@ -241,12 +258,13 @@ export class UserManager extends OidcClient { Log.error("UserManager._validateIdTokenFromTokenRefreshToken: azp not in id_token, but present in original id_token"); return Promise.reject(new Error("azp not in id_token, but present in original id_token")); } + return Promise.resolve(); }); }); }); } - - _signinSilentIframe(args = {}) { + + _signinSilentIframe(args: any = {}) { let url = args.redirect_uri || this.settings.silent_redirect_uri || this.settings.redirect_uri; if (!url) { Log.error("UserManager.signinSilent: No silent_redirect_uri configured"); @@ -273,7 +291,7 @@ export class UserManager extends OidcClient { }); } - signinSilentCallback(url) { + signinSilentCallback(url: string) { return this._signinCallback(url, this._iframeNavigator).then(user => { if (user) { if (user.profile && user.profile.sub) { @@ -288,8 +306,8 @@ export class UserManager extends OidcClient { }); } - signinCallback(url) { - return this.readSigninResponseState(url).then(({state, response}) => { + signinCallback(url: string) { + return this.readSigninResponseState(url).then(({state}) => { if (state.request_type === "si:r") { return this.signinRedirectCallback(url); } @@ -303,7 +321,8 @@ export class UserManager extends OidcClient { }); } - signoutCallback(url, keepOpen) { + signoutCallback(url: string, keepOpen: boolean): Promise { + // @ts-ignore return this.readSignoutResponseState(url).then(({state, response}) => { if (state) { if (state.request_type === "so:r") { @@ -318,7 +337,7 @@ export class UserManager extends OidcClient { }); } - querySessionStatus(args = {}) { + querySessionStatus(args: any = {}) { args = Object.assign({}, args); args.request_type = "si:s"; // this acts like a signin silent @@ -337,7 +356,8 @@ export class UserManager extends OidcClient { return this._signinStart(args, this._iframeNavigator, { startUrl: url, silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout - }).then(navResponse => { + }).then((navResponse: any) => { + // @ts-ignore return this.processSigninResponse(navResponse.url).then(signinResponse => { Log.debug("UserManager.querySessionStatus: got signin response"); @@ -355,9 +375,9 @@ export class UserManager extends OidcClient { }) .catch(err => { if (err.session_state && this.settings.monitorAnonymousSession) { - if (err.message == "login_required" || - err.message == "consent_required" || - err.message == "interaction_required" || + if (err.message == "login_required" || + err.message == "consent_required" || + err.message == "interaction_required" || err.message == "account_selection_required" ) { Log.info("UserManager.querySessionStatus: querySessionStatus success for anonymous user"); @@ -372,13 +392,12 @@ export class UserManager extends OidcClient { }); } - _signin(args, navigator, navigatorParams = {}) { - return this._signinStart(args, navigator, navigatorParams).then(navResponse => { + _signin(args: any, navigator: INavigator, navigatorParams: any = {}): Promise { + return this._signinStart(args, navigator, navigatorParams).then((navResponse: any) => { return this._signinEnd(navResponse.url, args); }); } - _signinStart(args, navigator, navigatorParams = {}) { - + _signinStart(args: any, navigator: INavigator, navigatorParams: any = {}) { return navigator.prepare(navigatorParams).then(handle => { Log.debug("UserManager._signinStart: got navigator window handle"); @@ -390,15 +409,13 @@ export class UserManager extends OidcClient { return handle.navigate(navigatorParams); }).catch(err => { - if (handle.close) { - Log.debug("UserManager._signinStart: Error after preparing navigator, closing navigator window"); - handle.close(); - } + Log.debug("UserManager._signinStart: Error after preparing navigator, closing navigator window"); + handle.close(); throw err; }); }); } - _signinEnd(url, args = {}) { + _signinEnd(url: string, args: any = {}): Promise { return this.processSigninResponse(url).then(signinResponse => { Log.debug("UserManager._signinEnd: got signin response"); @@ -423,14 +440,15 @@ export class UserManager extends OidcClient { }); }); } - _signinCallback(url, navigator) { + _signinCallback(url: string, navigator: IFrameNavigator | PopupNavigator): Promise { Log.debug("UserManager._signinCallback"); let useQuery = this._settings.response_mode === "query" || (!this._settings.response_mode && SigninRequest.isCode(this._settings.response_type)); let delimiter = useQuery ? "?" : "#"; + // @ts-ignore return navigator.callback(url, undefined, delimiter); } - signoutRedirect(args = {}) { + signoutRedirect(args: any = {}) { args = Object.assign({}, args); args.request_type = "so:r"; @@ -445,14 +463,14 @@ export class UserManager extends OidcClient { Log.info("UserManager.signoutRedirect: successful"); }); } - signoutRedirectCallback(url) { + signoutRedirectCallback(url: string) { return this._signoutEnd(url || this._redirectNavigator.url).then(response=>{ Log.info("UserManager.signoutRedirectCallback: successful"); return response; }); } - signoutPopup(args = {}) { + signoutPopup(args: any = {}) { args = Object.assign({}, args); args.request_type = "so:p"; @@ -476,7 +494,7 @@ export class UserManager extends OidcClient { Log.info("UserManager.signoutPopup: successful"); }); } - signoutPopupCallback(url, keepOpen) { + signoutPopupCallback(url: any, keepOpen: any) { if (typeof(keepOpen) === 'undefined' && typeof(url) === 'boolean') { keepOpen = url; url = null; @@ -488,19 +506,19 @@ export class UserManager extends OidcClient { }); } - _signout(args, navigator, navigatorParams = {}) { - return this._signoutStart(args, navigator, navigatorParams).then(navResponse => { + _signout(args: any, navigator: INavigator, navigatorParams: any = {}) { + return this._signoutStart(args, navigator, navigatorParams).then((navResponse: any) => { return this._signoutEnd(navResponse.url); }); } - _signoutStart(args = {}, navigator, navigatorParams = {}) { + _signoutStart(args: any = {}, navigator: INavigator, navigatorParams: any = {}) { return navigator.prepare(navigatorParams).then(handle => { Log.debug("UserManager._signoutStart: got navigator window handle"); return this._loadUser().then(user => { Log.debug("UserManager._signoutStart: loaded current user from storage"); - var revokePromise = this._settings.revokeAccessTokenOnSignout ? this._revokeInternal(user) : Promise.resolve(); + var revokePromise = this._settings.revokeAccessTokenOnSignout ? this._revokeInternal(user) : Promise.resolve(true); return revokePromise.then(() => { var id_token = args.id_token_hint || user && user.id_token; @@ -524,15 +542,13 @@ export class UserManager extends OidcClient { }); }); }).catch(err => { - if (handle.close) { - Log.debug("UserManager._signoutStart: Error after preparing navigator, closing navigator window"); - handle.close(); - } + Log.debug("UserManager._signoutStart: Error after preparing navigator, closing navigator window"); + handle.close(); throw err; }); }); } - _signoutEnd(url) { + _signoutEnd(url: string) { return this.processSignoutResponse(url).then(signoutResponse => { Log.debug("UserManager._signoutEnd: got signout response"); @@ -543,15 +559,15 @@ export class UserManager extends OidcClient { revokeAccessToken() { return this._loadUser().then(user => { return this._revokeInternal(user, true).then(success => { - if (success) { + if (success && user) { Log.debug("UserManager.revokeAccessToken: removing token properties from user and re-storing"); - user.access_token = null; - user.refresh_token = null; - user.expires_at = null; - user.token_type = null; + user.access_token = ""; + user.refresh_token = ""; + user.expires_at = 0; + user.token_type = ""; - return this.storeUser(user).then(() => { + this.storeUser(user).then(() => { Log.debug("UserManager.revokeAccessToken: user stored"); this._events.load(user); }); @@ -562,7 +578,7 @@ export class UserManager extends OidcClient { }); } - _revokeInternal(user, required) { + _revokeInternal(user: User | null, required = false) { if (user) { var access_token = user.access_token; var refresh_token = user.refresh_token; @@ -574,7 +590,7 @@ export class UserManager extends OidcClient { if (!atSuccess && !rtSuccess) { Log.debug("UserManager.revokeAccessToken: no need to revoke due to no token(s), or JWT format"); } - + return atSuccess || rtSuccess; }); }); @@ -583,7 +599,7 @@ export class UserManager extends OidcClient { return Promise.resolve(false); } - _revokeAccessTokenInternal(access_token, required) { + _revokeAccessTokenInternal(access_token: string, required: boolean): Promise { // check for JWT vs. reference token if (!access_token || access_token.indexOf('.') >= 0) { return Promise.resolve(false); @@ -592,7 +608,7 @@ export class UserManager extends OidcClient { return this._tokenRevocationClient.revoke(access_token, required).then(() => true); } - _revokeRefreshTokenInternal(refresh_token, required) { + _revokeRefreshTokenInternal(refresh_token: string, required: boolean): Promise { if (!refresh_token) { return Promise.resolve(false); } @@ -624,7 +640,7 @@ export class UserManager extends OidcClient { }); } - storeUser(user) { + storeUser(user: User | null): Promise { if (user) { Log.debug("UserManager.storeUser: storing user"); diff --git a/src/UserManagerEvents.js b/src/UserManagerEvents.ts similarity index 58% rename from src/UserManagerEvents.js rename to src/UserManagerEvents.ts index 1beb12972..81218c2e4 100644 --- a/src/UserManagerEvents.js +++ b/src/UserManagerEvents.ts @@ -1,13 +1,28 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { AccessTokenEvents } from './AccessTokenEvents.js'; -import { Event } from './Event.js'; +import { Log } from './Log'; +import { AccessTokenEvents } from './AccessTokenEvents'; +import { UserManagerSettings } from './UserManagerSettings'; +import { Event } from './Event'; +import { User } from './User'; + +export type UserLoadedCallback = (user: User) => void; +export type UserUnloadedCallback = () => void; +export type SilentRenewErrorCallback = (error: Error) => void; +export type UserSignedInCallback = () => void; +export type UserSignedOutCallback = () => void; +export type UserSessionChangedCallback = () => void; export class UserManagerEvents extends AccessTokenEvents { + private _userLoaded: Event; + private _userUnloaded: Event; + private _silentRenewError: Event; + private _userSignedIn: Event; + private _userSignedOut: Event; + private _userSessionChanged: Event; - constructor(settings) { + constructor(settings: UserManagerSettings) { super(settings); this._userLoaded = new Event("User loaded"); this._userUnloaded = new Event("User unloaded"); @@ -17,7 +32,7 @@ export class UserManagerEvents extends AccessTokenEvents { this._userSessionChanged = new Event("User session changed"); } - load(user, raiseEvent=true) { + load(user: User, raiseEvent=true) { Log.debug("UserManagerEvents.load"); super.load(user); if (raiseEvent) { @@ -30,35 +45,35 @@ export class UserManagerEvents extends AccessTokenEvents { this._userUnloaded.raise(); } - addUserLoaded(cb) { + addUserLoaded(cb: UserLoadedCallback) { this._userLoaded.addHandler(cb); } - removeUserLoaded(cb) { + removeUserLoaded(cb: UserLoadedCallback) { this._userLoaded.removeHandler(cb); } - addUserUnloaded(cb) { + addUserUnloaded(cb: UserUnloadedCallback) { this._userUnloaded.addHandler(cb); } - removeUserUnloaded(cb) { + removeUserUnloaded(cb: UserUnloadedCallback) { this._userUnloaded.removeHandler(cb); } - addSilentRenewError(cb) { + addSilentRenewError(cb: SilentRenewErrorCallback) { this._silentRenewError.addHandler(cb); } - removeSilentRenewError(cb) { + removeSilentRenewError(cb: SilentRenewErrorCallback) { this._silentRenewError.removeHandler(cb); } - _raiseSilentRenewError(e) { + _raiseSilentRenewError(e: Error) { Log.debug("UserManagerEvents._raiseSilentRenewError", e.message); this._silentRenewError.raise(e); } - addUserSignedIn(cb) { + addUserSignedIn(cb: UserSignedInCallback) { this._userSignedIn.addHandler(cb); } - removeUserSignedIn(cb) { + removeUserSignedIn(cb: UserSignedInCallback) { this._userSignedIn.removeHandler(cb); } _raiseUserSignedIn() { @@ -66,10 +81,10 @@ export class UserManagerEvents extends AccessTokenEvents { this._userSignedIn.raise(); } - addUserSignedOut(cb) { + addUserSignedOut(cb: UserSignedOutCallback) { this._userSignedOut.addHandler(cb); } - removeUserSignedOut(cb) { + removeUserSignedOut(cb: UserSignedOutCallback) { this._userSignedOut.removeHandler(cb); } _raiseUserSignedOut() { @@ -77,10 +92,10 @@ export class UserManagerEvents extends AccessTokenEvents { this._userSignedOut.raise(); } - addUserSessionChanged(cb) { + addUserSessionChanged(cb: UserSessionChangedCallback) { this._userSessionChanged.addHandler(cb); } - removeUserSessionChanged(cb) { + removeUserSessionChanged(cb: UserSessionChangedCallback) { this._userSessionChanged.removeHandler(cb); } _raiseUserSessionChanged() { diff --git a/src/UserManagerSettings.js b/src/UserManagerSettings.ts similarity index 55% rename from src/UserManagerSettings.js rename to src/UserManagerSettings.ts index 67e1be197..f7ed2133a 100644 --- a/src/UserManagerSettings.js +++ b/src/UserManagerSettings.ts @@ -1,19 +1,80 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { OidcClientSettings } from './OidcClientSettings.js'; -import { RedirectNavigator } from './RedirectNavigator.js'; -import { PopupNavigator } from './PopupNavigator.js'; -import { IFrameNavigator } from './IFrameNavigator.js'; -import { WebStorageStateStore } from './WebStorageStateStore.js'; -import { Global } from './Global.js'; -import { SigninRequest } from './SigninRequest.js'; +import { OidcClientArgs, OidcClientSettings } from './OidcClientSettings'; +import { RedirectNavigator } from './RedirectNavigator'; +import { PopupNavigator } from './PopupNavigator'; +import { IFrameNavigator } from './IFrameNavigator'; +import { WebStorageStateStore } from './WebStorageStateStore'; +import { Global } from './Global'; +import { SigninRequest } from './SigninRequest'; const DefaultAccessTokenExpiringNotificationTime = 60; const DefaultCheckSessionInterval = 2000; +export interface UserManagerArgs extends OidcClientArgs { + /** The URL for the page containing the call to signinPopupCallback to handle the callback from the OIDC/OAuth2 */ + popup_redirect_uri?: string, + popup_post_logout_redirect_uri?: string, + /** The features parameter to window.open for the popup signin window. + * default: 'location=no,toolbar=no,width=500,height=500,left=100,top=100' + */ + popupWindowFeatures?: string; + /** The target parameter to window.open for the popup signin window (default: '_blank') */ + popupWindowTarget?: any; + /** The URL for the page containing the code handling the silent renew */ + silent_redirect_uri?: any; + /** Number of milliseconds to wait for the silent renew to return before assuming it has failed or timed out (default: 10000) */ + silentRequestTimeout?: any; + /** Flag to indicate if there should be an automatic attempt to renew the access token prior to its expiration (default: false) */ + automaticSilentRenew?: boolean; + validateSubOnSilentRenew?: boolean; + /** Flag to control if id_token is included as id_token_hint in silent renew calls (default: true) */ + includeIdTokenInSilentRenew?: boolean; + /** Will raise events for when user has performed a signout at the OP (default: true) */ + monitorSession?: boolean; + monitorAnonymousSession?: boolean; + /** Interval, in ms, to check the user's session (default: 2000) */ + checkSessionInterval?: number; + query_status_response_type?: string; + stopCheckSessionOnError?: boolean; + /** Will invoke the revocation endpoint on signout if there is an access token for the user (default: false) */ + revokeAccessTokenOnSignout?: boolean; + /** The number of seconds before an access token is to expire to raise the accessTokenExpiring event (default: 60) */ + accessTokenExpiringNotificationTime?: number; + redirectNavigator?: any; + popupNavigator?: any; + iframeNavigator?: any; + /** Storage object used to persist User for currently authenticated user (default: session storage) */ + userStore?: WebStorageStateStore; +} + export class UserManagerSettings extends OidcClientSettings { + private readonly _popup_redirect_uri?: string; + private readonly _popup_post_logout_redirect_uri?: string; + private readonly _popupWindowFeatures?: string; + private readonly _popupWindowTarget?: any; + + private readonly _silent_redirect_uri?: any; + private readonly _silentRequestTimeout?: any; + private readonly _automaticSilentRenew: boolean; + private readonly _validateSubOnSilentRenew: boolean; + private readonly _includeIdTokenInSilentRenew: boolean; + + private readonly _monitorSession: boolean; + private readonly _monitorAnonymousSession: boolean; + private readonly _checkSessionInterval: number; + private readonly _query_status_response_type?: string; + private readonly _stopCheckSessionOnError?: boolean; + private readonly _revokeAccessTokenOnSignout: boolean; + private readonly _accessTokenExpiringNotificationTime: number; + + private readonly _redirectNavigator: RedirectNavigator; + private readonly _popupNavigator: PopupNavigator; + private readonly _iframeNavigator: IFrameNavigator; + + private readonly _userStore: WebStorageStateStore; + constructor({ popup_redirect_uri, popup_post_logout_redirect_uri, @@ -35,7 +96,7 @@ export class UserManagerSettings extends OidcClientSettings { popupNavigator = new PopupNavigator(), iframeNavigator = new IFrameNavigator(), userStore = new WebStorageStateStore({ store: Global.sessionStorage }) - } = {}) { + }: UserManagerArgs = {}) { super(arguments[0]); this._popup_redirect_uri = popup_redirect_uri; @@ -56,7 +117,7 @@ export class UserManagerSettings extends OidcClientSettings { this._stopCheckSessionOnError = stopCheckSessionOnError; if (query_status_response_type) { this._query_status_response_type = query_status_response_type; - } + } else if (arguments[0] && arguments[0].response_type) { this._query_status_response_type = SigninRequest.isOidc(arguments[0].response_type) ? "id_token" : "code"; } diff --git a/src/Version.ts b/src/Version.ts new file mode 100644 index 000000000..6ec007169 --- /dev/null +++ b/src/Version.ts @@ -0,0 +1,2 @@ +const Version = "1.11.5"; +export { Version }; diff --git a/src/WebStorageStateStore.js b/src/WebStorageStateStore.ts similarity index 82% rename from src/WebStorageStateStore.js rename to src/WebStorageStateStore.ts index 91903dbbd..875a33ce1 100644 --- a/src/WebStorageStateStore.js +++ b/src/WebStorageStateStore.ts @@ -1,43 +1,40 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -import { Log } from './Log.js'; -import { Global } from './Global.js'; +import { Log } from './Log'; +import { Global } from './Global'; export class WebStorageStateStore { + private _store: Storage + private _prefix: string + constructor({prefix = "oidc.", store = Global.localStorage} = {}) { this._store = store; this._prefix = prefix; } - set(key, value) { + set(key: string, value: string) { Log.debug("WebStorageStateStore.set", key); key = this._prefix + key; - this._store.setItem(key, value); - return Promise.resolve(); } - get(key) { + get(key: string) { Log.debug("WebStorageStateStore.get", key); key = this._prefix + key; - let item = this._store.getItem(key); - return Promise.resolve(item); } - remove(key) { + remove(key: string) { Log.debug("WebStorageStateStore.remove", key); key = this._prefix + key; - let item = this._store.getItem(key); this._store.removeItem(key); - return Promise.resolve(item); } @@ -45,15 +42,12 @@ export class WebStorageStateStore { Log.debug("WebStorageStateStore.getAllKeys"); var keys = []; - for (let index = 0; index < this._store.length; index++) { let key = this._store.key(index); - - if (key.indexOf(this._prefix) === 0) { + if (key && key.indexOf(this._prefix) === 0) { keys.push(key.substr(this._prefix.length)); } } - return Promise.resolve(keys); } } diff --git a/src/crypto/jsrsasign.js b/src/crypto/jsrsasign.ts similarity index 95% rename from src/crypto/jsrsasign.js rename to src/crypto/jsrsasign.ts index b6ab76080..3777e9332 100644 --- a/src/crypto/jsrsasign.js +++ b/src/crypto/jsrsasign.ts @@ -1,3 +1,4 @@ +// @ts-ignore import { jws, KEYUTIL as KeyUtil, X509, crypto, hextob64u, b64tohex } from '../../jsrsasign/dist/jsrsasign.js'; const AllowedSigningAlgs = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512']; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index ab1e4b60f..000000000 --- a/src/index.js +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from './Log.js'; -import { OidcClient } from './OidcClient.js'; -import { OidcClientSettings } from './OidcClientSettings.js'; -import { WebStorageStateStore } from './WebStorageStateStore.js'; -import { InMemoryWebStorage } from './InMemoryWebStorage.js'; -import { UserManager } from './UserManager.js'; -import { AccessTokenEvents } from './AccessTokenEvents.js'; -import { MetadataService } from './MetadataService.js'; -import { CordovaPopupNavigator } from './CordovaPopupNavigator.js'; -import { CordovaIFrameNavigator } from './CordovaIFrameNavigator.js'; -import { CheckSessionIFrame } from './CheckSessionIFrame.js'; -import { TokenRevocationClient } from './TokenRevocationClient.js'; -import { SessionMonitor } from './SessionMonitor.js'; -import { Global } from './Global.js'; -import { User } from './User.js'; - -import { Version } from './version.js'; - -export default { - Version, - Log, - OidcClient, - OidcClientSettings, - WebStorageStateStore, - InMemoryWebStorage, - UserManager, - AccessTokenEvents, - MetadataService, - CordovaPopupNavigator, - CordovaIFrameNavigator, - CheckSessionIFrame, - TokenRevocationClient, - SessionMonitor, - Global, - User -}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..34de1429d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,39 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from './Log'; +import { OidcClient } from './OidcClient'; +import { OidcClientSettings } from './OidcClientSettings'; +import { WebStorageStateStore } from './WebStorageStateStore'; +import { InMemoryWebStorage } from './InMemoryWebStorage'; +import { UserManager } from './UserManager'; +import { AccessTokenEvents } from './AccessTokenEvents'; +import { MetadataService } from './MetadataService'; +import { CordovaPopupNavigator } from './CordovaPopupNavigator'; +import { CordovaIFrameNavigator } from './CordovaIFrameNavigator'; +import { CheckSessionIFrame } from './CheckSessionIFrame'; +import { TokenRevocationClient } from './TokenRevocationClient'; +import { SessionMonitor } from './SessionMonitor'; +import { Global } from './Global'; +import { User } from './User'; + +import { Version } from './Version'; + +export default { + Version, + Log, + OidcClient, + OidcClientSettings, + WebStorageStateStore, + InMemoryWebStorage, + UserManager, + AccessTokenEvents, + MetadataService, + CordovaPopupNavigator, + CordovaIFrameNavigator, + CheckSessionIFrame, + TokenRevocationClient, + SessionMonitor, + Global, + User +}; diff --git a/src/random.js b/src/random.js deleted file mode 100644 index 7742dd0a1..000000000 --- a/src/random.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Generates RFC4122 version 4 guid () - */ - -var crypto = (typeof window !== 'undefined') ? (window.crypto || window.msCrypto) : null; - -function _cryptoUuidv4() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => - (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - ) -} - -function _uuidv4() { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => - (c ^ Math.random() * 16 >> c / 4).toString(16) - ) -} - -export default function random() { - var hasCrypto = crypto != 'undefined' && crypto !== null; - var hasRandomValues = hasCrypto && (typeof(crypto.getRandomValues) != 'undefined'); - var uuid = hasRandomValues ? _cryptoUuidv4 : _uuidv4; - return uuid().replace(/-/g, ''); -} diff --git a/src/random.ts b/src/random.ts new file mode 100644 index 000000000..db1a0fdf9 --- /dev/null +++ b/src/random.ts @@ -0,0 +1,26 @@ +/** + * Generates RFC4122 version 4 guid () + */ + +// @ts-ignore +const crypto = (typeof window !== 'undefined') ? (window.crypto || window.msCrypto) : undefined; + +function _cryptoUuidv4() { + // @ts-ignore + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto!.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ) +} + +function _uuidv4() { + // @ts-ignore + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ Math.random() * 16 >> c / 4).toString(16) + ) +} + +export default function random(): string { + const hasRandomValues = crypto && crypto.hasOwnProperty("getRandomValues"); + var uuid = hasRandomValues ? _cryptoUuidv4 : _uuidv4; + return uuid().replace(/-/g, ''); +} diff --git a/src/version.js b/src/version.js deleted file mode 100644 index 7f6c67e95..000000000 --- a/src/version.js +++ /dev/null @@ -1 +0,0 @@ -const Version = "1.11.5"; export {Version}; \ No newline at end of file