oauth3.js/oauth3.issuer.js

529 lines
16 KiB
JavaScript
Raw Normal View History

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;
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;
};
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;
};
OAUTH3.url.checkRedirect = function (client, query) {
2017-02-20 22:22:48 +00:00
console.warn("[security] URL path checking not yet implemented");
2017-07-26 22:27:03 +00:00
if (!query) {
query = client;
client = query.client_uri;
}
client = client.url || client;
2017-02-20 18:12:48 +00:00
2017-07-26 22:27:03 +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);
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)
if (OAUTH3.url._isRedirectHostSafe(clientUrl, redirectUrl)) {
2017-02-20 22:22:48 +00:00
return true;
}
2017-07-26 22:27:03 +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;
};
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;
}
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-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)
//
// 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;
2017-02-20 18:12:48 +00:00
}
2017-02-20 22:22:48 +00:00
}
2017-02-20 18:12:48 +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
, 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
};
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
}
var scope = opts.scope || directive.authn_scope;
2017-02-20 22:22:48 +00:00
if (scope) {
params.scope = OAUTH3.scope.stringify(scope);
2017-02-20 22:22:48 +00:00
}
var uri = args.url;
var body;
2017-02-20 22:22:48 +00:00
if ('GET' === args.method.toUpperCase()) {
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 } }
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-07-26 22:27:03 +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-07-26 22:27:03 +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
}
if ('string' !== typeof opts.sub) {
console.log("provide 'sub' to urls.grants to specify the PPID for the client");
}
2017-02-20 22:22:48 +00:00
}
2017-02-20 18:12:48 +00:00
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')
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
, sub: opts.sub
2017-02-20 22:22:48 +00:00
};
2017-02-20 18:12:48 +00:00
2017-07-26 22:27:03 +00:00
var body;
2017-02-20 22:22:48 +00:00
if ('GET' === opts.method) {
url += '?' + OAUTH3.query.stringify(data);
} 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
};
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
};
};
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-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-05-24 01:00:07 +00:00
, url: OAUTH3.url.resolve(directive.api, directive.credential_meta.url)
2017-02-20 22:22:48 +00:00
.replace(':type', 'email')
.replace(':id', opts.email)
});
};
2017-02-21 19:15:58 +00:00
OAUTH3.authn.otp = function (directive, opts) {
2017-03-21 07:02:41 +00:00
// TODO client_uri
2017-02-22 01:38:45 +00:00
var preq = {
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-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;
//var scope = opts.scope;
//var appId = opts.appId;
return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts);
2017-03-21 07:02:41 +00:00
// TODO return not the raw request?
2017-02-20 22:22:48 +00:00
return OAUTH3.request(prequest).then(function (req) {
var data = req.data;
data.provider_uri = providerUri;
if (data.error) {
2017-02-23 00:58:49 +00:00
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data));
2017-02-20 18:12:48 +00:00
}
2017-02-24 19:18:45 +00:00
2017-02-22 01:38:45 +00:00
return OAUTH3.hooks.session.refresh(
2017-02-20 22:22:48 +00:00
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
, data
);
2017-02-20 18:12:48 +00:00
});
2017-02-20 22:22:48 +00:00
});
};
OAUTH3.authz = {};
2017-02-20 22:22:48 +00:00
OAUTH3.authz.scopes = function (providerUri, session, clientParams) {
var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer);
2017-07-26 22:27:03 +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-02-20 22:22:48 +00:00
return OAUTH3.authz.grants(providerUri, {
method: 'GET'
, client_id: clientUri
, client_uri: clientUri
, session: session
2017-07-26 22:27:03 +00:00
}).then(function (results) {
return results.grants;
}, function (err) {
if (!/no .*grants .*found/i.test(err.message)) {
console.error(err);
2017-02-20 22:22:48 +00:00
}
2017-07-26 22:27:03 +00:00
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);
2017-02-20 22:22:48 +00:00
}
});
2017-02-20 18:12:48 +00:00
2017-02-20 22:22:48 +00:00
return {
2017-07-26 22:27:03 +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-07-26 22:27:03 +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));
}
if ('POST' === opts.method) {
return grants;
}
2017-02-20 18:12:48 +00:00
2017-07-26 22:27:03 +00:00
OAUTH3.hooks.grants.set(grants.sub+'/'+grants.azp, grants.scope);
return {
client: grants.azp
, grants: OAUTH3.scope.parse(grants.scope)
};
2017-02-20 22:22:48 +00:00
});
};
OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) {
2017-07-26 22:27:03 +00:00
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
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
, scope: scopes.accepted.concat(scopes.new).join(',')
2017-02-20 22:22:48 +00:00
});
} else {
prom = OAUTH3.PromiseA.resolve();
2017-02-20 22:22:48 +00:00
}
return prom.then(function () {
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 (results) {
// TODO limit refresh token to an expirable token
// TODO inform client not to persist token
OAUTH3.url.redirect(clientParams, scopes, results.originalData || results.data);
});
2017-02-20 22:22:48 +00:00
};
2017-07-26 22:27:03 +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-05-24 08:05:23 +00:00
, url: OAUTH3.url.normalize(directive.api) + '/api/org.oauth3.provider/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-05-24 08:05:23 +00:00
, url: OAUTH3.url.normalize(directive.api) + '/api/org.oauth3.provider/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-02-22 01:38:45 +00:00
OAUTH3.hooks.grants = {
// Provider Only
set: function (clientUri, newGrants) {
clientUri = OAUTH3.uri.normalize(clientUri);
console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault');
console.warn(newGrants);
if (!this._cache) { this._cache = {}; }
console.log('clientUri, newGrants');
console.log(clientUri, newGrants);
this._cache[clientUri] = newGrants;
return newGrants;
}
, get: function (clientUri) {
clientUri = OAUTH3.uri.normalize(clientUri);
console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault');
if (!this._cache) { this._cache = {}; }
console.log('clientUri, existingGrants');
console.log(clientUri, this._cache[clientUri]);
return this._cache[clientUri];
}
};
2017-02-20 22:22:48 +00:00
OAUTH3._browser.isIframe = function isIframe () {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
};
}('undefined' !== typeof exports ? exports : window));