;(function (exports) { 'use strict'; var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; OAUTH3.url.parse = function (url) { // TODO browser // Node should replace this var parser = document.createElement('a'); parser.href = url; return parser; }; OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) { var src = OAUTH3.url.parse(referrerUrl); var dst = OAUTH3.url.parse(redirectUrl); // TODO how should we handle subdomains? // It should be safe for api.example.com to redirect to example.com // But it may not be safe for to example.com to redirect to aj.example.com // It is also probably not safe for sally.example.com to redirect to john.example.com // The client should have a list of allowed URLs to choose from and perhaps a wildcard will do // // api.example.com.evil.com SHOULD NOT match example.com return dst.hostname === src.hostname; }; OAUTH3.url.checkRedirect = function (client, query) { console.warn("[security] URL path checking not yet implemented"); if (!query) { query = client; client = query.client_uri; } client = client.url || client; // it doesn't matter who the referrer is as long as the destination // is an authorized destination for the client in question // (though it may not hurt to pass the referrer's info on to the client) var clientUrl = OAUTH3.url.normalize(client); var redirectUrl = OAUTH3.url.normalize(query.redirect_uri); // General rule: // I can callback to a shorter domain (fewer subs) or a shorter path (on the same domain) // but not a longer (more subs) or different domain or a longer path (on the same domain) // We can callback to an explicitly listed domain (TODO and path) if (OAUTH3.url._isRedirectHostSafe(clientUrl, redirectUrl)) { return true; } var callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK?'+OAUTH3.query.stringify({ 'redirect_uri': redirectUrl , 'allowed_urls': clientUrl , 'client_id': client , 'referrer_uri': OAUTH3.uri.normalize(window.document.referrer) }); if (query.debug) { console.log('Redirect Attack'); console.log(query); window.alert("DEBUG MODE checkRedirect error encountered. Check the console."); } location.href = callbackUrl; return false; }; OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) { // TODO OAUTH3.redirect(clientParams, grants, tokenOrError) // TODO check redirect safeness right here with grants.client.urls // TODO check for '#' and '?'. If none, issue warning and use '?' (for backwards compat) var authz = { access_token: tokenOrError.access_token , token_type: tokenOrError.token_type // 'Bearer' , refresh_token: tokenOrError.refresh_token , expires_in: tokenOrError.expires_in // 1800 (but superceded by jwt.exp) , scope: tokenOrError.scope // superceded by jwt.scp , state: clientParams.state , debug: clientParams.debug }; if (tokenOrError.error) { authz.error = tokenOrError.error.code || tokenOrError.error; authz.error_description = tokenOrError.error.message || tokenOrError.error_description; authz.error_uri = tokenOrError.error.uri || tokenOrError.error_uri; } var redirect = clientParams.redirect_uri + '#' + window.OAUTH3.query.stringify(authz); if (clientParams.debug) { console.info('final redirect_uri:', redirect); window.alert("You're in debug mode so we've taken a pause. Hit OK to continue"); } window.location = redirect; }; OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { // // Example Resource Owner Password Request // (generally for 1st party and direct-partner mobile apps, and webapps) // // POST https://example.com/api/issuer@oauth3.org/access_token // { "grant_type": "password", "client_id": "<>", "scope": "<>" // , "username": "<>", "password": "password" } // opts = opts || {}; if (!opts.password) { if (opts.otp) { // for backwards compat opts.password = opts.otp; // 'otp:' + opts.otpUuid + ':' + opts.otp; } } var args = directive.access_token; var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined; // TODO require user agent var params = { client_id: opts.client_id || opts.client_uri , client_uri: opts.client_uri , grant_type: 'password' , username: opts.username , password: opts.password || otpCode || undefined , totp: opts.totp || opts.totpToken || opts.totp_token || undefined , otp: otpCode , password_type: otpCode && 'otp' , otp_code: otpCode , otp_uuid: opts.otpUuid || opts.otp_uuid || undefined , user_agent: opts.userAgent || opts.useragent || opts.user_agent || undefined // AJ's Macbook , jwk: (opts.rememberDevice || opts.remember_device) && opts.jwk || undefined //, "public_key": opts.rememberDevice && opts.publicKey || undefined //, "public_key_type": opts.rememberDevice && opts.publicKeyType || undefined // RSA/ECDSA //, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key , debug: opts.debug || undefined }; if (opts.client_uri) { params.clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos; if (!params.clientAgreeTos) { throw new Error('Developer Error: missing clientAgreeTos uri'); } } var scope = opts.scope || directive.authn_scope; if (scope) { params.scope = OAUTH3.scope.stringify(scope); } var uri = args.url; var body; if ('GET' === args.method.toUpperCase()) { uri += '?' + OAUTH3.query.stringify(params); } else { body = params; } return { url: OAUTH3.url.resolve(directive.api, uri) , method: args.method , data: body }; }; OAUTH3.urls.grants = function (directive, opts) { // directive = { issuer, authorization_decision } // opts = { response_type, scopes{ granted, requested, pending, accepted } } var grantsDir = directive.grants; if (!grantsDir) { throw new Error("provider doesn't support grants"); } if (!opts) { throw new Error("You must supply a directive and an options object."); } if (!opts.client_id) { throw new Error("You must supply options.client_id."); } if (!opts.session) { throw new Error("You must supply options.session."); } if (!opts.referrer) { console.warn("You should supply options.referrer"); } if (!opts.method) { console.warn("You should supply options.method as either 'GET', or 'POST'"); opts.method = grantsDir.method || 'GET'; } if ('POST' === opts.method) { if ('string' !== typeof opts.scope) { throw new Error("You must supply options.scope as a comma-delimited string of scopes"); } if ('string' !== typeof opts.sub) { console.log("provide 'sub' to urls.grants to specify the PPID for the client"); } } var url = OAUTH3.url.resolve(directive.api, grantsDir.url) .replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED') .replace(/(:azp|:client_id)/g, !opts.all && OAUTH3.uri.normalize(opts.client_id || opts.client_uri) || '') .replace(/\/\/$/, '/') // if there's a double slash due to the sub not existing ; var data = { client_id: opts.client_id , client_uri: opts.client_uri , referrer: opts.referrer , scope: opts.scope , sub: opts.sub }; var body; if ('GET' === opts.method) { url += '?' + OAUTH3.query.stringify(data); } else { body = data; } return { method: opts.method , url: url , data: body , session: opts.session }; }; //OAUTH3.urls.accessToken = function (directive, opts) OAUTH3.urls.clientToken = function (directive, opts) { var tokenDir = directive.access_token; if (!tokenDir) { throw new Error("provider doesn't support getting access tokens"); } if (!opts) { throw new Error("You must supply a directive and an options object."); } if (!(opts.azp || opts.client_id)) { throw new Error("You must supply options.client_id."); } if (!opts.session) { throw new Error("You must supply options.session."); } if (!opts.method) { opts.method = tokenDir.method || 'POST'; } var params = { grant_type: 'issuer_token' , client_id: opts.azp || opts.client_id , azp: opts.azp || opts.client_id , aud: opts.aud , exp: opts.exp , refresh_token: opts.refresh_token , refresh_exp: opts.refresh_exp }; var url = OAUTH3.url.resolve(directive.api, tokenDir.url); var body; if ('GET' === opts.method) { url += '?' + OAUTH3.query.stringify(params); } else { body = params; } return { method: opts.method , url: url , data: body , session: opts.session }; }; OAUTH3.urls.publishKey = function (directive, opts) { var jwkDir = directive.publish_jwk; if (!jwkDir) { throw new Error("provider doesn't support publishing public keys"); } if (!opts) { throw new Error("You must supply a directive and an options object."); } if (!opts.session) { throw new Error("You must supply 'options.session'."); } if (!(opts.public_key || opts.publicKey)) { throw new Error("You must supply 'options.public_key'."); } var url = OAUTH3.url.resolve(directive.api, jwkDir.url) .replace(/(:sub|:account_id)/g, opts.session.token.sub) ; return { method: jwkDir.method || opts.method || 'POST' , url: url , data: opts.public_key || opts.publicKey , session: opts.session }; }; OAUTH3.urls.credentialMeta = function (directive, opts) { return OAUTH3.url.resolve(directive.api, directive.credential_meta.url) .replace(':type', 'email') .replace(':id', opts.email) }; OAUTH3.authn = OAUTH3.authn || {}; OAUTH3.authn.loginMeta = function (directive, opts) { var url = OAUTH3.urls.credentialMeta(directive, opts); return OAUTH3.request({ method: directive.credential_meta.method || 'GET' // TODO lint urls // TODO client_uri , url: url }); }; OAUTH3.urls.otp = function (directive, opts) { // TODO client_uri return { method: directive.credential_otp.method || 'POST' , url: OAUTH3.url.resolve(directive.api, directive.credential_otp.url) , data: { // TODO replace with signed hosted file client_agree_tos: 'oauth3.org/tos/draft' // TODO unbreak the client_uri option (if broken) , client_id: /*opts.client_id ||*/ OAUTH3.uri.normalize(directive.issuer) // In this case, the issuer is its own client , client_uri: /*opts.client_uri ||*/ OAUTH3.uri.normalize(directive.issuer) , request_otp: true , username: opts.email } }; }; OAUTH3.authn.otp = function (directive, opts) { var preq = OAUTH3.urls.otp(directive, opts); return OAUTH3.request(preq); }; OAUTH3.authn.resourceOwnerPassword = function (directive, opts) { var providerUri = directive.issuer; return OAUTH3.request(OAUTH3.urls.resourceOwnerPassword(directive, opts)).then(function (resp) { var data = resp.data; data.provider_uri = providerUri; if (data.error) { return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data)); } return OAUTH3.hooks.session.refresh( opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } , data ); }).then(function (session) { if (!opts.rememberDevice && !opts.remember_device) { return session; } return OAUTH3.PromiseA.resolve().then(function () { if (!OAUTH3.crypto) { throw new Error("OAuth3 crypto library unavailable"); } return OAUTH3.crypto.createKeyPair().then(function (keyPair) { return OAUTH3.request(OAUTH3.urls.publishKey(directive, { session: session , publicKey: keyPair.publicKey })).then(function () { return OAUTH3.hooks.keyPairs.set(session.token.sub, keyPair); }); }); }).then(function () { return session; }, function (err) { console.error('failed to save keys to remember device', err); window.alert('Failed to remember device'); return session; }); }); }; OAUTH3.authz = {}; OAUTH3.authz.scopes = function (providerUri, session, clientParams) { var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer); var scope = clientParams.scope || 'authn@oauth3.org'; if ('authn@oauth3.org' === scope.toString()) { // implicit ppid grant is automatic console.warn('[security] fix scope checking on backend so that we can do automatic grants'); // TODO check user preference if implicit ppid grant is allowed //return generateToken(session, clientObj); } return OAUTH3.hooks.grants.get(session.token.sub, clientUri).then(function (granted) { if (granted) { if (typeof granted.scope === 'string') { return OAUTH3.scope.parse(granted.scope); } else if (Array.isArray(granted.scope)) { return granted.scope; } } return OAUTH3.authz.grants(providerUri, { method: 'GET' , client_id: clientUri , client_uri: clientUri , session: session }).then(function (results) { return results.grants; }, function (err) { if (!/no .*grants .*found/i.test(err.message)) { throw err; } return []; }); }).then(function (granted) { var requested = OAUTH3.scope.parse(scope); var accepted = []; var pending = []; requested.forEach(function (scp) { if (granted.indexOf(scp) < 0) { pending.push(scp); } else { accepted.push(scp); } }); return { requested: requested // all requested, now , granted: granted // all granted, ever , accepted: accepted // intersection of granted (ever) and requested (now) , pending: pending // not yet accepted }; }); }; OAUTH3.authz.grants = function (providerUri, opts) { return OAUTH3.discover(providerUri, { client_id: providerUri , debug: opts.debug }).then(function (directive) { return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts); }).then(function (grantsResult) { var grants = grantsResult.originalData || grantsResult.data; if (grants.error) { return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants)); } // the responses for GET and POST requests are now the same, so we should alway be able to // use the response and save it the same way. if (opts.all || ('GET' !== opts.method && 'POST' !== opts.method)) { return grants; } OAUTH3.hooks.grants.set(grants.sub, grants.azp, grants); return { client: grants.azp , clientSub: grants.azpSub , grants: OAUTH3.scope.parse(grants.scope) }; }); }; function calcExpiration(exp, now) { if (!exp) { return; } if (typeof exp === 'string') { var match = /^(\d+\.?\d*) *(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(exp); if (!match) { return now; } var num = parseFloat(match[1]); var type = (match[2] || 's').toLowerCase()[0]; switch (type) { case 'y': num *= 365.25; /* falls through */ case 'd': num *= 24; /* falls through */ case 'h': num *= 60; /* falls through */ case 'm': num *= 60; /* falls through */ case 's': exp = num; } } if (typeof exp !== 'number') { throw new Error('invalid expiration provided: '+exp); } now = now || Math.floor(Date.now() / 1000); if (exp > now) { return exp; } else if (exp > 31557600) { console.warn('tried to set expiration to more that a year'); exp = 31557600; } return now + exp; } OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) { if (!OAUTH3.url.checkRedirect(clientParams.client_uri, clientParams)) { return; } if ('token' !== clientParams.response_type) { var message; if ('code' === clientParams.response_type) { message = "Authorization Code Redirect NOT IMPLEMENTED"; } else { message = "Authorization response type '"+clientParams.response_type+"' not supported"; } window.alert(message); throw new Error(message); } var prom; if (scopes.new) { prom = OAUTH3.authz.grants(providerUri, { session: session , method: 'POST' , client_id: clientParams.client_uri , referrer: clientParams.referrer , scope: scopes.accepted.concat(scopes.new).join(',') }); } else { prom = OAUTH3.PromiseA.resolve(); } return prom.then(function () { return OAUTH3.hooks.keyPairs.get(session.token.sub); }).then(function (keyPair) { if (!keyPair) { return OAUTH3.discover(providerUri, { client_id: providerUri , debug: clientParams.debug }).then(function (directive) { return OAUTH3.request(OAUTH3.urls.clientToken(directive, { method: 'POST' , session: session , referrer: clientParams.referrer , response_type: clientParams.response_type , client_id: clientParams.client_uri , azp: clientParams.client_uri , aud: clientParams.aud , exp: clientParams.exp , refresh_token: clientParams.refresh_token , refresh_exp: clientParams.refresh_exp , debug: clientParams.debug })).then(function (result) { return result.originalData || result.data; }); }); } return OAUTH3.hooks.grants.get(keyPair.sub, clientParams.client_uri).then(function (grant) { var now = Math.floor(Date.now()/1000); var payload = { iat: now , iss: providerUri , aud: clientParams.aud || providerUri , azp: clientParams.client_uri , sub: grant.azpSub , scope: OAUTH3.scope.stringify(grant.scope) , }; var signProms = []; signProms.push(OAUTH3.jwt.sign(Object.assign({ exp: calcExpiration(clientParams.exp || '1h', now) }, payload), keyPair)); // if (clientParams.refresh_token) { signProms.push(OAUTH3.jwt.sign(Object.assign({ exp: calcExpiration(clientParams.refresh_exp, now) }, payload), keyPair)); // } return OAUTH3.PromiseA.all(signProms).then(function (tokens) { console.log('created new tokens for client'); return { access_token: tokens[0] , refresh_token: tokens[1] , scope: OAUTH3.scope.stringify(grant.scope) , token_type: 'bearer' }; }); }); }).then(function (session) { // TODO limit refresh token to an expirable token // TODO inform client not to persist token OAUTH3.url.redirect(clientParams, scopes, session); }, function (err) { console.error('unexpected error creating client tokens', err); OAUTH3.url.redirect(clientParams, scopes, {error: err}); }); }; OAUTH3.requests = {}; //OAUTH3.accounts = {}; OAUTH3.requests.accounts = {}; OAUTH3.urls.accounts = {}; OAUTH3.urls.accounts._ = function (directives, directive, session, opts) { opts = opts || {}; var dir = directive || { //url: OAUTH3.url.normalize(directives.api) + '/api/issuer@oauth3.org/accounts/:accountId' url: OAUTH3.url.normalize(directives.api) + '/api/issuer@oauth3.org/acl/profiles/:accountId' //, method: 'GET' , bearer: 'Bearer' }; var url = dir.url .replace(/:accountId/, opts.accountId || '') .replace(/\/$/, '') ; return { url: url //, method: dir.method || 'POST' , session: session /* , headers: { 'Authorization': (dir.bearer || 'Bearer') + ' ' + (session.access_token || session.accessToken) } */ }; }; OAUTH3.urls.accounts.get = function (directives, session) { var urlObj = OAUTH3.urls.accounts._(directives, directives.account, session); urlObj.method = (directives.account || { method: 'GET' }).method; return urlObj; }; OAUTH3.urls.accounts.update = function (directives, session, opts) { var urlObj = OAUTH3.urls.accounts._(directives, directives.update_account, session, opts); urlObj.method = (directives.update_account || { method: 'POST' }).method; urlObj.json = { name: opts.name , comment: opts.comment , displayName: opts.displayName , priority: opts.priority }; return urlObj; }; OAUTH3.urls.accounts.create = function (directives, session, account) { var urlObj = OAUTH3.urls.accounts._(directives, directives.create_account, session); var profile = { nick: account.display_name // "name" is unique and what would be reserved in a url {{name}}.issuer.org or issuer.org/users/{{name}} , name: account.name , comment: account.comment , display_name: account.display_name , priority: account.priority }; var credentials = [ { token: session.access_token } ]; urlObj.method = (directives.create_account || { method: 'POST' }).method; urlObj.json = { // TODO fix the server to just use one scheme // account = { nick, self: { comment, username } } // account = { name, comment, display_name, priority } credentials: credentials , profile: profile // 'account' is deprecated in favor of 'profile' , account: profile // 'logins' is deprecated in favor of 'credentials' , logins: credentials }; return urlObj; }; OAUTH3.requests.accounts.get = function (directives, session) { var urlObj = OAUTH3.urls.accounts.get(directives, session); return OAUTH3.request(urlObj); }; OAUTH3.requests.accounts.update = function (directives, session, opts) { var urlObj = OAUTH3.urls.accounts.update(directives, session, opts); return OAUTH3.request(urlObj); }; OAUTH3.requests.accounts.create = function (directive, session, account) { var urlObj = OAUTH3.urls.accounts.create(directives, session, account); return OAUTH3.request(urlObj); }; OAUTH3.hooks.grants = { get: function (id, clientUri) { OAUTH3.hooks._checkStorage('grants', 'get'); if (!id) { throw new Error("id is not set"); } if (!clientUri) { throw new Error("clientUri is not set"); } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(id, OAUTH3.uri.normalize(clientUri))); } , set: function (id, clientUri, grants) { OAUTH3.hooks._checkStorage('grants', 'set'); if (!id) { throw new Error("id is not set"); } if (!clientUri) { throw new Error("clientUri is not set"); } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.set(id, OAUTH3.uri.normalize(clientUri), grants)); } , all: function () { OAUTH3.hooks._checkStorage('grants', 'all'); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.all()); } , clear: function () { OAUTH3.hooks._checkStorage('grants', 'clear'); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.clear()); } }; OAUTH3.hooks.keyPairs = { get: function (id) { OAUTH3.hooks._checkStorage('keyPairs', 'get'); if (!id) { throw new Error("id is not set"); } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.get(id)); } , set: function (id, keyPair) { OAUTH3.hooks._checkStorage('keyPairs', 'set'); if (!keyPair && id.privateKey && id.publicKey && id.sub) { keyPair = id; id = keyPair.sub; } if (!keyPair) { return OAUTH3.PromiseA.reject(new Error("no key pair provided to save")); } if (!id) { throw new Error("id is not set"); } keyPair.sub = keyPair.sub || id; return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.set(id, keyPair)); } , all: function () { OAUTH3.hooks._checkStorage('keyPairs', 'all'); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.all()); } , clear: function () { OAUTH3.hooks._checkStorage('keyPairs', 'clear'); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.clear()); } }; OAUTH3.hooks.session.get = function (providerUri, id) { OAUTH3.hooks._checkStorage('sessions', 'get'); var sessProm = OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id)); if (providerUri !== OAUTH3.clientUri(window.location)) { return sessProm; } return sessProm.then(function (session) { if (session && OAUTH3.jwt.freshness(session.token) === 'fresh') { return session; } return OAUTH3.hooks.keyPairs.all().then(function (keyPairs) { var pair; if (id) { pair = keyPairs[id]; } else if (Object.keys(keyPairs).length === 1) { id = Object.keys(keyPairs)[0]; pair = keyPairs[id]; } else if (Object.keys(keyPairs).length > 1) { console.error("too many users, don't know which key to use"); } if (!pair) { // even if the access token isn't fresh, the session might have a refresh token return session; } var now = Math.floor(Date.now()/1000); var payload = { iat: now , iss: providerUri , aud: providerUri , azp: providerUri , sub: pair.sub || id , scope: '' , exp: now + 3600 }; return OAUTH3.jwt.sign(payload, pair.privateKey).then(function (token) { console.log('created new token for provider'); return OAUTH3.hooks.session.refresh( { provider_uri: providerUri, client_uri: providerUri || providerUri } , { access_token: token } ); }); }); }); }; OAUTH3._defaultStorage.grants = { prefix: 'grants-' , get: function (id, clientUri) { var key = this.prefix + id+'/'+clientUri; var result = JSON.parse(window.localStorage.getItem(key) || 'null'); return OAUTH3.PromiseA.resolve(result); } , set: function (id, clientUri, grants) { var key = this.prefix + id+'/'+clientUri; window.localStorage.setItem(key, JSON.stringify(grants)); return this.get(clientUri); } , all: function () { var prefix = this.prefix; var result = {}; OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) { var split = key.replace(prefix, '').split('/'); if (!result[split[0]]) { result[split[0]] = {}; } result[split[0]][split[1]] = JSON.parse(window.localStorage.getItem(key) || 'null'); }); return OAUTH3.PromiseA.resolve(result); } , clear: function () { OAUTH3._defaultStorage._getStorageKeys(this.prefix, window.localStorage).forEach(function (key) { window.localStorage.removeItem(key); }); return OAUTH3.PromiseA.resolve(); } }; OAUTH3._defaultStorage.keyPairs = { prefix: 'key_pairs-' , get: function (id) { var result = JSON.parse(window.localStorage.getItem(this.prefix + id) || 'null'); return OAUTH3.PromiseA.resolve(result); } , set: function (id, keyPair) { window.localStorage.setItem(this.prefix + id, JSON.stringify(keyPair)); return this.get(id); } , all: function () { var prefix = this.prefix; var result = {}; OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) { result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || 'null'); }); return OAUTH3.PromiseA.resolve(result); } , clear: function () { OAUTH3._defaultStorage._getStorageKeys(this.prefix, window.localStorage).forEach(function (key) { window.localStorage.removeItem(key); }); return OAUTH3.PromiseA.resolve(); } }; OAUTH3._browser.isIframe = function isIframe () { try { return window.self !== window.top; } catch (e) { return true; } }; }('undefined' !== typeof exports ? exports : window));