UNPKG

mongodb-stitch

Version:

[![Join the chat at https://gitter.im/mongodb/stitch](https://badges.gitter.im/mongodb/stitch.svg)](https://gitter.im/mongodb/stitch?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

397 lines (340 loc) 11.7 kB
/* global window, document, fetch */ import { createStorage } from './storage'; import { createProviders, PROVIDER_TYPE_MONGODB_CLOUD } from './providers'; import { StitchError } from '../errors'; import * as authCommon from './common'; import * as common from '../common'; import * as _platform from 'detect-browser'; const jwtDecode = require('jwt-decode'); const EMBEDDED_USER_AUTH_DATA_PARTS = 4; /** @private */ export class AuthFactory { constructor() { throw new StitchError('Auth can only be made from the AuthFactory.create function'); } static create(client, rootUrl, options) { return newAuth(client, rootUrl, options); } } /** @private */ export function newAuth(client, rootUrl, options) { let auth = Object.create(Auth.prototype); let namespace; if (!client || client.clientAppID === '') { namespace = 'admin'; } else { namespace = `client.${client.clientAppID}`; } options = Object.assign({ codec: authCommon.APP_CLIENT_CODEC, namespace: namespace, storageType: 'localStorage' }, options); auth.client = client; auth.rootUrl = rootUrl; auth.codec = options.codec; auth.requestOrigin = options.requestOrigin; auth.platform = options.platform || _platform; auth.storage = createStorage(options); auth.providers = createProviders(auth, options); return Promise.all([ auth._get(), auth.storage.get(authCommon.REFRESH_TOKEN_KEY), auth.storage.get(authCommon.USER_LOGGED_IN_PT_KEY), auth.storage.get(authCommon.DEVICE_ID_KEY) ]).then(([authObj, rt, loggedInProviderType, deviceId]) => { auth.auth = authObj; auth.authedId = authObj.userId; auth.rt = rt; auth.loggedInProviderType = loggedInProviderType; auth.deviceId = deviceId; return auth; }); } /** @private */ export class Auth { constructor(client, rootUrl, options) { throw new StitchError('Auth can only be made from the AuthFactory.create function'); } /** * Create the device info for this client. * * @private * @memberof module:auth * @method getDeviceInfo * @param {String} appId The app ID for this client * @param {String} appVersion The version of the app * @returns {Object} The device info object */ getDeviceInfo(deviceId, appId, appVersion = '') { const deviceInfo = { appId, appVersion, sdkVersion: common.SDK_VERSION }; if (deviceId) { deviceInfo.deviceId = deviceId; } if (this.platform) { deviceInfo.platform = this.platform.name; deviceInfo.platformVersion = this.platform.version; } return deviceInfo; } provider(name) { if (!this.providers.hasOwnProperty(name)) { throw new Error('Invalid auth provider specified: ' + name); } return this.providers[name]; } refreshToken() { return this.client.doSessionPost() .then((json) => this.set(json)); } pageRootUrl() { return [window.location.protocol, '//', window.location.host, window.location.pathname].join(''); } error() { return this._error; } isAppClient() { if (!this.client) { return true; // Handle the case where Auth is constructed with null } return this.client.type === common.APP_CLIENT_TYPE; } handleRedirect() { if (typeof (window) === 'undefined') { // This means we're running in some environment other // than a browser - so handling a redirect makes no sense here. return; } if (!window.location || !window.location.hash) { return; } return Promise.all([ this.storage.get(authCommon.STATE_KEY), this.storage.get(authCommon.STITCH_REDIRECT_PROVIDER) ]).then(([ourState, redirectProvider]) => { let redirectFragment = window.location.hash.substring(1); const redirectState = this.parseRedirectFragment(redirectFragment, ourState); if (redirectState.lastError || (redirectState.found && !redirectProvider)) { console.error(`StitchClient: error from redirect: ${redirectState.lastError ? redirectState.lastError : 'provider type not set'}`); this._error = redirectState.lastError; window.history.replaceState(null, '', this.pageRootUrl()); return Promise.reject(); } if (!redirectState.found) { return Promise.reject(); } return Promise.all( [ this.storage.remove(authCommon.STATE_KEY), this.storage.remove(authCommon.STITCH_REDIRECT_PROVIDER) ] ).then(() => ({redirectState, redirectProvider})); }).then(({redirectState, redirectProvider}) => { if (!redirectState.stateValid) { console.error('StitchClient: state values did not match!'); window.history.replaceState(null, '', this.pageRootUrl()); return; } if (!redirectState.ua) { console.error('StitchClient: no UA value was returned from redirect!'); return; } // If we get here, the state is valid - set auth appropriately. return this.set(redirectState.ua, redirectProvider); }).then(() => window.history.replaceState(null, '', this.pageRootUrl())) .catch(error => { if (error) { throw error; } }); } getCookie(name) { let splitCookies = document.cookie.split(' '); for (let i = 0; i < splitCookies.length; i++) { let cookie = splitCookies[i]; let sepIdx = cookie.indexOf('='); let cookieName = cookie.substring(0, sepIdx); if (cookieName === name) { let cookieVal = cookie.substring(sepIdx + 1, cookie.length); if (cookieVal[cookieVal.length - 1] === ';') { return cookieVal.substring(0, cookieVal.length - 1); } return cookieVal; } } } handleCookie() { if (typeof (window) === 'undefined' || typeof (document) === 'undefined') { // This means we're running in some environment other // than a browser - so handling a cookie makes no sense here. return; } if (!document.cookie) { return; } let uaCookie = this.getCookie(authCommon.USER_AUTH_COOKIE_NAME); if (!uaCookie) { return; } document.cookie = `${authCommon.USER_AUTH_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; const userAuth = this.unmarshallUserAuth(uaCookie); return this.set(userAuth, PROVIDER_TYPE_MONGODB_CLOUD).then(() => window.history.replaceState(null, '', this.pageRootUrl()) ); } clear() { this.auth = null; this.authedId = null; this.rt = null; this.loggedInProviderType = null; return Promise.all( [ this.storage.remove(authCommon.USER_AUTH_KEY), this.storage.remove(authCommon.REFRESH_TOKEN_KEY), this.storage.remove(authCommon.USER_LOGGED_IN_PT_KEY), this.storage.remove(authCommon.STITCH_REDIRECT_PROVIDER) ] ); } getDeviceId() { return this.deviceId; } // Returns whether or not the access token is expired or is going to expire within 'withinSeconds' // seconds, according to current system time. Returns false if the token is malformed in any way. isAccessTokenExpired(withinSeconds = authCommon.DEFAULT_ACCESS_TOKEN_EXPIRE_WITHIN_SECS) { const token = this.getAccessToken(); if (!token) { return false; } let decodedToken; try { decodedToken = jwtDecode(token); } catch (e) { return false; } if (!decodedToken) { return false; } return decodedToken.exp && Math.floor(Date.now() / 1000) >= decodedToken.exp - withinSeconds; } getAccessToken() { return this.auth.accessToken; } getRefreshToken() { return this.rt; } set(json, authType = '') { if (!json) { return; } let newUserAuth = {}; let setters = []; if (authType) { this.loggedInProviderType = authType; setters.push(this.storage.set(authCommon.USER_LOGGED_IN_PT_KEY, authType)); } if (json[this.codec.refreshToken]) { this.rt = json[this.codec.refreshToken]; delete json[this.codec.refreshToken]; setters.push(this.storage.set(authCommon.REFRESH_TOKEN_KEY, this.rt)); } if (json[this.codec.deviceId]) { this.deviceId = json[this.codec.deviceId]; delete json[this.codec.deviceId]; setters.push(this.storage.set(authCommon.DEVICE_ID_KEY, this.deviceId)); } // Merge in new fields with old fields. Typically the first json value // is complete with every field inside a user auth, but subsequent requests // do not include everything. This merging behavior is safe so long as json // value responses with absent fields do not indicate that the field should // be unset. if (json[this.codec.accessToken]) { newUserAuth.accessToken = json[this.codec.accessToken]; } if (json[this.codec.userId]) { newUserAuth.userId = json[this.codec.userId]; } this.auth = Object.assign(this.auth ? this.auth : {}, newUserAuth); this.authedId = this.auth.userId; setters.push(this.storage.set(authCommon.USER_AUTH_KEY, JSON.stringify(this.auth))); return Promise.all(setters).then(() => this.auth); } _get() { return this.storage.get(authCommon.USER_AUTH_KEY).then((data) => { if (!data) { return {}; } try { return JSON.parse(data); } catch (e) { // Need to back out and clear auth otherwise we will never // be able to do anything useful. return this.clear().then(() => { throw new StitchError('Failure retrieving stored auth'); }); } }); } getLoggedInProviderType() { return this.loggedInProviderType; } parseRedirectFragment(fragment, ourState) { // After being redirected from oauth, the URL will look like: // https://todo.examples.stitch.mongodb.com/#_baas_state=...&_baas_ua=... // This function parses out stitch-specific tokens from the fragment and // builds an object describing the result. const vars = fragment.split('&'); const result = { ua: null, found: false, stateValid: false, lastError: null }; let shouldBreak = false; for (let i = 0; i < vars.length && !shouldBreak; ++i) { const pairParts = vars[i].split('='); const pairKey = decodeURIComponent(pairParts[0]); switch (pairKey) { case authCommon.STITCH_ERROR_KEY: result.lastError = decodeURIComponent(pairParts[1]); result.found = true; shouldBreak = true; break; case authCommon.USER_AUTH_KEY: try { result.ua = this.unmarshallUserAuth(decodeURIComponent(pairParts[1])); result.found = true; } catch (e) { result.lastError = e; } continue; case authCommon.STITCH_LINK_KEY: result.found = true; continue; case authCommon.STATE_KEY: result.found = true; let theirState = decodeURIComponent(pairParts[1]); if (ourState && ourState === theirState) { result.stateValid = true; } continue; default: continue; } } return result; } unmarshallUserAuth(data) { let parts = data.split('$'); if (parts.length !== EMBEDDED_USER_AUTH_DATA_PARTS) { throw new RangeError('invalid user auth data provided: ' + data); } return { [this.codec.accessToken]: parts[0], [this.codec.refreshToken]: parts[1], [this.codec.userId]: parts[2], [this.codec.deviceId]: parts[3] }; } fetchArgsWithLink(fetchArgs, link) { if (link) { fetchArgs.headers.Authorization = `Bearer ${this.getAccessToken()}`; } return fetchArgs; } }