complete redirect flow
This commit is contained in:
parent
c2370d9b76
commit
bb6bcd826e
|
@ -7,11 +7,13 @@
|
||||||
clientUri: function (location) {
|
clientUri: function (location) {
|
||||||
return OAUTH3.utils.uri.normalize(location.host + location.pathname);
|
return OAUTH3.utils.uri.normalize(location.host + location.pathname);
|
||||||
}
|
}
|
||||||
, _formatError: function (providerUri, params) {
|
, _error: {
|
||||||
var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'");
|
parse: function (providerUri, params) {
|
||||||
err.uri = params.error_uri || params.error.uri;
|
var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'");
|
||||||
err.code = params.error.code || params.error;
|
err.uri = params.error_uri || params.error.uri;
|
||||||
return err;
|
err.code = params.error.code || params.error;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, atob: function (base64) {
|
, atob: function (base64) {
|
||||||
return (exports.atob || require('atob'))(base64);
|
return (exports.atob || require('atob'))(base64);
|
||||||
|
@ -130,7 +132,7 @@
|
||||||
// 'abc.qrs.xyz'
|
// 'abc.qrs.xyz'
|
||||||
// [ 'abc', 'qrs', 'xyz' ]
|
// [ 'abc', 'qrs', 'xyz' ]
|
||||||
// [ {}, {}, 'foo' ]
|
// [ {}, {}, 'foo' ]
|
||||||
// { header: {}, payload: {}, signature: }
|
// { header: {}, payload: {}, signature: '' }
|
||||||
var parts = str.split(/\./g);
|
var parts = str.split(/\./g);
|
||||||
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
|
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
|
||||||
var atob = exports.atob || require('atob');
|
var atob = exports.atob || require('atob');
|
||||||
|
@ -676,6 +678,8 @@
|
||||||
return OAUTH3.request({
|
return OAUTH3.request({
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
, url: OAUTH3.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json'
|
, url: OAUTH3.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json'
|
||||||
|
}).then(function (resp) {
|
||||||
|
return resp.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -913,8 +917,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
OAUTH3.utils._formatError = OAUTH3.utils._error.parse;
|
||||||
|
|
||||||
if ('undefined' !== typeof Promise) {
|
if ('undefined' !== typeof Promise) {
|
||||||
OAUTH3.PromiseA = Promise;
|
OAUTH3.PromiseA = Promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
}('undefined' !== typeof exports ? exports : window));
|
}('undefined' !== typeof exports ? exports : window));
|
764
oauth3.issuer.js
764
oauth3.issuer.js
|
@ -3,285 +3,549 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
OAUTH3.utils.query.parse = function (search) {
|
OAUTH3.utils.query.parse = function (search) {
|
||||||
// parse a query or a hash
|
// parse a query or a hash
|
||||||
if (-1 !== ['#', '?'].indexOf(search[0])) {
|
if (-1 !== ['#', '?'].indexOf(search[0])) {
|
||||||
search = search.substring(1);
|
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;
|
||||||
}
|
}
|
||||||
// Solve for case of search within hash
|
else {
|
||||||
// example: #/authorization_dialog/?state=...&redirect_uri=...
|
kvp = arg.split('=');
|
||||||
var queryIndex = search.indexOf('?');
|
key = decodeURIComponent(kvp[0]).trim();
|
||||||
if (-1 !== queryIndex) {
|
value = decodeURIComponent(kvp[1]).trim();
|
||||||
search = search.substr(queryIndex + 1);
|
argsParsed[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return argsParsed;
|
||||||
|
};
|
||||||
|
OAUTH3.utils.scope.parse = function (scope) {
|
||||||
|
return (scope||'').split(/[, ]/g);
|
||||||
|
};
|
||||||
|
OAUTH3.utils.url.parse = function (url) {
|
||||||
|
// TODO browser
|
||||||
|
// Node should replace this
|
||||||
|
var parser = document.createElement('a');
|
||||||
|
parser.href = url;
|
||||||
|
return parser;
|
||||||
|
};
|
||||||
|
OAUTH3.utils.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) {
|
||||||
|
var src = OAUTH3.utils.url.parse(referrerUrl);
|
||||||
|
var dst = OAUTH3.utils.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.utils.url.checkRedirect = function (client, query) {
|
||||||
|
console.warn("[security] URL path checking not yet implemented");
|
||||||
|
|
||||||
|
var clientUrl = OAUTH3.utils.url.normalize(client.url);
|
||||||
|
var redirectUrl = OAUTH3.utils.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.utils.url._isRedirectHostSafe(clientUrl, redirectUrl)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
OAUTH3.utils.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.utils.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 || {};
|
||||||
|
var type = 'access_token';
|
||||||
|
var grantType = 'password';
|
||||||
|
|
||||||
|
if (!opts.password) {
|
||||||
|
if (opts.otp) {
|
||||||
|
// for backwards compat
|
||||||
|
opts.password = opts.otp; // 'otp:' + opts.otpUuid + ':' + opts.otp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scope = opts.scope || directive.authn_scope;
|
||||||
|
var clientId = opts.appId || opts.clientId || opts.client_id;
|
||||||
|
var clientAgreeTos = opts.clientAgreeTos || opts.client_agree_tos;
|
||||||
|
var clientUri = opts.clientUri || opts.client_uri || opts.clientUrl || opts.client_url;
|
||||||
|
var args = directive[type];
|
||||||
|
var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined;
|
||||||
|
var params = {
|
||||||
|
"grant_type": grantType
|
||||||
|
, "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
|
||||||
|
};
|
||||||
|
var uri = args.url;
|
||||||
|
var body;
|
||||||
|
if (opts.totp) {
|
||||||
|
params.totp = opts.totp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId) {
|
||||||
|
params.clientId = clientId;
|
||||||
|
}
|
||||||
|
if (clientUri) {
|
||||||
|
params.clientUri = clientUri;
|
||||||
|
params.clientAgreeTos = clientAgreeTos;
|
||||||
|
if (!clientAgreeTos) {
|
||||||
|
throw new Error('Developer Error: missing clientAgreeTos uri');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope) {
|
||||||
|
params.scope = OAUTH3.utils.scope.stringify(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('GET' === args.method.toUpperCase()) {
|
||||||
|
uri += '?' + OAUTH3.utils.query.stringify(params);
|
||||||
|
} else {
|
||||||
|
body = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: uri
|
||||||
|
, method: args.method
|
||||||
|
, data: body
|
||||||
|
};
|
||||||
|
};
|
||||||
|
OAUTH3.urls.grants = function (directive, opts) {
|
||||||
|
// directive = { issuer, authorization_decision }
|
||||||
|
// opts = { response_type, scopes{ granted, requested, pending, accepted } }
|
||||||
|
|
||||||
|
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 must supply options.method as either 'GET', or 'POST'");
|
||||||
|
}
|
||||||
|
if ('POST' === opts.method) {
|
||||||
|
if ('string' !== typeof opts.scope) {
|
||||||
|
console.warn("You should supply options.scope as a space-delimited string of scopes");
|
||||||
|
}
|
||||||
|
if (-1 === ['token', 'code'].indexOf(opts.response_type)) {
|
||||||
|
throw new Error("You must supply options.response_type as 'token' or 'code'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = OAUTH3.utils.url.resolve(directive.issuer, directive.grants.url)
|
||||||
|
.replace(/(:azp|:client_id)/g, OAUTH3.utils.uri.normalize(opts.client_id || opts.client_uri))
|
||||||
|
.replace(/(:sub|:account_id)/g, opts.session.token.sub)
|
||||||
|
;
|
||||||
|
var data = {
|
||||||
|
client_id: opts.client_id
|
||||||
|
, client_uri: opts.client_uri
|
||||||
|
, referrer: opts.referrer
|
||||||
|
, response_type: opts.response_type
|
||||||
|
, scope: opts.scope
|
||||||
|
, tenant_id: opts.tenant_id
|
||||||
|
};
|
||||||
|
var body;
|
||||||
|
|
||||||
|
if ('GET' === opts.method) {
|
||||||
|
url += '?' + OAUTH3.utils.query.stringify(data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
body = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: opts.method
|
||||||
|
, url: url
|
||||||
|
, data: body
|
||||||
|
, session: opts.session
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.authn = {};
|
||||||
|
|
||||||
|
OAUTH3.authz = {};
|
||||||
|
OAUTH3.authz.loginMeta = function (directive, opts) {
|
||||||
|
if (opts.mock) {
|
||||||
|
if (opts.mockError) {
|
||||||
|
return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}});
|
||||||
|
}
|
||||||
|
return OAUTH3.PromiseA.resolve({data: {}});
|
||||||
|
}
|
||||||
|
|
||||||
|
return OAUTH3.request({
|
||||||
|
method: directive.credential_meta.method || 'GET'
|
||||||
|
// TODO lint urls
|
||||||
|
, url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_meta.url)
|
||||||
|
.replace(':type', 'email')
|
||||||
|
.replace(':id', opts.email)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.authz.otp = function (directive, opts) {
|
||||||
|
if (opts.mock) {
|
||||||
|
if (opts.mockError) {
|
||||||
|
return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}});
|
||||||
|
}
|
||||||
|
return OAUTH3.PromiseA.resolve({data: {uuid: "uuidblah"}});
|
||||||
|
}
|
||||||
|
|
||||||
|
return OAUTH3.request({
|
||||||
|
method: directive.credential_otp.url.method || 'POST'
|
||||||
|
, url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_otp.url)
|
||||||
|
, data: {
|
||||||
|
// TODO replace with signed hosted file
|
||||||
|
client_agree_tos: 'oauth3.org/tos/draft'
|
||||||
|
, client_id: directive.issuer // In this case, the issuer is its own client
|
||||||
|
, client_uri: directive.issuer
|
||||||
|
, request_otp: true
|
||||||
|
, username: opts.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
|
||||||
|
console.log('ginger bread man');
|
||||||
|
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);
|
||||||
|
|
||||||
|
return OAUTH3.request(prequest).then(function (req) {
|
||||||
|
var data = req.data;
|
||||||
|
data.provider_uri = providerUri;
|
||||||
|
if (data.error) {
|
||||||
|
return oauth3.PromiseA.reject(OAUTH3.utils._error.parse(providerUri, data.error));
|
||||||
|
}
|
||||||
|
return OAUTH3.hooks.refreshSession(
|
||||||
|
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
|
||||||
|
, data
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
OAUTH3.authz.scopes = function (providerUri, session, clientParams) {
|
||||||
|
if (clientParams.mock) {
|
||||||
|
return {
|
||||||
|
pending: ['oauth3_authn'] // not yet accepted
|
||||||
|
, granted: [] // all granted, ever
|
||||||
|
, requested: ['oauth3_authn'] // all requested, now
|
||||||
|
, accepted: [] // granted (ever) and requested (now)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth3.requests.grants(providerUri, {}); // return list of grants
|
||||||
|
// OAuth3.checkGrants(providerUri, {}); //
|
||||||
|
var clientUri = OAUTH3.utils.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer);
|
||||||
|
var scope = clientParams.scope || '';
|
||||||
|
var clientObj = clientParams;
|
||||||
|
|
||||||
|
if (!scope) {
|
||||||
|
scope = 'oauth3_authn';
|
||||||
|
}
|
||||||
|
|
||||||
|
//$('.js-user-avatar').attr('src', userAvatar);
|
||||||
|
|
||||||
|
/*
|
||||||
|
console.log('grants options');
|
||||||
|
console.log(loc.hash);
|
||||||
|
console.log(loc.search);
|
||||||
|
console.log(clientObj);
|
||||||
|
console.log(session.token);
|
||||||
|
console.log(window.document.referrer);
|
||||||
|
*/
|
||||||
|
|
||||||
|
return OAUTH3.authz.grants(providerUri, {
|
||||||
|
method: 'GET'
|
||||||
|
, client_id: clientUri
|
||||||
|
, client_uri: clientUri
|
||||||
|
, session: session
|
||||||
|
}).then(function (grantResults) {
|
||||||
|
var grants;
|
||||||
|
var grantedScopes;
|
||||||
|
var grantedScopesMap;
|
||||||
|
var pendingScopes;
|
||||||
|
var acceptedScopes;
|
||||||
|
var scopes = scope.split(/[+, ]/g);
|
||||||
|
var callbackUrl;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if (!OAUTH3.utils.url.checkRedirect(grantResults.client, clientObj)) {
|
||||||
|
callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK'
|
||||||
|
+ '?redirect_uri=' + clientObj.redirect_uri
|
||||||
|
+ '&allowed_urls=' + grantResults.client.url
|
||||||
|
+ '&client_id=' + clientUri
|
||||||
|
+ '&referrer_uri=' + OAUTH3.utils.uri.normalize(window.document.referrer)
|
||||||
|
;
|
||||||
|
if (opts.debug) {
|
||||||
|
console.log('grantResults Redirect Attack');
|
||||||
|
console.log(grantResults);
|
||||||
|
console.log(clientObj);
|
||||||
|
window.alert("DEBUG MODE checkRedirect error encountered. Check the console.");
|
||||||
|
}
|
||||||
|
location.href = callbackUrl;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var args = search.split('&');
|
if ('oauth3_authn' === scope) {
|
||||||
var argsParsed = {};
|
// implicit ppid grant is automatic
|
||||||
var i, arg, kvp, key, value;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
for (i = 0; i < args.length; i += 1) {
|
grants = (grantResults).grants.filter(function (grant) {
|
||||||
arg = args[i];
|
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
|
||||||
if (-1 === arg.indexOf('=')) {
|
return true;
|
||||||
argsParsed[decodeURIComponent(arg).trim()] = true;
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grantedScopesMap = {};
|
||||||
|
acceptedScopes = [];
|
||||||
|
pendingScopes = scopes.filter(function (requestedScope) {
|
||||||
|
return grants.every(function (grant) {
|
||||||
|
if (!grant.scope) {
|
||||||
|
grant.scope = 'oauth3_authn';
|
||||||
|
}
|
||||||
|
var gscopes = grant.scope.split(/[+, ]/g);
|
||||||
|
gscopes.forEach(function (s) { grantedScopesMap[s] = true; });
|
||||||
|
if (-1 !== gscopes.indexOf(requestedScope)) {
|
||||||
|
// already accepted in the past
|
||||||
|
acceptedScopes.push(requestedScope);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
kvp = arg.split('=');
|
// true, is pending
|
||||||
key = decodeURIComponent(kvp[0]).trim();
|
return true;
|
||||||
value = decodeURIComponent(kvp[1]).trim();
|
|
||||||
argsParsed[key] = value;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
return argsParsed;
|
});
|
||||||
};
|
grantedScopes = Object.keys(grantedScopesMap);
|
||||||
|
|
||||||
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 || {};
|
|
||||||
var type = 'access_token';
|
|
||||||
var grantType = 'password';
|
|
||||||
|
|
||||||
if (!opts.password) {
|
|
||||||
if (opts.otp) {
|
|
||||||
// for backwards compat
|
|
||||||
opts.password = opts.otp; // 'otp:' + opts.otpUuid + ':' + opts.otp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var scope = opts.scope || directive.authn_scope;
|
|
||||||
var clientId = opts.appId || opts.clientId || opts.client_id;
|
|
||||||
var clientAgreeTos = opts.clientAgreeTos || opts.client_agree_tos;
|
|
||||||
var clientUri = opts.clientUri || opts.client_uri || opts.clientUrl || opts.client_url;
|
|
||||||
var args = directive[type];
|
|
||||||
var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined;
|
|
||||||
var params = {
|
|
||||||
"grant_type": grantType
|
|
||||||
, "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
|
|
||||||
};
|
|
||||||
var uri = args.url;
|
|
||||||
var body;
|
|
||||||
if (opts.totp) {
|
|
||||||
params.totp = opts.totp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientId) {
|
|
||||||
params.clientId = clientId;
|
|
||||||
}
|
|
||||||
if (clientUri) {
|
|
||||||
params.clientUri = clientUri;
|
|
||||||
params.clientAgreeTos = clientAgreeTos;
|
|
||||||
if (!clientAgreeTos) {
|
|
||||||
throw new Error('Developer Error: missing clientAgreeTos uri');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scope) {
|
|
||||||
params.scope = core.stringifyscope(scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('GET' === args.method.toUpperCase()) {
|
|
||||||
uri += '?' + core.querystringify(params);
|
|
||||||
} else {
|
|
||||||
body = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: uri
|
pending: pendingScopes // not yet accepted
|
||||||
, method: args.method
|
, granted: grantedScopes // all granted, ever
|
||||||
, data: body
|
, requested: scopes // all requested, now
|
||||||
|
, accepted: acceptedScopes // granted (ever) and requested (now)
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
OAUTH3.authz.grants = function (providerUri, opts) {
|
||||||
|
return OAUTH3.discover(providerUri, {
|
||||||
|
client_id: providerUri
|
||||||
|
, debug: opts.debug
|
||||||
|
}).then(function (directive) {
|
||||||
|
console.log('providerUri', providerUri);
|
||||||
|
console.log('directive', directive);
|
||||||
|
|
||||||
OAUTH3.authz = {};
|
return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts).then(function (grantsResult) {
|
||||||
OAUTH3.authz.loginMeta = function (directive, opts) {
|
if ('POST' === opts.method) {
|
||||||
if (opts.mock) {
|
// TODO this is clientToken
|
||||||
if (opts.mockError) {
|
return grantsResult.originalData || grantsResult.data;
|
||||||
return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}});
|
|
||||||
}
|
|
||||||
return OAUTH3.PromiseA.resolve({data: {}});
|
|
||||||
}
|
|
||||||
|
|
||||||
return OAUTH3.request({
|
|
||||||
method: directive.credential_meta.method || 'GET'
|
|
||||||
// TODO lint urls
|
|
||||||
, url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_meta.url)
|
|
||||||
.replace(':type', 'email')
|
|
||||||
.replace(':id', opts.email)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
OAUTH3.authz.otp = function (directive, opts) {
|
|
||||||
if (opts.mock) {
|
|
||||||
if (opts.mockError) {
|
|
||||||
return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}});
|
|
||||||
}
|
|
||||||
return OAUTH3.PromiseA.resolve({data: {uuid: "uuidblah"}});
|
|
||||||
}
|
|
||||||
|
|
||||||
return OAUTH3.request({
|
|
||||||
method: directive.credential_otp.url.method || 'POST'
|
|
||||||
, url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_otp.url)
|
|
||||||
, data: {
|
|
||||||
// TODO replace with signed hosted file
|
|
||||||
client_agree_tos: 'oauth3.org/tos/draft'
|
|
||||||
, client_id: directive.issuer // In this case, the issuer is its own client
|
|
||||||
, client_uri: directive.issuer
|
|
||||||
, request_otp: true
|
|
||||||
, username: opts.email
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
|
|
||||||
var providerUri = directive.issuer;
|
|
||||||
if (opts.mock) {
|
|
||||||
if (opts.mockError) {
|
|
||||||
return OAUTH3.PromiseA.resolve({data: {error_description: "fake error", error: "errorcode", error_uri: "https://blah"}});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//core.jwt.encode({header: {alg: 'none'}, payload: {exp: Date.now() / 1000 + 900, sub: 'fakeUserId'}, signature: "fakeSig" })
|
var grants = grantsResult.originalData || grantsResult.data;
|
||||||
|
// TODO
|
||||||
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.refresh(
|
if (grants.error) {
|
||||||
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
|
return oauth3.PromiseA.reject(oauth3.utils._formatError(grants.error));
|
||||||
|
}
|
||||||
|
|
||||||
, { access_token: "eyJhbGciOiJub25lIn0.eyJleHAiOjE0ODc2MTQyMzUuNzg3LCJzdWIiOiJmYWtlVXNlcklkIn0.fakeSig"
|
console.warn('requests.grants', grants);
|
||||||
, refresh_token: "eyJhbGciOiJub25lIn0.eyJleHAiOjE0ODc2MTQyMzUuNzg3LCJzdWIiOiJmYWtlVXNlcklkIn0.fakeSig"
|
|
||||||
, expires_in: "900"
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
//var scope = opts.scope;
|
oauth3.hooks.setGrants(opts.client_id + '-client', grants.client);
|
||||||
//var appId = opts.appId;
|
grants.grants.forEach(function (grant) {
|
||||||
return oauth3.discover(providerUri, opts).then(function (directive) {
|
var clientId = grant.client_id || grant.oauth_client_id || grant.oauthClientId;
|
||||||
var prequest = core.urls.resourceOwnerPassword(directive, opts);
|
// TODO should save as an array
|
||||||
|
oauth3.hooks.setGrants(clientId, [ grant ]);
|
||||||
return oauth3.request(prequest).then(function (req) {
|
|
||||||
var data = (req.originalData || req.data);
|
|
||||||
data.provider_uri = providerUri;
|
|
||||||
if (data.error) {
|
|
||||||
return oauth3.PromiseA.reject(oauth3.core.formatError(providerUri, data.error));
|
|
||||||
}
|
|
||||||
return oauth3.hooks.refreshSession(
|
|
||||||
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
|
|
||||||
, data
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: oauth3.hooks.getGrants(opts.client_id + '-client')
|
||||||
|
, grants: oauth3.hooks.getGrants(opts.client_id) || []
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) {
|
||||||
|
console.info('redirectWithToken scopes');
|
||||||
|
console.log(scopes);
|
||||||
|
|
||||||
OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) {
|
scopes.new = scopes.new || [];
|
||||||
console.info('redirectWithToken scopes');
|
|
||||||
console.log(scopes);
|
|
||||||
|
|
||||||
scopes.new = scopes.new || [];
|
if ('token' === clientParams.response_type) {
|
||||||
|
// get token and redirect client-side
|
||||||
if ('token' === clientParams.response_type) {
|
return OAUTH3.authz.grants(providerUri, {
|
||||||
// get token and redirect client-side
|
|
||||||
return OAUTH3.requests.grants(providerUri, {
|
|
||||||
method: 'POST'
|
|
||||||
, client_id: clientParams.client_uri
|
|
||||||
, client_uri: clientParams.client_uri
|
|
||||||
, scope: scopes.granted.concat(scopes.new).join(',')
|
|
||||||
, response_type: clientParams.response_type
|
|
||||||
, referrer: clientParams.referrer
|
|
||||||
, session: session
|
|
||||||
, debug: clientParams.debug
|
|
||||||
}).then(function (results) {
|
|
||||||
console.info('generate token results');
|
|
||||||
console.info(results);
|
|
||||||
|
|
||||||
OAUTH3.redirect(clientParams, scopes, results);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if ('code' === clientParams.response_type) {
|
|
||||||
// get token and redirect server-side
|
|
||||||
// (requires insecure form post as per spec)
|
|
||||||
//OAUTH3.requests.authorizationDecision();
|
|
||||||
window.alert("Authorization Code Redirect NOT IMPLEMENTED");
|
|
||||||
throw new Error("Authorization Code Redirect NOT IMPLEMENTED");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
OAUTH3.requests = {};
|
|
||||||
OAUTH3.requests.accounts = {};
|
|
||||||
OAUTH3.requests.accounts.update = function (directive, session, opts) {
|
|
||||||
var dir = directive.update_account || {
|
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
, url: 'https://' + directive.provider_url + '/api/org.oauth3.provider/accounts/:accountId'
|
, client_id: clientParams.client_uri
|
||||||
, bearer: 'Bearer'
|
, client_uri: clientParams.client_uri
|
||||||
};
|
, scope: scopes.granted.concat(scopes.new).join(',')
|
||||||
var url = dir.url
|
, response_type: clientParams.response_type
|
||||||
.replace(/:accountId/, opts.accountId)
|
, referrer: clientParams.referrer
|
||||||
;
|
, session: session
|
||||||
|
, debug: clientParams.debug
|
||||||
|
}).then(function (results) {
|
||||||
|
console.info('generate token results');
|
||||||
|
console.info(results);
|
||||||
|
|
||||||
return OAUTH3.request({
|
OAUTH3.utils.url.redirect(clientParams, scopes, results);
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
else if ('code' === clientParams.response_type) {
|
||||||
|
// get token and redirect server-side
|
||||||
|
// (requires insecure form post as per spec)
|
||||||
|
//OAUTH3.requests.authorizationDecision();
|
||||||
|
window.alert("Authorization Code Redirect NOT IMPLEMENTED");
|
||||||
|
throw new Error("Authorization Code Redirect NOT IMPLEMENTED");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
OAUTH3.requests = {};
|
||||||
|
OAUTH3.requests.accounts = {};
|
||||||
|
OAUTH3.requests.accounts.update = function (directive, session, opts) {
|
||||||
|
var dir = directive.update_account || {
|
||||||
|
method: 'POST'
|
||||||
|
, url: 'https://' + directive.provider_url + '/api/org.oauth3.provider/accounts/:accountId'
|
||||||
|
, bearer: 'Bearer'
|
||||||
};
|
};
|
||||||
OAUTH3.requests.accounts.create = function (directive, session, account) {
|
var url = dir.url
|
||||||
var dir = directive.create_account || {
|
.replace(/:accountId/, opts.accountId)
|
||||||
method: 'POST'
|
;
|
||||||
, url: 'https://' + directive.issuer + '/api/org.oauth3.provider/accounts'
|
|
||||||
, bearer: 'Bearer'
|
return OAUTH3.request({
|
||||||
};
|
method: dir.method || 'POST'
|
||||||
var data = {
|
, url: url
|
||||||
// TODO fix the server to just use one scheme
|
, headers: {
|
||||||
// account = { nick, self: { comment, username } }
|
'Authorization': (dir.bearer || 'Bearer') + ' ' + session.accessToken
|
||||||
// account = { name, comment, display_name, priority }
|
}
|
||||||
account: {
|
, 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: 'https://' + directive.issuer + '/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
|
nick: account.display_name
|
||||||
, name: account.name
|
, name: account.name
|
||||||
, comment: account.comment
|
, comment: account.comment
|
||||||
, display_name: account.display_name
|
, display_name: account.display_name
|
||||||
, priority: account.priority
|
, priority: account.priority
|
||||||
, self: {
|
|
||||||
nick: account.display_name
|
|
||||||
, name: account.name
|
|
||||||
, comment: account.comment
|
|
||||||
, display_name: account.display_name
|
|
||||||
, priority: account.priority
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
, logins: [
|
}
|
||||||
{
|
, logins: [
|
||||||
token: session.access_token
|
{
|
||||||
}
|
token: session.access_token
|
||||||
]
|
}
|
||||||
};
|
]
|
||||||
|
|
||||||
return OAUTH3.request({
|
|
||||||
method: dir.method || 'POST'
|
|
||||||
, url: dir.url
|
|
||||||
, session: session
|
|
||||||
, data: data
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return OAUTH3.request({
|
||||||
|
method: dir.method || 'POST'
|
||||||
|
, url: dir.url
|
||||||
|
, session: session
|
||||||
|
, data: data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3._browser.isIframe = function isIframe () {
|
||||||
|
try {
|
||||||
|
return window.self !== window.top;
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}('undefined' !== typeof exports ? exports : window));
|
}('undefined' !== typeof exports ? exports : window));
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* global Promise */
|
||||||
|
;(function (exports) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
OAUTH3.utils._base64ToUrlSafeBase64 = function (b64) {
|
||||||
|
// Base64 to URL-safe Base64
|
||||||
|
b64 = b64.replace(/\+/g, '-').replace(/\//g, '_');
|
||||||
|
b64 = b64.replace(/=+/g, '');
|
||||||
|
return b64;
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.jwt.encode = function (parts) {
|
||||||
|
parts.header = parts.header || { alg: 'none', typ: 'jwt' };
|
||||||
|
parts.signature = parts.signature || '';
|
||||||
|
|
||||||
|
var btoa = exports.btoa || require('btoa');
|
||||||
|
var result = [
|
||||||
|
OAUTH3.utils._base64ToUrlSafeBase64(btoa(JSON.stringify(parts.header, null)))
|
||||||
|
, OAUTH3.utils._base64ToUrlSafeBase64(btoa(JSON.stringify(parts.payload, null)))
|
||||||
|
, parts.signature // should already be url-safe base64
|
||||||
|
].join('.');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
|
||||||
|
var providerUri = directive.issuer;
|
||||||
|
|
||||||
|
if (opts.mockError) {
|
||||||
|
return OAUTH3.PromiseA.resolve({data: {error_description: "fake error", error: "errorcode", error_uri: "https://blah"}});
|
||||||
|
}
|
||||||
|
|
||||||
|
return OAUTH3._mockToken(providerUri, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.authz.grants = function (providerUri, opts) {
|
||||||
|
if ('POST' === opts.method) {
|
||||||
|
return OAUTH3._mockToken(providerUri, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OAUTH3.discover(providerUri, {
|
||||||
|
client_id: providerUri
|
||||||
|
, debug: opts.debug
|
||||||
|
}).then(function (directive) {
|
||||||
|
return {
|
||||||
|
client: {
|
||||||
|
name: "foo"
|
||||||
|
, client_id: "localhost.foo.daplie.me:8443"
|
||||||
|
, url: "https://localhost.foo.daplie.me:8443"
|
||||||
|
}
|
||||||
|
, grants: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3._refreshToken = function (providerUri, opts) {
|
||||||
|
return OAUTH3._mockToken(providerUri, opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3._mockToken = function (providerUri, opts) {
|
||||||
|
var accessToken = OAUTH3.jwt.encode({
|
||||||
|
header: { alg: 'none' }
|
||||||
|
, payload: { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope }
|
||||||
|
, signature: "fakeSig"
|
||||||
|
});
|
||||||
|
|
||||||
|
return OAUTH3.hooks.session.refresh(
|
||||||
|
opts.session || {
|
||||||
|
provider_uri: providerUri
|
||||||
|
, client_id: opts.client_id
|
||||||
|
, client_uri: opts.client_uri || opts.clientUri
|
||||||
|
}
|
||||||
|
, { access_token: accessToken
|
||||||
|
, refresh_token: accessToken
|
||||||
|
, expires_in: "900"
|
||||||
|
, scope: opts.scope
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
}('undefined' !== typeof exports ? exports : window));
|
Loading…
Reference in New Issue