2017-02-20 18:12:48 +00:00
|
|
|
;(function (exports) {
|
|
|
|
'use strict';
|
|
|
|
|
2017-02-28 01:19:01 +00:00
|
|
|
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
|
2017-02-21 18:49:41 +00:00
|
|
|
|
|
|
|
OAUTH3.url.parse = function (url) {
|
2017-02-20 22:22:48 +00:00
|
|
|
// TODO browser
|
|
|
|
// Node should replace this
|
|
|
|
var parser = document.createElement('a');
|
|
|
|
parser.href = url;
|
|
|
|
return parser;
|
|
|
|
};
|
2017-02-21 18:49:41 +00:00
|
|
|
OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) {
|
|
|
|
var src = OAUTH3.url.parse(referrerUrl);
|
|
|
|
var dst = OAUTH3.url.parse(redirectUrl);
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
// 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;
|
|
|
|
};
|
2017-02-21 18:49:41 +00:00
|
|
|
OAUTH3.url.checkRedirect = function (client, query) {
|
2017-02-20 22:22:48 +00:00
|
|
|
console.warn("[security] URL path checking not yet implemented");
|
2017-11-09 22:25:19 +00:00
|
|
|
if (!query) {
|
|
|
|
query = client;
|
|
|
|
client = query.client_uri;
|
|
|
|
}
|
|
|
|
client = client.url || client;
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
// 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);
|
2017-02-21 18:49:41 +00:00
|
|
|
var redirectUrl = OAUTH3.url.normalize(query.redirect_uri);
|
2017-02-20 22:22:48 +00:00
|
|
|
|
|
|
|
// 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)
|
2017-02-21 18:49:41 +00:00
|
|
|
if (OAUTH3.url._isRedirectHostSafe(clientUrl, redirectUrl)) {
|
2017-02-20 22:22:48 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
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;
|
2017-02-20 22:22:48 +00:00
|
|
|
return false;
|
|
|
|
};
|
2017-02-21 18:49:41 +00:00
|
|
|
OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) {
|
2017-02-20 22:22:48 +00:00
|
|
|
// 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
|
2017-02-20 18:12:48 +00:00
|
|
|
};
|
2017-02-20 22:22:48 +00:00
|
|
|
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;
|
|
|
|
}
|
2017-02-21 18:49:41 +00:00
|
|
|
var redirect = clientParams.redirect_uri + '#' + window.OAUTH3.query.stringify(authz);
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
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");
|
|
|
|
}
|
2017-02-21 18:49:41 +00:00
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
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)
|
|
|
|
//
|
2017-11-09 22:25:19 +00:00
|
|
|
// POST https://example.com/api/issuer@oauth3.org/access_token
|
2017-02-20 22:22:48 +00:00
|
|
|
// { "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;
|
2017-02-20 18:12:48 +00:00
|
|
|
}
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
var args = directive.access_token;
|
2017-02-20 22:22:48 +00:00
|
|
|
var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined;
|
2017-03-21 07:02:41 +00:00
|
|
|
// TODO require user agent
|
2017-02-20 22:22:48 +00:00
|
|
|
var params = {
|
2017-02-24 20:05:07 +00:00
|
|
|
client_id: opts.client_id || opts.client_uri
|
|
|
|
, client_uri: opts.client_uri
|
2017-11-09 22:25:19 +00:00
|
|
|
, grant_type: 'password'
|
2017-02-24 20:05:07 +00:00
|
|
|
, 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
|
2017-02-20 22:22:48 +00:00
|
|
|
//, "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
|
|
|
|
};
|
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
if (opts.client_uri) {
|
|
|
|
params.clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos;
|
|
|
|
if (!params.clientAgreeTos) {
|
2017-02-20 22:22:48 +00:00
|
|
|
throw new Error('Developer Error: missing clientAgreeTos uri');
|
2017-02-20 18:12:48 +00:00
|
|
|
}
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
var scope = opts.scope || directive.authn_scope;
|
2017-02-20 22:22:48 +00:00
|
|
|
if (scope) {
|
2017-02-21 18:49:41 +00:00
|
|
|
params.scope = OAUTH3.scope.stringify(scope);
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
var uri = args.url;
|
|
|
|
var body;
|
2017-02-20 22:22:48 +00:00
|
|
|
if ('GET' === args.method.toUpperCase()) {
|
2017-02-21 18:49:41 +00:00
|
|
|
uri += '?' + OAUTH3.query.stringify(params);
|
2017-02-20 22:22:48 +00:00
|
|
|
} else {
|
|
|
|
body = params;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2017-05-24 01:00:07 +00:00
|
|
|
url: OAUTH3.url.resolve(directive.api, uri)
|
2017-02-20 22:22:48 +00:00
|
|
|
, method: args.method
|
|
|
|
, data: body
|
|
|
|
};
|
|
|
|
};
|
|
|
|
OAUTH3.urls.grants = function (directive, opts) {
|
|
|
|
// directive = { issuer, authorization_decision }
|
|
|
|
// opts = { response_type, scopes{ granted, requested, pending, accepted } }
|
2017-11-09 22:25:19 +00:00
|
|
|
var grantsDir = directive.grants;
|
|
|
|
if (!grantsDir) {
|
|
|
|
throw new Error("provider doesn't support grants");
|
|
|
|
}
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
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) {
|
2017-11-09 22:25:19 +00:00
|
|
|
console.warn("You should supply options.method as either 'GET', or 'POST'");
|
|
|
|
opts.method = grantsDir.method || 'GET';
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
|
|
|
if ('POST' === opts.method) {
|
|
|
|
if ('string' !== typeof opts.scope) {
|
2017-11-09 22:25:19 +00:00
|
|
|
throw new Error("You must supply options.scope as a comma-delimited string of scopes");
|
2017-02-20 18:12:48 +00:00
|
|
|
}
|
2017-11-09 22:25:19 +00:00
|
|
|
if ('string' !== typeof opts.sub) {
|
|
|
|
console.log("provide 'sub' to urls.grants to specify the PPID for the client");
|
2017-02-20 18:12:48 +00:00
|
|
|
}
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
var url = OAUTH3.url.resolve(directive.api, grantsDir.url)
|
2017-03-23 00:13:06 +00:00
|
|
|
.replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED')
|
2017-11-16 05:30:27 +00:00
|
|
|
.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
|
2017-02-20 22:22:48 +00:00
|
|
|
;
|
|
|
|
var data = {
|
|
|
|
client_id: opts.client_id
|
|
|
|
, client_uri: opts.client_uri
|
|
|
|
, referrer: opts.referrer
|
|
|
|
, scope: opts.scope
|
2017-11-09 22:25:19 +00:00
|
|
|
, sub: opts.sub
|
2017-02-20 22:22:48 +00:00
|
|
|
};
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
var body;
|
2017-02-20 22:22:48 +00:00
|
|
|
if ('GET' === opts.method) {
|
2017-02-21 18:49:41 +00:00
|
|
|
url += '?' + OAUTH3.query.stringify(data);
|
2017-11-09 22:25:19 +00:00
|
|
|
} else {
|
2017-02-20 22:22:48 +00:00
|
|
|
body = data;
|
|
|
|
}
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
return {
|
|
|
|
method: opts.method
|
|
|
|
, url: url
|
|
|
|
, data: body
|
|
|
|
, session: opts.session
|
2017-02-20 18:12:48 +00:00
|
|
|
};
|
2017-02-20 22:22:48 +00:00
|
|
|
};
|
2017-11-09 22:25:19 +00:00
|
|
|
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
|
|
|
|
};
|
|
|
|
};
|
2017-11-14 23:39:14 +00:00
|
|
|
OAUTH3.urls.credentialMeta = function (directive, opts) {
|
|
|
|
return OAUTH3.url.resolve(directive.api, directive.credential_meta.url)
|
|
|
|
.replace(':type', 'email')
|
|
|
|
.replace(':id', opts.email)
|
|
|
|
};
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
OAUTH3.authn = {};
|
2017-02-21 19:15:58 +00:00
|
|
|
OAUTH3.authn.loginMeta = function (directive, opts) {
|
2017-11-14 23:39:14 +00:00
|
|
|
var url = OAUTH3.urls.credentialMeta(directive, opts);
|
2017-02-20 22:22:48 +00:00
|
|
|
return OAUTH3.request({
|
|
|
|
method: directive.credential_meta.method || 'GET'
|
|
|
|
// TODO lint urls
|
2017-03-21 07:02:41 +00:00
|
|
|
// TODO client_uri
|
2017-11-14 23:39:14 +00:00
|
|
|
, url: url
|
2017-02-20 22:22:48 +00:00
|
|
|
});
|
|
|
|
};
|
2017-11-14 23:39:14 +00:00
|
|
|
OAUTH3.urls.otp = function (directive, opts) {
|
2017-03-21 07:02:41 +00:00
|
|
|
// TODO client_uri
|
2017-11-14 23:39:14 +00:00
|
|
|
return {
|
2017-02-22 01:38:45 +00:00
|
|
|
method: directive.credential_otp.method || 'POST'
|
2017-05-24 01:00:07 +00:00
|
|
|
, url: OAUTH3.url.resolve(directive.api, directive.credential_otp.url)
|
2017-02-20 22:22:48 +00:00
|
|
|
, data: {
|
|
|
|
// TODO replace with signed hosted file
|
|
|
|
client_agree_tos: 'oauth3.org/tos/draft'
|
2017-03-21 07:02:41 +00:00
|
|
|
// 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)
|
2017-02-20 22:22:48 +00:00
|
|
|
, request_otp: true
|
|
|
|
, username: opts.email
|
|
|
|
}
|
2017-02-22 01:38:45 +00:00
|
|
|
};
|
2017-11-14 23:39:14 +00:00
|
|
|
};
|
|
|
|
OAUTH3.authn.otp = function (directive, opts) {
|
|
|
|
var preq = OAUTH3.urls.otp(directive, opts);
|
2017-02-24 19:18:45 +00:00
|
|
|
|
2017-02-22 01:38:45 +00:00
|
|
|
return OAUTH3.request(preq);
|
2017-02-20 22:22:48 +00:00
|
|
|
};
|
2017-02-21 19:15:58 +00:00
|
|
|
OAUTH3.authn.resourceOwnerPassword = function (directive, opts) {
|
2017-02-20 22:22:48 +00:00
|
|
|
var providerUri = directive.issuer;
|
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
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");
|
2017-02-20 18:12:48 +00:00
|
|
|
}
|
2017-02-24 19:18:45 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
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;
|
2017-02-20 18:12:48 +00:00
|
|
|
});
|
2017-02-20 22:22:48 +00:00
|
|
|
});
|
|
|
|
};
|
2017-02-21 18:49:41 +00:00
|
|
|
|
|
|
|
OAUTH3.authz = {};
|
2017-02-20 22:22:48 +00:00
|
|
|
OAUTH3.authz.scopes = function (providerUri, session, clientParams) {
|
2017-02-21 18:49:41 +00:00
|
|
|
var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer);
|
2017-11-09 22:25:19 +00:00
|
|
|
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);
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
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;
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
2017-02-20 18:12:48 +00:00
|
|
|
}
|
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
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;
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
2017-11-09 22:25:19 +00:00
|
|
|
return [];
|
2017-02-20 22:22:48 +00:00
|
|
|
});
|
2017-11-09 22:25:19 +00:00
|
|
|
}).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);
|
|
|
|
}
|
2017-02-20 18:12:48 +00:00
|
|
|
});
|
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
return {
|
2017-11-09 22:25:19 +00:00
|
|
|
requested: requested // all requested, now
|
|
|
|
, granted: granted // all granted, ever
|
|
|
|
, accepted: accepted // intersection of granted (ever) and requested (now)
|
|
|
|
, pending: pending // not yet accepted
|
2017-02-20 18:12:48 +00:00
|
|
|
};
|
2017-02-20 22:22:48 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
OAUTH3.authz.grants = function (providerUri, opts) {
|
|
|
|
return OAUTH3.discover(providerUri, {
|
|
|
|
client_id: providerUri
|
|
|
|
, debug: opts.debug
|
|
|
|
}).then(function (directive) {
|
2017-11-09 22:25:19 +00:00
|
|
|
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.
|
2017-11-16 05:30:27 +00:00
|
|
|
if (opts.all || ('GET' !== opts.method && 'POST' !== opts.method)) {
|
2017-11-09 22:25:19 +00:00
|
|
|
return grants;
|
|
|
|
}
|
2017-02-20 18:12:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
OAUTH3.hooks.grants.set(grants.sub, grants.azp, grants);
|
|
|
|
return {
|
|
|
|
client: grants.azp
|
|
|
|
, clientSub: grants.azpSub
|
|
|
|
, grants: OAUTH3.scope.parse(grants.scope)
|
|
|
|
};
|
2017-02-20 22:22:48 +00:00
|
|
|
});
|
|
|
|
};
|
2017-11-09 22:25:19 +00:00
|
|
|
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);
|
|
|
|
}
|
2017-02-20 22:22:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
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);
|
|
|
|
}
|
2017-02-20 22:22:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
var prom;
|
|
|
|
if (scopes.new) {
|
|
|
|
prom = OAUTH3.authz.grants(providerUri, {
|
|
|
|
session: session
|
|
|
|
, method: 'POST'
|
2017-02-20 22:22:48 +00:00
|
|
|
, client_id: clientParams.client_uri
|
|
|
|
, referrer: clientParams.referrer
|
2017-11-09 22:25:19 +00:00
|
|
|
, scope: scopes.accepted.concat(scopes.new).join(',')
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
prom = OAUTH3.PromiseA.resolve();
|
|
|
|
}
|
2017-02-20 22:22:48 +00:00
|
|
|
|
2017-11-09 22:25:19 +00:00
|
|
|
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;
|
2017-03-16 21:23:19 +00:00
|
|
|
});
|
2017-11-09 22:25:19 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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'
|
|
|
|
};
|
|
|
|
});
|
2017-02-20 22:22:48 +00:00
|
|
|
});
|
2017-11-09 22:25:19 +00:00
|
|
|
}).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});
|
|
|
|
});
|
2017-02-20 22:22:48 +00:00
|
|
|
};
|
2017-11-09 22:25:19 +00:00
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
OAUTH3.requests = {};
|
|
|
|
OAUTH3.requests.accounts = {};
|
|
|
|
OAUTH3.requests.accounts.update = function (directive, session, opts) {
|
|
|
|
var dir = directive.update_account || {
|
|
|
|
method: 'POST'
|
2017-08-02 02:30:43 +00:00
|
|
|
, url: OAUTH3.url.normalize(directive.api) + '/api/issuer@oauth3.org/accounts/:accountId'
|
2017-02-20 22:22:48 +00:00
|
|
|
, 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'
|
2017-08-02 02:30:43 +00:00
|
|
|
, url: OAUTH3.url.normalize(directive.api) + '/api/issuer@oauth3.org/accounts'
|
2017-02-20 22:22:48 +00:00
|
|
|
, 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: {
|
2017-02-20 18:12:48 +00:00
|
|
|
nick: account.display_name
|
|
|
|
, name: account.name
|
|
|
|
, comment: account.comment
|
|
|
|
, display_name: account.display_name
|
|
|
|
, priority: account.priority
|
|
|
|
}
|
2017-02-20 22:22:48 +00:00
|
|
|
}
|
|
|
|
, logins: [
|
|
|
|
{
|
|
|
|
token: session.access_token
|
|
|
|
}
|
|
|
|
]
|
2017-02-20 18:12:48 +00:00
|
|
|
};
|
|
|
|
|
2017-02-20 22:22:48 +00:00
|
|
|
return OAUTH3.request({
|
|
|
|
method: dir.method || 'POST'
|
|
|
|
, url: dir.url
|
|
|
|
, session: session
|
|
|
|
, data: data
|
|
|
|
});
|
|
|
|
};
|
2017-11-09 22:25:19 +00:00
|
|
|
|
2017-02-22 01:38:45 +00:00
|
|
|
OAUTH3.hooks.grants = {
|
2017-11-09 22:25:19 +00:00
|
|
|
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();
|
2017-02-22 01:38:45 +00:00
|
|
|
}
|
|
|
|
};
|
2017-02-20 22:22:48 +00:00
|
|
|
|
|
|
|
OAUTH3._browser.isIframe = function isIframe () {
|
|
|
|
try {
|
|
|
|
return window.self !== window.top;
|
|
|
|
} catch (e) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-02-21 18:49:41 +00:00
|
|
|
}('undefined' !== typeof exports ? exports : window));
|