/* global Promise */ ;(function (exports) { 'use strict'; if ('undefined' !== typeof window && 'https:' !== window.location.protocol) { window.alert("You must use https. We suggest using caddy as your webserver (or serve-https if testing locally)"); } var OAUTH3 = exports.OAUTH3 = { clientUri: function (location) { return OAUTH3.uri.normalize(location.host + (location.pathname || '')); } , error: { parse: function (providerUri, params) { var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'"); err.uri = params.error_uri || params.error.uri; err.code = params.error.code || params.error; return err; } , create: function (opts) { var err = new Error(opts.message); err.code = opts.code; err.uri = opts.uri || opts.url; err.subErr = opts.subErr; return err; } } , _binStr: { bufferToBinStr: function (buf) { return Array.prototype.map.call(new Uint8Array(buf), function(ch) { return String.fromCharCode(ch); }).join(''); } , binStrToBuffer: function (str) { var buf; if ('undefined' !== typeof Uint8Array) { buf = new Uint8Array(str.length); } else { buf = []; } Array.prototype.forEach.call(str, function (ch, ind) { buf[ind] = ch.charCodeAt(0); }); return buf; } } , _base64: { atob: function (base64) { // atob must be called from the global context // http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome return (exports.atob || require('atob'))(base64); } , btoa: function (b64) { // for directive passing in .well-known/oauth3 // http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome return (exports.btoa || require('btoa'))(b64); } , decodeUrlSafe: function (b64) { // URL-safe Base64 to Base64 // https://en.wikipedia.org/wiki/Base64 // https://gist.github.com/catwell/3046205 var mod = b64.length % 4; if (2 === mod) { b64 += '=='; } if (3 === mod) { b64 += '='; } b64 = b64.replace(/-/g, '+').replace(/_/g, '/'); return OAUTH3._base64.atob(b64); } , encodeUrlSafe: function (b64) { // for directive passing in .well-known/oauth3 // Base64 to URL-safe Base64 b64 = OAUTH3._base64.btoa(b64); b64 = b64.replace(/\+/g, '-').replace(/\//g, '_'); b64 = b64.replace(/=+/g, ''); return b64; } , urlSafeToBuffer: function (str) { return OAUTH3._binStr.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str)); } , bufferToUrlSafe: function (buf) { return OAUTH3._base64.encodeUrlSafe(OAUTH3._binStr.bufferToBinStr(buf)); } } , uri: { normalize: function (uri) { if ('string' !== typeof uri) { throw new Error("attempted to normalize non-string URI: "+JSON.stringify(uri)); } // tested with // example.com // example.com/ // http://example.com // https://example.com/ return uri .replace(/^(https?:\/\/)?/i, '') .replace(/\/?$/, '') ; } } , url: { normalize: function (url) { if ('string' !== typeof url) { throw new Error("attempted to normalize non-string URL: "+JSON.stringify(url)); } // tested with // example.com // example.com/ // http://example.com // https://example.com/ return url .replace(/^(https?:\/\/)?/i, 'https://') .replace(/\/?$/, '') ; } , resolve: function (base, next) { if (/^https:\/\//i.test(next)) { return next; } return this.normalize(base) + '/' + this._normalizePath(next); } , _normalizePath: function (path) { return path.replace(/^\//, '').replace(/\/$/, ''); } } , query: { parse: function (search) { // needed for .well-known/oauth3 // parse a query or a hash if (-1 !== ['#', '?'].indexOf(search[0])) { search = search.substring(1); } // Solve for case of search within hash // example: #/authorization_dialog/?state=...&redirect_uri=... var queryIndex = search.indexOf('?'); if (-1 !== queryIndex) { search = search.substr(queryIndex + 1); } var args = search.split('&'); var argsParsed = {}; var i, arg, kvp, key, value; for (i = 0; i < args.length; i += 1) { arg = args[i]; if (-1 === arg.indexOf('=')) { argsParsed[decodeURIComponent(arg).trim()] = true; } else { kvp = arg.split('='); key = decodeURIComponent(kvp[0]).trim(); value = decodeURIComponent(kvp[1]).trim(); argsParsed[key] = value; } } return argsParsed; } , stringify: function (params) { var qs = []; Object.keys(params).forEach(function (key) { // TODO nullify instead? if ('undefined' === typeof params[key]) { return; } if ('scope' === key) { params[key] = OAUTH3.scope.stringify(params[key]); } qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); }); return qs.join('&'); } } , scope: { parse: function (scope) { return (scope||'').toString().split(/[+, ]+/g); } , stringify: function (scope) { if (Array.isArray(scope)) { scope = scope.join(','); } return scope; } } , randomState: function () { // TODO put in different file for browser vs node try { return Array.prototype.slice.call( OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(16)) ).map(function (ch) { return (ch).toString(16); }).join(''); } catch(e) { return OAUTH3.utils._insecureRandomState(); } } , _insecureRandomState: function () { var i; var ch; var str; // TODO use fisher-yates on 0..255 and select [0] 16 times // [security] https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.5qx0bf95a // https://github.com/v8/v8/blob/b0e4dce6091a8777bda80d962df76525dc6c5ea9/src/js/math.js#L135-L144 // Note: newer versions of v8 do not have this bug, but other engines may still console.warn('[security] crypto.getRandomValues() failed, falling back to Math.random()'); str = ''; for (i = 0; i < 32; i += 1) { ch = Math.round(Math.random() * 255).toString(16); if (ch.length < 2) { ch = '0' + ch; } str += ch; } return str; } , jwk: { get: function (decoded) { return OAUTH3.discover(decoded.payload.iss).then(function (directives) { var urlObj = OAUTH3.urls.jwk(directives, decoded); return OAUTH3.request(urlObj).catch(function (err) { return OAUTH3.PromiseA.reject({ message: 'failed to retrieve public key from token issuer' , code: 'E_NO_PUB_KEY' , url: 'https://oauth3.org/docs/errors#E_NO_PUB_KEY' , subErr: err.toString() }); }); }, function (err) { return OAUTH3.PromiseA.reject({ message: 'token issuer is not a valid OAuth3 provider' , code: 'E_INVALID_ISS' , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS' , subErr: err.toString() }); }).then(function (res) { if (res.data.error) { return OAUTH3.PromiseA.reject(res.data.error); } return res.data; }); } , verifyToken: function (token) { var decoded; if (!token) { return OAUTH3.PromiseA.reject({ message: 'no token provided' , code: 'E_NO_TOKEN' , url: 'https://oauth3.org/docs/errors#E_NO_TOKEN' }); } try { decoded = OAUTH3.jwt.decode(token, {complete: true}); } catch (e) {} if (!decoded) { return OAUTH3.PromiseA.reject({ message: 'provided token not a JSON Web Token' , code: 'E_NOT_JWT' , url: 'https://oauth3.org/docs/errors#E_NOT_JWT' }); } return OAUTH3.jwk.get(decoded).then(function (jwk) { var opts = {}; if (Array.isArray(jwk.alg)) { opts.algorithms = jwk.alg; } else if (typeof jwk.alg === 'string') { opts.algorithms = [ jwk.alg ]; } try { return OAUTH3.jwt.verify(token, jwk, opts); } catch (err) { return OAUTH3.PromiseA.reject({ message: 'token verification failed' , code: 'E_INVALID_TOKEN' , url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN' , subErr: err.toString() }); } }); } } , jwt: { // decode only (no verification) decode: function (token, opts) { // 'abc.qrs.xyz' // [ 'abc', 'qrs', 'xyz' ] // {} var parts = token.split(/\./g); var err; if (parts.length !== 3) { err = new Error("Invalid JWT: required 3 '.' separated components not "+parts.length); err.code = 'E_INVALID_JWT'; throw err; } if (!opts || !opts.complete) { return JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1])); } return { header: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[0])) , payload: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1])) }; } , verify: function (token, jwk/*, opts*/) { if (!OAUTH3.crypto) { return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable")); } jwk = jwk.publicKey || jwk; var parts = token.split(/\./g); var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.')); var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]); var decoded = OAUTH3.jwt.decode(token, { complete: true }); // TODO disallow none and hmac algorithms // https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ if (!decoded.header.alg || 'none' === decoded.header.alg.toString() || /^HS/i.test(decoded.header.alg.toString())) { throw new Error("token algorithm '" + decoded.header.alg + "' is not accepted"); } return OAUTH3.crypto.core.verify(jwk, data, signature).then(function () { return decoded; }); } , sign: function (payload, jwk) { if (!OAUTH3.crypto) { return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable")); } jwk = jwk.private_key || jwk.privateKey || jwk; var prom; if (jwk.kid) { prom = OAUTH3.PromiseA.resolve(jwk.kid); } else { prom = OAUTH3.crypto.thumbprintJwk(jwk); } return prom.then(function (kid) { // Currently the crypto part of the OAuth3 library only supports ES256 var header = {type: 'JWT', alg: 'ES256', kid: kid}; var input = [ OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null)) , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null)) ].join('.'); return OAUTH3.crypto.core.sign(jwk, OAUTH3._binStr.binStrToBuffer(input)) .then(OAUTH3._base64.bufferToUrlSafe) .then(function (signature) { return input + '.' + signature; }); }); } , freshness: function (tokenMeta, staletime, now) { // If the token doesn't expire then it's always fresh. if (!tokenMeta.exp) { return 'fresh'; } staletime = staletime || (15 * 60); now = now || Date.now(); // This particular number used to check if time is in milliseconds or seconds will work // for any date between the years 1973 and 5138. if (now > 1e11) { now = Math.round(now / 1000); } var exp = parseInt(tokenMeta.exp, 10) || 0; if (exp < now) { return 'expired'; } else if (exp < now + staletime) { return 'stale'; } else { return 'fresh'; } } } , urls: { rpc: function (providerUri, opts) { if (!providerUri) { throw new Error("cannot run rpc without providerUri"); } if (!opts.client_id) { throw new Error("cannot run rpc without options.client_id"); } var clientId = OAUTH3.url.normalize(opts.client_id || opts.client_uri); providerUri = OAUTH3.url.normalize(providerUri); var params = { state: opts.state || OAUTH3.utils.randomState() , redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/') , response_type: 'rpc' , _method: 'GET' , _scheme: opts._scheme , _pathname: opts._pathname , debug: opts.debug || undefined }; var toRequest = { url: providerUri + '/.well-known/oauth3/#/?' + OAUTH3.query.stringify(params) , state: params.state , method: 'GET' , query: params }; return toRequest; } , broker: function (providerUri, opts) { opts._scheme = "localstorage:"; opts._pathname = "issuer"; return OAUTH3.urls.rpc(providerUri, opts); } , discover: function (providerUri, opts) { return OAUTH3.urls.directives(providerUri, opts); } , directives: function (providerUri, opts) { opts._pathname = ".well-known/oauth3/scopes.json"; return OAUTH3.urls.rpc(providerUri, opts); } , jwk: function (directives, decoded) { var sub = decoded.payload.sub || decoded.payload.ppid || decoded.payload.appScopedId; if (!sub) { throw OAUTH3.error.create({ message: 'token missing sub' , code: 'E_MISSING_SUB' , url: 'https://oauth3.org/docs/errors#E_MISSING_SUB' }); } var kid = decoded.header.kid || decoded.payload.kid; if (!kid) { throw OAUTH3.error.create({ message: 'token missing kid' , code: 'E_MISSING_KID' , url: 'https://oauth3.org/docs/errors#E_MISSING_KID' }); } if (!decoded.payload.iss) { throw OAUTH3.error.create({ message: 'token missing iss' , code: 'E_MISSING_ISS' , url: 'https://oauth3.org/docs/errors#E_MISSING_ISS' }); } var args = (directives || {}).retrieve_jwk; if (typeof args === 'string') { args = { url: args, method: 'GET' }; } if (typeof (args || {}).url !== 'string') { throw OAUTH3.error.create({ message: 'token issuer does not support retrieving JWKs' , code: 'E_INVALID_ISS' , url: 'https://oauth3.org/docs/errors#E_INVALID_ISS' }); } var params = { sub: sub , kid: kid }; var url = args.url; var body; Object.keys(params).forEach(function (key) { if (url.indexOf(':'+key) !== -1) { url = url.replace(':'+key, params[key]); delete params[key]; } }); if (Object.keys(params).length > 0) { if ('GET' === (args.method || 'GET').toUpperCase()) { url += '?' + OAUTH3.query.stringify(params); } else { body = params; } } return { url: OAUTH3.url.resolve(directives.api, url) , method: args.method , data: body }; } , implicitGrant: function (directive, opts) { // // Example Implicit Grant Request // (for generating a browser-only session, not a session on your server) // // GET https://example.com/api/issuer@oauth3.org/authorization_dialog // ?response_type=token // &scope=`encodeURIComponent('profile.login profile.email')` // &state=`cryptoutil.random().toString('hex')` // &client_id=xxxxxxxxxxx // &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')` // // NOTE: `redirect_uri` itself may also contain URI-encoded components // opts = opts || {}; var type = 'authorization_dialog'; var responseType = 'token'; var scope = opts.scope || directive.authn_scope; var args = directive[type]; var uri = args.url; var state = opts.state || OAUTH3.utils.randomState(); var params = { debug: opts.debug || undefined , client_uri: opts.client_uri || opts.clientUri || undefined , client_id: opts.client_id || opts.client_uri || undefined , subject: opts.subject , state: state }; var result; console.log('implicitGrant opts.subject: ', opts.subject); params.response_type = responseType; if (scope) { params.scope = OAUTH3.scope.stringify(scope); } if (!opts.redirect_uri) { // TODO consider making this optional //console.warn("auto-generating redirect_uri from hard-coded callback.html" // + " (should be configurable... but then redirect_uri could just be manually-generated)"); opts.redirect_uri = OAUTH3.url.resolve( OAUTH3.url.normalize(params.client_uri) , '.well-known/oauth3/callback.html' ); } params.redirect_uri = opts.redirect_uri; uri += '?' + OAUTH3.query.stringify(params); result = { url: uri , state: state , method: args.method , query: params }; return result; } , refreshToken: function (directive, opts) { // grant_type=refresh_token // Example Refresh Token Request // (generally for 1st or 3rd party server-side, mobile, and desktop apps) // // POST https://example.com/api/oauth3/access_token // { "grant_type": "refresh_token", "client_id": "<>", "scope": "<>" // , "username": "<>", "password": "password" } // opts = opts || {}; var refresh_token = opts.refresh_token || (opts.session && opts.session.refresh_token); var err; if (!refresh_token) { err = new Error('refreshing a token requires a refresh token'); err.code = 'E_NO_TOKEN'; throw err; } if (OAUTH3.jwt.freshness(OAUTH3.jwt.decode(refresh_token)) === 'expired') { err = new Error('refresh token has also expired, login required again'); err.code = 'E_EXPIRED_TOKEN'; throw err; } var scope = opts.scope || directive.authn_scope; var args = directive.access_token; var params = { "grant_type": 'refresh_token' , "refresh_token": refresh_token , "response_type": 'token' , "client_id": opts.client_id || opts.client_uri , "client_uri": opts.client_uri , debug: opts.debug || undefined }; var uri = args.url; var body; if (opts.client_secret) { // TODO not allowed in the browser console.warn("if this is a browser, you must not use client_secret"); params.client_secret = opts.client_secret; } if (scope) { params.scope = OAUTH3.scope.stringify(scope); } if ('GET' === args.method.toUpperCase()) { uri += '?' + OAUTH3.query.stringify(params); } else { body = params; } return { url: uri , method: args.method , data: body }; } , logout: function (directive, opts) { // action=logout // Example Logout Request // (generally for 1st or 3rd party server-side, mobile, and desktop apps) // // GET https://example.com/#/logout/ // ?client_id=<> // &access_token=<> // &sub=<> // // Note that the use of # keeps certain parameters from traveling across // the network at all (and we use https anyway) // opts = opts || {}; var action = 'logout'; var args = directive[action]; var state = opts.state || OAUTH3.utils.randomState(); var params = { action: action //, response_type: 'confirmation' , client_id: opts.client_id || opts.client_uri , client_uri: opts.client_uri || opts.client_id , state: state , redirect_uri: opts.redirect_uri = OAUTH3.url.resolve( OAUTH3.url.normalize(opts.client_uri || opts.client_id) , '.well-known/oauth3/callback.html' ) , debug: opts.debug }; 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.issuer, uri) , method: args.method , state: state , data: body }; } } , hooks: { _checkStorage: function (grpName, funcName) { if (!OAUTH3._hooks) { OAUTH3._hooks = {}; } if (!OAUTH3._hooks[grpName]) { console.log('using default storage for '+grpName+', set OAUTH3._hooks.'+grpName+' for custom storage'); OAUTH3._hooks[grpName] = OAUTH3._defaultStorage[grpName]; } if (!OAUTH3._hooks[grpName][funcName]) { throw new Error("'"+funcName+"' is not defined for custom "+grpName+" storage"); } } , directives: { get: function (providerUri) { OAUTH3.hooks._checkStorage('directives', 'get'); if (!providerUri) { throw new Error("providerUri is not set"); } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.get(OAUTH3.uri.normalize(providerUri))); } , set: function (providerUri, directives) { OAUTH3.hooks._checkStorage('directives', 'set'); if (!providerUri) { throw new Error("providerUri is not set"); } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.set(OAUTH3.uri.normalize(providerUri), directives)); } , all: function () { OAUTH3.hooks._checkStorage('directives', 'all'); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.all()); } , clear: function () { OAUTH3.hooks._checkStorage('directives', 'clear'); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.clear()); } } , scopes: { get: function(providerUri) { //TODO: retrieve cached scopes } , set: function(providerUri, scopes) { //TODO: cache scopes } } , session: { refresh: function (oldSession, newSession) { var providerUri = oldSession.provider_uri; var clientUri = oldSession.client_uri; ['access_token', 'token', 'client_uri', 'refresh', 'refresh_token', 'expires_in', 'provider_uri', 'scope', 'token_type'].forEach(function (key) { oldSession[key] = undefined; }); Object.keys(newSession).forEach(function (key) { oldSession[key] = newSession[key]; }); // info about the session of this API call oldSession.provider_uri = providerUri; // aud oldSession.client_uri = clientUri; // azp // info about the newly-discovered token oldSession.token = OAUTH3.jwt.decode(oldSession.access_token); oldSession.token.sub = oldSession.token.sub || (oldSession.token.acx||{}).id || ((oldSession.token.axs||[])[0]||{}).appScopedId || ((oldSession.token.axs||[])[0]||{}).id ; oldSession.token.client_uri = clientUri; oldSession.token.provider_uri = providerUri; if (oldSession.refresh_token) { oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token); oldSession.refresh.sub = oldSession.refresh.sub || (oldSession.refresh.acx||{}).id || ((oldSession.refresh.axs||[])[0]||{}).appScopedId || ((oldSession.refresh.axs||[])[0]||{}).id ; oldSession.refresh.provider_uri = providerUri; } // set for a set of audiences return OAUTH3.hooks.session.set(providerUri, oldSession); } , check: function (preq, opts) { opts = opts || {}; if (!preq.session) { return OAUTH3.PromiseA.resolve(null); } var freshness = OAUTH3.jwt.freshness(preq.session.token, opts.staletime); switch (freshness) { case 'stale': return OAUTH3.hooks.session.stale(preq.session); case 'expired': return OAUTH3.hooks.session.expired(preq.session).then(function (newSession) { preq.session = newSession; return newSession; }); //case 'fresh': default: return OAUTH3.PromiseA.resolve(preq.session); } } , stale: function (staleSession) { if (OAUTH3.hooks.session._stalePromise) { return OAUTH3.PromiseA.resolve(staleSession); } OAUTH3.hooks.session._stalePromise = OAUTH3._refreshToken( staleSession.provider_uri , { client_uri: staleSession.client_uri , session: staleSession , debug: staleSession.debug } ).then(function (newSession) { OAUTH3.hooks.session._stalePromise = null; return newSession; // oauth3.hooks.refreshSession(staleSession, newSession); }, function () { OAUTH3.hooks.session._stalePromise = null; }); return OAUTH3.PromiseA.resolve(staleSession); } , expired: function (expiredSession) { return OAUTH3._refreshToken( expiredSession.provider_uri , { client_uri: expiredSession.client_uri , session: expiredSession , debug: expiredSession.debug } ).then(function (newSession) { return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession); }); } , set: function (providerUri, newSession, id) { OAUTH3.hooks._checkStorage('sessions', 'set'); if (!providerUri) { console.error(new Error('no providerUri').stack); throw new Error("providerUri is not set"); } providerUri = OAUTH3.uri.normalize(providerUri); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.set(providerUri, newSession, id)); } , get: function (providerUri, id) { OAUTH3.hooks._checkStorage('sessions', 'get'); if (!providerUri) { throw new Error("providerUri is not set"); } providerUri = OAUTH3.uri.normalize(providerUri); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id)); } , all: function (providerUri) { OAUTH3.hooks._checkStorage('sessions', 'all'); if (providerUri) { providerUri = OAUTH3.uri.normalize(providerUri); } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.all(providerUri)); } , clear: function (providerUri) { OAUTH3.hooks._checkStorage('sessions', 'clear'); if (providerUri) { providerUri = OAUTH3.uri.normalize(providerUri); } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.clear(providerUri)); } } } , discoverScopes: function (providerUri, opts) { return OAUTH.scopes(providerUri, opts); } , scopes: function (providerUri, opts) { if (!providerUri) { throw new Error('oauth3.discoverScopes(providerUri, opts) received providerUri as :', providerUri); } opts = opts || {}; opts._pathname = ".well-known/oauth3/scopes.json"; //TODO: add caching return OAUTH3._rpcHelper(providerUri, opts).then(function(scopes) { return scopes; }); } , discover: function (providerUri, opts) { return OAUTH3.directives(providerUri, opts); } , directives: function (providerUri, opts) { if (!providerUri) { throw new Error('oauth3.discover(providerUri, opts) received providerUri as :', providerUri); } return OAUTH3.hooks.directives.get(providerUri).then(function (directives) { if (directives && directives.issuer) { return directives; } opts._pathname = ".well-known/oauth3/directives.json"; return OAUTH3._rpcHelper(providerUri, opts).then(function (directives) { directives.azp = directives.azp || OAUTH3.url.normalize(providerUri); directives.issuer = directives.issuer || OAUTH3.url.normalize(providerUri); directives.api = OAUTH3.url.normalize((directives.api||':hostname').replace(/:hostname/, OAUTH3.uri.normalize(directives.issuer) || OAUTH3.uri.normalize(providerUri))); // OAUTH3.PromiseA.resolve() is taken care of because this is wrapped return OAUTH3.hooks.directives.set(providerUri, directives); }); }); } , _rpcHelper: function(providerUri, opts) { return OAUTH3._browser.rpc(providerUri, opts); } , request: function (preq, opts) { function fetch() { if (preq.session) { // TODO check session.token.aud against preq.url to make sure they match //console.warn("[security] session audience checking has not been implemented yet (it's up to you to check)"); preq.headers = preq.headers || {}; preq.headers.Authorization = 'Bearer ' + (preq.session.access_token || preq.session.accessToken); } return OAUTH3._requestHelper(preq, opts); } if (!preq.session) { return fetch(); } return OAUTH3.hooks.session.check(preq, opts).then(fetch); } , _requestHelper: function (preq, opts) { /* if (opts && opts.directives) { preq.url = OAUTH3.url.resolve(opts.directives.issuer, preq.url); } */ return OAUTH3._browser.request(preq, opts); } , issuer: function (opts) { if (!opts) { opts = {}; } // TODO this will default to browserlogin.org var broker = opts.broker || 'https://new.oauth3.org'; //var broker = opts.broker || 'https://broker.oauth3.org'; opts._rpc = "broker"; opts._scheme = "localstorage:"; opts._pathname = "issuer"; return OAUTH3._rpcHelper(broker, opts).then(function(issuer) { return issuer; }); } , implicitGrant: function (directives, opts) { var promise; var providerUri = directives.azp || directives.issuer || directives; if (opts.broker) { // Discovery can happen in-flow because we know that this is // a valid oauth3 provider promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts); } else { // Discovery must take place before calling implicitGrant promise = OAUTH3.hooks.directives.get(providerUri).then(function (directives) { return OAUTH3._implicitGrant(directives, opts); }); } return promise.then(function (tokens) { // TODO abstract browser bits away try { OAUTH3._browser.closeFrame(tokens.state || opts._state, opts); } catch(e) { console.warn("[implicitGrant] TODO abstract browser bits away"); } opts._state = undefined; return OAUTH3.hooks.session.refresh( opts.session || { provider_uri: providerUri , client_id: opts.client_id , client_uri: opts.client_uri || opts.clientUri } , tokens ).then(function (session) { // TODO set cookie with JWT and TTL return OAUTH3.request({ method: 'POST' , url: OAUTH3.url.normalize( (directives.assets || 'https://assets.:hostname/assets/issuer@oauth3.org/session') .replace(/:hostname/, OAUTH3.uri.normalize(directives.issuer) || OAUTH3.uri.normalize(providerUri)) ) , session: session }).then(function () { return session; }, function (/*err*/) { return session; }); }); }); } , _discoverThenImplicitGrant: function(providerUri, opts) { opts.windowType = opts.windowType || 'popup'; return OAUTH3.discover(providerUri, opts).then(function (directives) { return OAUTH3._implicitGrant(directives, opts).then(function (tokens) { return tokens; }); }); } , _implicitGrant: function(directives, opts) { // TODO this may need to be synchronous for browser security policy // Do some stuff var authReq = OAUTH3.urls.implicitGrant( directives , { redirect_uri: opts.redirect_uri , client_id: opts.client_id || opts.client_uri , client_uri: opts.client_uri || opts.client_id , scope: opts.scope , subject: opts.subject , state: opts._state || undefined , debug: opts.debug } ); if (opts.debug) { window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!"); } return OAUTH3._browser.frameRequest( OAUTH3.url.resolve(directives.issuer, authReq.url) , authReq.state // state should recycle params , { windowType: opts.windowType , reuseWindow: opts.broker && '-broker' , debug: opts.debug } ).then(function (tokens) { if (tokens.error) { // TODO directives.audience return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, tokens)); } return tokens; }); } , _refreshToken: function (providerUri, opts) { return OAUTH3.discover(providerUri, opts).then(function (directives) { var prequest = OAUTH3.urls.refreshToken(directives, opts); prequest.url = OAUTH3.url.resolve(directives.api, prequest.url); return OAUTH3.request(prequest/*, { directives: directive }*/).then(function (req) { var data = req.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 ); }); }); } , logout: function(issuerUri, opts) { var directives; if ('string' !== typeof issuerUri) { directives = issuerUri; return OAUTH3._logoutHelper(directives, opts); } return OAUTH3.hooks.directives.get(issuerUri).then(function (directives) { return OAUTH3._logoutHelper(directives, opts); }); } , _logoutHelper: function(directives, opts) { var issuerUri = directives.issuer_uri || directives.provider_uri; var logoutReq = OAUTH3.urls.logout( directives , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)) , windowType: 'popup' // TODO: figure out background later , broker: opts.broker //, state: opts._state , debug: opts.debug } ); return OAUTH3._browser.frameRequest( OAUTH3.url.resolve(directives.issuer, logoutReq.url) , logoutReq.state // state should recycle params , { windowType: 'popup' , reuseWindow: opts.broker && '-broker' , debug: opts.debug } ).then(function (params) { OAUTH3._browser.closeFrame(params.state || opts._state, opts); if (params.error) { // TODO directives.audience return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*issuerUri*/, params)); } OAUTH3.hooks.session.clear(issuerUri); return params; }); } // // Let the Code Waste begin!! // , _browser: { window: 'undefined' !== typeof window ? window : null , rpc: function(providerUri, opts) { opts = opts || {}; providerUri = OAUTH3.url.normalize(providerUri); // TODO SECURITY should we whitelist our own self? if (OAUTH3.uri.normalize(providerUri).replace(/\/.*/, '') === OAUTH3.uri.normalize(OAUTH3._browser.window.location.hostname)) { console.warn("It looks like you're a provider trying to run rpc on yourself," + " so we we're just gonna use" + " OAUTH3.request({ method: 'GET', url: " + "'" + opts._pathname + "' })"); if (/localstorage/i.test(opts._scheme)) { return OAUTH3.PromiseA.resolve(localStorage.getItem(opts._pathname)); } else { return OAUTH3.request({ method: 'GET' , url: OAUTH3.url.normalize(providerUri) + '/' + opts._pathname // '/.well-known/oauth3/' + discoverFile }).then(function (resp) { return resp.data; }); } } if (!(opts.client_id || opts.client_uri || '').match(OAUTH3._browser.window.location.hostname)) { console.warn("It looks like your client_id doesn't match your current window..." + " this probably won't end well"); console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname); } var discReq = OAUTH3.urls[opts._rpc || 'rpc']( providerUri , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)) , windowType: opts.broker && opts.windowType || 'background' , broker: opts.broker , state: opts._state || undefined , debug: opts.debug , _scheme: opts._scheme , _pathname: opts._pathname , _method: opts._method } ); opts._state = discReq.state; //var discReq = OAUTH3.urls.rpc(providerUri, opts); // hmm... we're gonna need a broker for this since switching windows is distracting, // popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS // eventually it should be the browser (and postMessage may be a viable option now), but whatever... // TODO allow postMessage from providerUri in addition to callback // TODO allow node to open a desktop browser window opts._windowType = opts.windowType; opts.windowType = opts.windowType || 'background'; return OAUTH3._browser.testPixel(providerUri).then(function () { return OAUTH3._browser.frameRequest( OAUTH3.url.resolve(providerUri, discReq.url) , discReq.state // why not just pass opts whole? , { windowType: opts.windowType , reuseWindow: opts.broker && '-broker' , debug: opts.debug } ).then(function (params) { opts.windowType = opts._windowType; // caller will call OAUTH3._browser.closeFrame(discReq.state, { debug: opts.debug || params.debug }); if (params.error) { // TODO directives.issuer || directives.audience return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, params)); } // TODO params should have response_type indicating json, binary, etc var result; try { result = JSON.parse(OAUTH3._base64.decodeUrlSafe(params.data || params.result || params.directives)); } catch(e) { result = params.data || params.result; } console.log('result:', result); // caller will call OAUTH3.hooks.directives.set(providerUri, directives); return result; }); }); } , request: function (preq, _sys) { return new OAUTH3.PromiseA(function (resolve, reject) { var xhr; var headers = preq.headers || {}; var multipart; try { xhr = new XMLHttpRequest(_sys); } catch(e) { xhr = new XMLHttpRequest(); } xhr.onreadystatechange = function () { if (xhr.readyState !== XMLHttpRequest.DONE) { // nothing to do here return; } var data, err; if (xhr.status !== 200) { err = new Error('bad status code: ' + xhr.status); } try { data = JSON.parse(xhr.responseText); } catch(e) { data = xhr.responseText; } if (data.error) { err = new Error(data.error.message || data.error_description || JSON.stringify(data.error)); Object.assign(err, data.error); } if (err) { err._request = xhr; err.status = xhr.status; err.data = data; reject(err); return; } resolve({ _request: xhr , headers: null // TODO , data: data , status: xhr.status }); }; xhr.ontimeout = function () { var err = new Error('ETIMEDOUT'); err.code = 'ETIMEDOUT'; reject(err); }; if (preq.progress) { xhr.upload.onprogress = function (ev) { preq.progress({ loaded: ev.loaded , total: ev.total }); if (OAUTH3._digest) { // $rootScope.$digest(); OAUTH3._digest(); } }; } xhr.open(preq.method || 'GET', preq.url, true); // For assets.example.com/assets xhr.withCredentials = true; if (preq.timeout) { xhr.timeout = preq.timeout; } if (preq.data) { headers['Content-Type'] = 'application/json'; // TODO XXX TODO utf8 } Object.keys(headers).forEach(function (key) { xhr.setRequestHeader(key, headers[key]); }); if (preq.multipart && !(preq.multipart instanceof window.FormData)) { multipart = new window.FormData(); Object.keys(preq.multipart).forEach(function (key) { multipart.append(key, preq.multipart[key]); }); } else { multipart = preq.multipart; } if (multipart) { xhr.send(multipart); } else { xhr.send(JSON.stringify(preq.data)); } }); } , testPixel: function (targetUri) { var url = OAUTH3.url.resolve(OAUTH3.url.normalize(targetUri), '.well-known/oauth3/clear.gif'); return new OAUTH3.PromiseA(function (resolve, reject) { var img = document.createElement('img'); img.addEventListener('load', function () { resolve(); }); img.addEventListener('error', function () { var err = new Error("OAuth3 support not detected: '" + url + "' not found"); err.code = 'E_NOT_SUPPORTED'; reject(err); }); // works with CSP img.style.position = 'absolute'; img.style.left = '-2px'; img.style.bottom = '-2px'; img.className = 'js-oauth3-discover'; img.src = url; document.body.appendChild(img); console.log('img', img); }); } , frameRequest: function (url, state, opts) { opts = opts || {}; var previousFrame = OAUTH3._browser._frames[state]; var windowType = opts.windowType; if (!windowType) { windowType = 'popup'; } var timeout = opts.timeout; if ('background' === windowType) { if (!timeout) { timeout = 7 * 1000; } } return new OAUTH3.PromiseA(function (resolve, reject) { // TODO periodically garbage collect expired handlers from window object var tok; function cleanup() { delete window['--oauth3-callback-' + state]; clearTimeout(tok); tok = null; // the actual close is done later (by the caller) so that the window/frame // can be reused or self-closes synchronously itself / by parent // (probably won't ever happen, but that's a negotiable implementation detail) } window['--oauth3-callback-' + state] = function (params) { resolve(params); cleanup(); }; if (timeout) { tok = setTimeout(function () { var err = new Error( "the '" + windowType + "' request did not complete within " + Math.round(timeout / 1000) + "s" ); err.code = "E_TIMEOUT"; reject(err); cleanup(); }, timeout); } setTimeout(function () { if (!OAUTH3._browser._frames[state]) { reject(new Error("TODO: open the iframe first and discover oauth3 directives before popup")); cleanup(); } }, 0); if ('background' === windowType) { if (previousFrame) { previousFrame.location = url; //promise = previousFrame.promise; } else { OAUTH3._browser._frames[state] = OAUTH3._browser.iframe(url, state, opts); } } else if ('popup' === windowType) { if (previousFrame) { previousFrame.location = url; if (opts.debug) { previousFrame.focus(); } } else { OAUTH3._browser._frames[state] = OAUTH3._browser.frame(url, state, opts); } } else if ('inline' === windowType) { // callback function will never execute and would need to redirect back to current page // rather than the callback.html url += '&original_url=' + OAUTH3._browser.window.location.href; OAUTH3._browser.window.location = url; //promise = OAUTH3.PromiseA.resolve({ url: url }); return; } else { throw new Error("login framing method options.windowType=" + opts.windowType + " not type yet implemented"); } }).then(function (params) { if (params.error) { // TODO directives.issuer || directives.audience return OAUTH3.PromiseA.reject(OAUTH3.error.parse('https://oauth3.org', params)); } return params; }); } , closeFrame: function (state, opts) { opts = opts || {}; function close() { try { OAUTH3._browser._frames[state].close(); } catch(e) { try { OAUTH3._browser._frames[state].remove(); } catch(e) { console.error(new Error("Could not clase window/iframe. closeFrame may have been called twice.")); } } delete OAUTH3._browser._frames[state]; } if (opts.debug) { if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) { close(); } } else { close(); } } , _frames: {} , iframe: function (url, state, opts) { var framesrc = '