UNPKG

@lando/platformsh

Version:

A Lando plugin that provides a tight integration with Platform.sh.

299 lines (293 loc) 11.9 kB
/*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } class OAuth2PopupFlow { constructor(options) { this.authorizationUri = options.authorizationUri; this.clientId = options.clientId; this.redirectUri = options.redirectUri; this.scope = options.scope; this.responseType = options.responseType || 'token'; this.accessTokenStorageKey = options.accessTokenStorageKey || 'token'; this.accessTokenResponseKey = options.accessTokenResponseKey || 'access_token'; this.storage = options.storage || window.localStorage; this.pollingTime = options.pollingTime || 200; this.additionalAuthorizationParameters = options.additionalAuthorizationParameters; this.tokenValidator = options.tokenValidator; this.beforePopup = options.beforePopup; this.afterResponse = options.afterResponse; this._eventListeners = {}; } get _rawToken() { return this.storage.getItem(this.accessTokenStorageKey) || undefined; } set _rawToken(value) { if (value === null) return; if (value === undefined) return; this.storage.setItem(this.accessTokenStorageKey, value); } get _rawTokenPayload() { const rawToken = this._rawToken; if (!rawToken) return undefined; const tokenSplit = rawToken.split('.'); const encodedPayload = tokenSplit[1]; if (!encodedPayload) return undefined; const decodedPayloadJson = window.atob(encodedPayload.replace('-', '+').replace('_', '/')); const decodedPayload = OAuth2PopupFlow.jsonParseOrUndefined(decodedPayloadJson); return decodedPayload; } /** * A simple synchronous method that returns whether or not the user is logged in by checking * whether or not their token is present and not expired. */ loggedIn() { const decodedPayload = this._rawTokenPayload; if (!decodedPayload) return false; if (this.tokenValidator) { const token = this._rawToken; if (!this.tokenValidator({ payload: decodedPayload, token })) return false; } const exp = decodedPayload.exp; if (!exp) return false; if (new Date().getTime() > exp * 1000) return false; return true; } /** * Returns true only if there is a token in storage and that token is expired. Use this to method * in conjunction with `loggedIn` to display a message like "you need to *re*login" vs "you need * to login". */ tokenExpired() { const decodedPayload = this._rawTokenPayload; if (!decodedPayload) return false; const exp = decodedPayload.exp; if (!exp) return false; if (new Date().getTime() <= exp * 1000) return false; return true; } /** * Deletes the token from the given storage causing `loggedIn` to return false on its next call. * Also dispatches `logout` event */ logout() { this.storage.removeItem(this.accessTokenStorageKey); this.dispatchEvent(new Event('logout')); } /** * Call this method in a route of the `redirectUri`. This method takes the value of the hash at * `window.location.hash` and attempts to grab the token from the URL. * * If the method was able to grab the token, it will return `'SUCCESS'` else it will return a * different string. */ handleRedirect() { const locationHref = window.location.href; if (!locationHref.startsWith(this.redirectUri)) return 'REDIRECT_URI_MISMATCH'; const rawHash = window.location.hash; if (!rawHash) return 'FALSY_HASH'; const hashMatch = /#(.*)/.exec(rawHash); // this case won't happen because the browser typically adds the `#` always if (!hashMatch) return 'NO_HASH_MATCH'; const hash = hashMatch[1]; const authorizationResponse = OAuth2PopupFlow.decodeUriToObject(hash); if (this.afterResponse) { this.afterResponse(authorizationResponse); } const rawToken = authorizationResponse[this.accessTokenResponseKey]; if (!rawToken) return 'FALSY_TOKEN'; this._rawToken = rawToken; window.location.hash = ''; return 'SUCCESS'; } /** * supported events are: * * 1. `logout`–fired when the `logout()` method is called and * 2. `login`–fired during the `tryLoginPopup()` method is called and succeeds */ addEventListener(type, listener) { const listeners = this._eventListeners[type] || []; listeners.push(listener); this._eventListeners[type] = listeners; } /** * Use this to dispatch an event to the internal `EventTarget` */ dispatchEvent(event) { const listeners = this._eventListeners[event.type] || []; for (const listener of listeners) { const dispatch = typeof listener === 'function' ? listener : typeof listener === 'object' && typeof listener.handleEvent === 'function' ? listener.handleEvent.bind(listener) : () => { }; dispatch(event); } return true; } /** * Removes the event listener in target's event listener list with the same type, callback, and options. */ removeEventListener(type, listener) { const listeners = this._eventListeners[type] || []; this._eventListeners[type] = listeners.filter((l) => l !== listener); } /** * Tries to open a popup to login the user in. If the user is already `loggedIn()` it will * immediately return `'ALREADY_LOGGED_IN'`. If the popup fails to open, it will immediately * return `'POPUP_FAILED'` else it will wait for `loggedIn()` to be `true` and eventually * return `'SUCCESS'`. * * Also dispatches `login` event */ tryLoginPopup() { return __awaiter(this, void 0, void 0, function* () { if (this.loggedIn()) return 'ALREADY_LOGGED_IN'; if (this.beforePopup) { yield Promise.resolve(this.beforePopup()); } const additionalParams = typeof this.additionalAuthorizationParameters === 'function' ? this.additionalAuthorizationParameters() : typeof this.additionalAuthorizationParameters === 'object' ? this.additionalAuthorizationParameters : {}; const popup = window.open(`${this.authorizationUri}?${OAuth2PopupFlow.encodeObjectToUri(Object.assign({ client_id: this.clientId, response_type: this.responseType, redirect_uri: this.redirectUri, scope: this.scope }, additionalParams))}`); if (!popup) return 'POPUP_FAILED'; yield this.authenticated(); popup.close(); this.dispatchEvent(new Event('login')); return 'SUCCESS'; }); } /** * A promise that does not resolve until `loggedIn()` is true. This uses the `pollingTime` * to wait until checking if `loggedIn()` is `true`. */ authenticated() { return __awaiter(this, void 0, void 0, function* () { while (!this.loggedIn()) { yield OAuth2PopupFlow.time(this.pollingTime); } }); } /** * If the user is `loggedIn()`, the token will be returned immediate, else it will open a popup * and wait until the user is `loggedIn()` (i.e. a new token has been added). */ token() { return __awaiter(this, void 0, void 0, function* () { yield this.authenticated(); const token = this._rawToken; if (!token) throw new Error('Token was falsy after being authenticated.'); return token; }); } /** * If the user is `loggedIn()`, the token payload will be returned immediate, else it will open a * popup and wait until the user is `loggedIn()` (i.e. a new token has been added). */ tokenPayload() { return __awaiter(this, void 0, void 0, function* () { yield this.authenticated(); const payload = this._rawTokenPayload; if (!payload) throw new Error('Token payload was falsy after being authenticated.'); return payload; }); } /** * wraps `JSON.parse` and return `undefined` if the parsing failed */ static jsonParseOrUndefined(json) { try { return JSON.parse(json); } catch (e) { return undefined; } } /** * wraps `setTimeout` in a `Promise` that resolves to `'TIMER'` */ static time(milliseconds) { return new Promise((resolve) => window.setTimeout(() => resolve('TIMER'), milliseconds)); } /** * wraps `decodeURIComponent` and returns the original string if it cannot be decoded */ static decodeUri(str) { try { return decodeURIComponent(str); } catch (_a) { return str; } } /** * Encodes an object of strings to a URL * * `{one: 'two', buckle: 'shoes or something'}` ==> `one=two&buckle=shoes%20or%20something` */ static encodeObjectToUri(obj) { return Object.keys(obj) .map((key) => ({ key, value: obj[key] })) .map(({ key, value }) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } /** * Decodes a URL string to an object of string * * `one=two&buckle=shoes%20or%20something` ==> `{one: 'two', buckle: 'shoes or something'}` */ static decodeUriToObject(str) { return str.split('&').reduce((decoded, keyValuePair) => { const [keyEncoded, valueEncoded] = keyValuePair.split('='); const key = this.decodeUri(keyEncoded); const value = this.decodeUri(valueEncoded); decoded[key] = value; return decoded; }, {}); } } export default OAuth2PopupFlow; export { OAuth2PopupFlow }; //# sourceMappingURL=index.esm.js.map