815 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			815 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| ;(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/org.oauth3.provider/access_token
 | |
|   //    { "grant_type": "password", "client_id": "<<id>>", "scope": "<<scope>>"
 | |
|   //    , "username": "<<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(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri))
 | |
|     .replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED')
 | |
|     ;
 | |
|   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.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.authn = {};
 | |
| OAUTH3.authn.loginMeta = function (directive, opts) {
 | |
|   return OAUTH3.request({
 | |
|     method: directive.credential_meta.method || 'GET'
 | |
|     // TODO lint urls
 | |
|     // TODO client_uri
 | |
|   , url: OAUTH3.url.resolve(directive.api, directive.credential_meta.url)
 | |
|       .replace(':type', 'email')
 | |
|       .replace(':id', opts.email)
 | |
|   });
 | |
| };
 | |
| OAUTH3.authn.otp = function (directive, opts) {
 | |
|   // TODO client_uri
 | |
|   var preq = {
 | |
|     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
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   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 || 'oauth3_authn';
 | |
|   if ('oauth3_authn' === scope) {
 | |
|     // 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 ('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.requests.accounts = {};
 | |
| OAUTH3.requests.accounts.update = function (directive, session, opts) {
 | |
|   var dir = directive.update_account || {
 | |
|     method: 'POST'
 | |
|   , url: OAUTH3.url.normalize(directive.api) + '/api/org.oauth3.provider/accounts/:accountId'
 | |
|   , bearer: 'Bearer'
 | |
|   };
 | |
|   var url = dir.url
 | |
|     .replace(/:accountId/, opts.accountId)
 | |
|   ;
 | |
| 
 | |
|   return OAUTH3.request({
 | |
|     method: dir.method || 'POST'
 | |
|   , url: url
 | |
|   , headers: {
 | |
|       'Authorization': (dir.bearer || 'Bearer') + ' ' + session.accessToken
 | |
|     }
 | |
|   , json: {
 | |
|       name: opts.name
 | |
|     , comment: opts.comment
 | |
|     , displayName: opts.displayName
 | |
|     , priority: opts.priority
 | |
|     }
 | |
|   });
 | |
| };
 | |
| OAUTH3.requests.accounts.create = function (directive, session, account) {
 | |
|   var dir = directive.create_account || {
 | |
|     method: 'POST'
 | |
|   , url: OAUTH3.url.normalize(directive.api) + '/api/org.oauth3.provider/accounts'
 | |
|   , bearer: 'Bearer'
 | |
|   };
 | |
|   var data = {
 | |
|     // TODO fix the server to just use one scheme
 | |
|     // account = { nick, self: { comment, username } }
 | |
|     // account = { name, comment, display_name, priority }
 | |
|     account: {
 | |
|       nick: account.display_name
 | |
|     , name: account.name
 | |
|     , comment: account.comment
 | |
|     , display_name: account.display_name
 | |
|     , priority: account.priority
 | |
|     , self: {
 | |
|         nick: account.display_name
 | |
|       , name: account.name
 | |
|       , comment: account.comment
 | |
|       , display_name: account.display_name
 | |
|       , priority: account.priority
 | |
|       }
 | |
|     }
 | |
|   , logins: [
 | |
|       {
 | |
|         token: session.access_token
 | |
|       }
 | |
|     ]
 | |
|   };
 | |
| 
 | |
|   return OAUTH3.request({
 | |
|     method: dir.method || 'POST'
 | |
|   , url: dir.url
 | |
|   , session: session
 | |
|   , data: data
 | |
|   });
 | |
| };
 | |
| 
 | |
| 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));
 |