@lando/platformsh
Version:
A Lando plugin that provides a tight integration with Platform.sh.
299 lines (293 loc) • 11.9 kB
JavaScript
/*! *****************************************************************************
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