WIP provider separation, grant flow
This commit is contained in:
parent
e25398f213
commit
9448ffea6f
|
@ -63,11 +63,11 @@
|
|||
opts = opts || {};
|
||||
var promise = new OAUTH3.PromiseA(function (resolve, reject) {
|
||||
var tok;
|
||||
var $iframe;
|
||||
var iframeDiv;
|
||||
|
||||
function cleanup() {
|
||||
delete window['--oauth3-callback-' + state];
|
||||
$iframe.remove();
|
||||
iframeDiv.remove();
|
||||
clearTimeout(tok);
|
||||
tok = null;
|
||||
}
|
||||
|
@ -94,7 +94,9 @@
|
|||
}
|
||||
framesrc += '></iframe>';
|
||||
|
||||
$('body').append(framesrc);
|
||||
iframeDiv = window.document.createElement('div');
|
||||
iframeDiv.innerHTML = framesrc;
|
||||
window.document.body.appendChild(iframeDiv);
|
||||
});
|
||||
|
||||
// TODO periodically garbage collect expired handlers from window object
|
||||
|
@ -299,7 +301,7 @@
|
|||
, error_uri: 'https://oauth3.org/docs/errors#access_denied'
|
||||
, state: scope.appQuery.state
|
||||
});
|
||||
$window.location.href = redirectUri;
|
||||
window.location.href = redirectUri;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -366,7 +368,7 @@
|
|||
});
|
||||
|
||||
if ('token' === scope.appQuery.response_type) {
|
||||
$window.location.href = redirectUri;
|
||||
window.location.href = redirectUri;
|
||||
return;
|
||||
}
|
||||
else if ('code' === scope.appQuery.response_type) {
|
||||
|
@ -389,13 +391,13 @@
|
|||
console.error('Grant Code Error NOT IMPLEMENTED');
|
||||
console.error(err);
|
||||
console.error(redirectUri);
|
||||
//$window.location.href = redirectUri;
|
||||
//window.location.href = redirectUri;
|
||||
});
|
||||
}
|
||||
|
||||
, hackFormSubmitHelper: function (uri) {
|
||||
// TODO de-jQuerify
|
||||
//$window.location.href = redirectUri;
|
||||
//window.location.href = redirectUri;
|
||||
//return;
|
||||
|
||||
// the only way to do a POST that redirects the current window
|
||||
|
|
181
oauth3.core.js
181
oauth3.core.js
|
@ -82,6 +82,9 @@
|
|||
err.code = params.error.code || params.error;
|
||||
return err;
|
||||
};
|
||||
core.normalizePath = function (path) {
|
||||
return path.replace(/^\//, '').replace(/\/$/, '');
|
||||
};
|
||||
core.normalizeUri = function (providerUri) {
|
||||
// tested with
|
||||
// example.com
|
||||
|
@ -105,34 +108,6 @@
|
|||
;
|
||||
};
|
||||
|
||||
core.urls.discover = function (providerUri, opts) {
|
||||
if (!providerUri) {
|
||||
throw new Error("cannot discover without providerUri");
|
||||
}
|
||||
if (!opts.appUrl) {
|
||||
throw new Error("cannot discover without opts.appUrl");
|
||||
}
|
||||
|
||||
var params = {
|
||||
action: 'directives'
|
||||
, state: core.utils.randomState()
|
||||
, redirect_uri: opts.appUrl + (opts.appCallbackPath || '/.well-known/oauth3/callback.html')
|
||||
, response_type: 'rpc'
|
||||
, _method: 'GET'
|
||||
, _pathname: '.well-known/oauth3/directives.json'
|
||||
, debug: opts.debug || undefined
|
||||
};
|
||||
|
||||
var result = {
|
||||
url: providerUri + '/.well-known/oauth3/#/?' + core.querystringify(params)
|
||||
, state: params.state
|
||||
, method: 'GET'
|
||||
, query: params
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// these might not really belong in core... not sure
|
||||
// there should be node.js- and browser-specific versions probably
|
||||
core.utils = {
|
||||
|
@ -228,6 +203,33 @@
|
|||
}
|
||||
};
|
||||
|
||||
core.urls.discover = function (providerUri, opts) {
|
||||
if (!providerUri) {
|
||||
throw new Error("cannot discover without providerUri");
|
||||
}
|
||||
if (!opts.appUrl) {
|
||||
throw new Error("cannot discover without opts.appUrl");
|
||||
}
|
||||
|
||||
var params = {
|
||||
action: 'directives'
|
||||
, state: core.utils.randomState()
|
||||
, redirect_uri: opts.appUrl + (opts.appCallbackPath || '/.well-known/oauth3/callback.html')
|
||||
, response_type: 'rpc'
|
||||
, _method: 'GET'
|
||||
, _pathname: '.well-known/oauth3/directives.json'
|
||||
, debug: opts.debug || undefined
|
||||
};
|
||||
|
||||
var result = {
|
||||
url: providerUri + '/.well-known/oauth3/#/?' + core.querystringify(params)
|
||||
, state: params.state
|
||||
, method: 'GET'
|
||||
, query: params
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
core.urls.authorizationCode = function (/*directive, scope, redirectUri, clientId*/) {
|
||||
//
|
||||
// Example Authorization Code Request
|
||||
|
@ -373,126 +375,11 @@
|
|||
return result;
|
||||
};
|
||||
|
||||
core.urls.loginCode = function (directive, opts) {
|
||||
//
|
||||
// Example Resource Owner Password Request
|
||||
// (generally for 1st party and direct-partner mobile apps, and webapps)
|
||||
//
|
||||
// POST https://api.example.com/api/org.oauth3.provider/otp
|
||||
// { "request_otp": true, "client_id": "<<id>>", "scope": "<<scope>>"
|
||||
// , "username": "<<username>>" }
|
||||
//
|
||||
opts = opts || {};
|
||||
var clientId = opts.appId || opts.clientId;
|
||||
|
||||
var args = directive.otp;
|
||||
if (!directive.otp) {
|
||||
console.log('[debug] loginCode directive:');
|
||||
console.log(directive);
|
||||
core.urls.resolve = function (base, next) {
|
||||
if (/^https:\/\//i.test(next)) {
|
||||
return next;
|
||||
}
|
||||
var params = {
|
||||
"username": opts.id || opts.username
|
||||
, "request_otp": true // opts.requestOtp || undefined
|
||||
//, "jwt": opts.jwt // TODO sign a proof
|
||||
, debug: opts.debug || undefined
|
||||
};
|
||||
var uri = args.url;
|
||||
var body;
|
||||
if (opts.clientUri) {
|
||||
params.client_uri = opts.clientUri;
|
||||
}
|
||||
if (opts.clientAgreeTos) {
|
||||
params.client_agree_tos = opts.clientAgreeTos;
|
||||
}
|
||||
if (clientId) {
|
||||
params.client_id = clientId;
|
||||
}
|
||||
if ('GET' === args.method.toUpperCase()) {
|
||||
uri += '?' + core.querystringify(params);
|
||||
} else {
|
||||
body = params;
|
||||
}
|
||||
|
||||
return {
|
||||
url: uri
|
||||
, method: args.method
|
||||
, data: body
|
||||
};
|
||||
};
|
||||
|
||||
core.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
|
||||
, "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 {
|
||||
url: uri
|
||||
, method: args.method
|
||||
, data: body
|
||||
};
|
||||
return core.normalizeUrl(base) + '/' + core.normalizePath(next);
|
||||
};
|
||||
|
||||
core.urls.refreshToken = function (directive, opts) {
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
;(function (exports) {
|
||||
'use strict';
|
||||
|
||||
var core = window.OAUTH3_CORE;
|
||||
|
||||
// Provider-Only
|
||||
core.urls.loginCode = function (directive, opts) {
|
||||
//
|
||||
// Example Resource Owner Password Request
|
||||
// (generally for 1st party and direct-partner mobile apps, and webapps)
|
||||
//
|
||||
// POST https://api.example.com/api/org.oauth3.provider/otp
|
||||
// { "request_otp": true, "client_id": "<<id>>", "scope": "<<scope>>"
|
||||
// , "username": "<<username>>" }
|
||||
//
|
||||
opts = opts || {};
|
||||
var clientId = opts.appId || opts.clientId;
|
||||
|
||||
var args = directive.credential_otp;
|
||||
if (!directive.credential_otp) {
|
||||
console.log('[debug] loginCode directive:');
|
||||
console.log(directive);
|
||||
}
|
||||
var params = {
|
||||
"username": opts.id || opts.username
|
||||
, "request_otp": true // opts.requestOtp || undefined
|
||||
//, "jwt": opts.jwt // TODO sign a proof
|
||||
, debug: opts.debug || undefined
|
||||
};
|
||||
var uri = args.url;
|
||||
var body;
|
||||
if (opts.clientUri) {
|
||||
params.client_uri = opts.clientUri;
|
||||
}
|
||||
if (opts.clientAgreeTos) {
|
||||
params.client_agree_tos = opts.clientAgreeTos;
|
||||
}
|
||||
if (clientId) {
|
||||
params.client_id = clientId;
|
||||
}
|
||||
if ('GET' === args.method.toUpperCase()) {
|
||||
uri += '?' + core.querystringify(params);
|
||||
} else {
|
||||
body = params;
|
||||
}
|
||||
|
||||
return {
|
||||
url: uri
|
||||
, method: args.method
|
||||
, data: body
|
||||
};
|
||||
};
|
||||
|
||||
core.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 {
|
||||
url: uri
|
||||
, method: args.method
|
||||
, data: body
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
core.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 && !opts.scope) {
|
||||
console.warn("You must supply options.scope as a space-delimited string of scopes");
|
||||
}
|
||||
|
||||
var url = core.urls.resolve(directive.issuer, directive.grants.url)
|
||||
.replace(/(:azp|:client_id)/g, opts.client_id || opts.client_uri)
|
||||
.replace(/(:sub|:account_id)/g, opts.session.meta.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 += '?' + core.querystringify(data);
|
||||
}
|
||||
else {
|
||||
body = data;
|
||||
}
|
||||
|
||||
return {
|
||||
method: opts.method
|
||||
, url: url
|
||||
, data: body
|
||||
, session: opts.session
|
||||
};
|
||||
};
|
||||
core.urls.authorizationDecision = function (directive, opts) {
|
||||
var url = core.urls.resolve(directive.issuer, directive.authorization_decision.url);
|
||||
if (!opts) {
|
||||
throw new Error("You must supply a directive and an options object");
|
||||
}
|
||||
console.info(url);
|
||||
throw new Error("NOT IMPLEMENTED authorization_decision");
|
||||
};
|
||||
|
||||
exports.OAUTH3_CORE_PROVIDER = core;
|
||||
|
||||
if ('undefined' !== typeof module) {
|
||||
module.exports = core;
|
||||
}
|
||||
}('undefined' !== typeof exports ? exports : window));
|
20
oauth3.js
20
oauth3.js
|
@ -151,6 +151,8 @@
|
|||
function lintAndRequest(preq) {
|
||||
function goGetHer() {
|
||||
if (preq.session) {
|
||||
// TODO check session.meta.aud against preq.url to make sure they match
|
||||
console.warn("[security] session audience checking has not been implemented yet (it's up to you to check)");
|
||||
preq.headers = preq.headers || {};
|
||||
preq.headers.Authorization = 'Bearer ' + (preq.session.access_token || preq.session.accessToken);
|
||||
}
|
||||
|
@ -198,6 +200,12 @@
|
|||
*/
|
||||
};
|
||||
|
||||
oauth3.requests.grants = function (providerUri, opts) {
|
||||
return oauth3.discover(providerUri, opts).then(function (directive) {
|
||||
console.log('core.urls.grants(directive, opts)', core.urls.grants(directive, opts));
|
||||
return oauth3.request(core.urls.grants(directive, opts));
|
||||
});
|
||||
};
|
||||
oauth3.requests.loginCode = function (providerUri, opts) {
|
||||
return oauth3.discover(providerUri, opts).then(function (directive) {
|
||||
var prequest = core.urls.loginCode(directive, opts);
|
||||
|
@ -219,11 +227,7 @@
|
|||
return oauth3.discover(providerUri, opts).then(function (directive) {
|
||||
var prequest = core.urls.resourceOwnerPassword(directive, opts);
|
||||
|
||||
return oauth3.request({
|
||||
url: prequest.url
|
||||
, method: prequest.method
|
||||
, data: prequest.data
|
||||
}).then(function (req) {
|
||||
return oauth3.request(prequest).then(function (req) {
|
||||
var data = (req.originalData || req.data);
|
||||
data.provider_uri = providerUri;
|
||||
if (data.error) {
|
||||
|
@ -243,11 +247,7 @@
|
|||
return oauth3.discover(providerUri, opts).then(function (directive) {
|
||||
var prequest = core.urls.refreshToken(directive, opts);
|
||||
|
||||
return oauth3.request({
|
||||
url: prequest.url
|
||||
, method: prequest.method
|
||||
, data: prequest.data
|
||||
}).then(function (req) {
|
||||
return oauth3.request(prequest).then(function (req) {
|
||||
var data = (req.originalData || req.data);
|
||||
data.provider_uri = providerUri;
|
||||
if (data.error) {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
var separator;
|
||||
|
||||
// TODO check that we appropriately use '#' for implicit and '?' for code
|
||||
// (server-side) in an OAuth2 backwards-compatible way
|
||||
if ('token' === scope.appQuery.response_type) {
|
||||
separator = '#';
|
||||
}
|
||||
else if ('code' === scope.appQuery.response_type) {
|
||||
separator = '?';
|
||||
}
|
||||
else {
|
||||
separator = '#';
|
||||
}
|
||||
|
||||
if (scope.pendingScope.length && !opts.allow) {
|
||||
redirectUri += separator + Oauth3.querystringify({
|
||||
error: 'access_denied'
|
||||
, error_description: 'None of the permissions were accepted'
|
||||
, error_uri: 'https://oauth3.org/docs/errors#access_denied'
|
||||
, state: scope.appQuery.state
|
||||
});
|
||||
$window.location.href = redirectUri;
|
||||
return;
|
||||
}
|
Loading…
Reference in New Issue