complete redirect flow

This commit is contained in:
AJ ONeal 2017-02-20 15:22:48 -07:00
parent c2370d9b76
commit bb6bcd826e
3 changed files with 608 additions and 257 deletions

View File

@ -7,12 +7,14 @@
clientUri: function (location) {
return OAUTH3.utils.uri.normalize(location.host + location.pathname);
}
, _formatError: function (providerUri, params) {
, _error: {
parse: function (providerUri, params) {
var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'");
err.uri = params.error_uri || params.error.uri;
err.code = params.error.code || params.error;
return err;
}
}
, atob: function (base64) {
return (exports.atob || require('atob'))(base64);
}
@ -130,7 +132,7 @@
// 'abc.qrs.xyz'
// [ 'abc', 'qrs', 'xyz' ]
// [ {}, {}, 'foo' ]
// { header: {}, payload: {}, signature: }
// { header: {}, payload: {}, signature: '' }
var parts = str.split(/\./g);
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
var atob = exports.atob || require('atob');
@ -676,6 +678,8 @@
return OAUTH3.request({
method: 'GET'
, url: OAUTH3.utils.url.normalize(providerUri) + '/.well-known/oauth3/directives.json'
}).then(function (resp) {
return resp.data;
});
}
@ -913,6 +917,8 @@
}
}
};
OAUTH3.utils._formatError = OAUTH3.utils._error.parse;
if ('undefined' !== typeof Promise) {
OAUTH3.PromiseA = Promise;
}

View File

@ -31,9 +31,80 @@ OAUTH3.utils.query.parse = function (search) {
}
}
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);
OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
// 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)
@ -93,11 +164,11 @@ OAUTH3.utils.query.parse = function (search) {
}
if (scope) {
params.scope = core.stringifyscope(scope);
params.scope = OAUTH3.utils.scope.stringify(scope);
}
if ('GET' === args.method.toUpperCase()) {
uri += '?' + core.querystringify(params);
uri += '?' + OAUTH3.utils.query.stringify(params);
} else {
body = params;
}
@ -107,10 +178,68 @@ OAUTH3.utils.query.parse = function (search) {
, method: args.method
, data: body
};
};
};
OAUTH3.urls.grants = function (directive, opts) {
// directive = { issuer, authorization_decision }
// opts = { response_type, scopes{ granted, requested, pending, accepted } }
OAUTH3.authz = {};
OAUTH3.authz.loginMeta = function (directive, opts) {
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'}}});
@ -125,9 +254,9 @@ OAUTH3.utils.query.parse = function (search) {
.replace(':type', 'email')
.replace(':id', opts.email)
});
};
};
OAUTH3.authz.otp = function (directive, opts) {
OAUTH3.authz.otp = function (directive, opts) {
if (opts.mock) {
if (opts.mockError) {
return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}});
@ -147,47 +276,174 @@ OAUTH3.utils.query.parse = function (search) {
, username: opts.email
}
});
};
};
OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
console.log('ginger bread man');
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" })
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.refresh(
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
, { access_token: "eyJhbGciOiJub25lIn0.eyJleHAiOjE0ODc2MTQyMzUuNzg3LCJzdWIiOiJmYWtlVXNlcklkIn0.fakeSig"
, refresh_token: "eyJhbGciOiJub25lIn0.eyJleHAiOjE0ODc2MTQyMzUuNzg3LCJzdWIiOiJmYWtlVXNlcklkIn0.fakeSig"
, expires_in: "900"
}
));
}
//var scope = opts.scope;
//var appId = opts.appId;
return oauth3.discover(providerUri, opts).then(function (directive) {
var prequest = core.urls.resourceOwnerPassword(directive, opts);
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.originalData || req.data);
return OAUTH3.request(prequest).then(function (req) {
var data = req.data;
data.provider_uri = providerUri;
if (data.error) {
return oauth3.PromiseA.reject(oauth3.core.formatError(providerUri, data.error));
return oauth3.PromiseA.reject(OAUTH3.utils._error.parse(providerUri, data.error));
}
return oauth3.hooks.refreshSession(
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.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) {
// 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;
}
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);
}
grants = (grantResults).grants.filter(function (grant) {
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
return 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 {
// true, is pending
return true;
}
});
});
grantedScopes = Object.keys(grantedScopesMap);
return {
pending: pendingScopes // not yet accepted
, granted: grantedScopes // all granted, ever
, 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);
return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts).then(function (grantsResult) {
if ('POST' === opts.method) {
// TODO this is clientToken
return grantsResult.originalData || grantsResult.data;
}
var grants = grantsResult.originalData || grantsResult.data;
// TODO
if (grants.error) {
return oauth3.PromiseA.reject(oauth3.utils._formatError(grants.error));
}
console.warn('requests.grants', grants);
oauth3.hooks.setGrants(opts.client_id + '-client', grants.client);
grants.grants.forEach(function (grant) {
var clientId = grant.client_id || grant.oauth_client_id || grant.oauthClientId;
// TODO should save as an array
oauth3.hooks.setGrants(clientId, [ grant ]);
});
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);
@ -195,7 +451,7 @@ OAUTH3.utils.query.parse = function (search) {
if ('token' === clientParams.response_type) {
// get token and redirect client-side
return OAUTH3.requests.grants(providerUri, {
return OAUTH3.authz.grants(providerUri, {
method: 'POST'
, client_id: clientParams.client_uri
, client_uri: clientParams.client_uri
@ -208,7 +464,7 @@ OAUTH3.utils.query.parse = function (search) {
console.info('generate token results');
console.info(results);
OAUTH3.redirect(clientParams, scopes, results);
OAUTH3.utils.url.redirect(clientParams, scopes, results);
});
}
else if ('code' === clientParams.response_type) {
@ -218,10 +474,10 @@ OAUTH3.utils.query.parse = function (search) {
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) {
};
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'
@ -244,8 +500,8 @@ OAUTH3.utils.query.parse = function (search) {
, priority: opts.priority
}
});
};
OAUTH3.requests.accounts.create = function (directive, session, account) {
};
OAUTH3.requests.accounts.create = function (directive, session, account) {
var dir = directive.create_account || {
method: 'POST'
, url: 'https://' + directive.issuer + '/api/org.oauth3.provider/accounts'
@ -282,6 +538,14 @@ OAUTH3.utils.query.parse = function (search) {
, session: session
, data: data
});
};
};
OAUTH3._browser.isIframe = function isIframe () {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
};
}('undefined' !== typeof exports ? exports : window));

81
oauth3.issuer.mock.js Normal file
View File

@ -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));