1763 lines
60 KiB
JavaScript
1763 lines
60 KiB
JavaScript
/ * global Promise */
|
|
;(function (exports) {
|
|
'use strict';
|
|
|
|
if ('undefined' !== typeof window && 'https:' !== window.location.protocol) {
|
|
window.alert("You must use https. We suggest using caddy as your webserver (or serve-https if testing locally)");
|
|
}
|
|
|
|
var OAUTH3 = exports.OAUTH3 = {
|
|
clientUri: function (location) {
|
|
return OAUTH3.uri.normalize(location.host + (location.pathname || ''));
|
|
}
|
|
, 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;
|
|
}
|
|
, create: function (opts) {
|
|
var err = new Error(opts.message);
|
|
err.code = opts.code;
|
|
err.uri = opts.uri || opts.url;
|
|
err.subErr = opts.subErr;
|
|
return err;
|
|
}
|
|
}
|
|
, _binStr: {
|
|
bufferToBinStr: function (buf) {
|
|
return Array.prototype.map.call(new Uint8Array(buf), function(ch) {
|
|
return String.fromCharCode(ch);
|
|
}).join('');
|
|
}
|
|
, binStrToBuffer: function (str) {
|
|
var buf;
|
|
|
|
if ('undefined' !== typeof Uint8Array) {
|
|
buf = new Uint8Array(str.length);
|
|
} else {
|
|
buf = [];
|
|
}
|
|
|
|
Array.prototype.forEach.call(str, function (ch, ind) {
|
|
buf[ind] = ch.charCodeAt(0);
|
|
});
|
|
return buf;
|
|
}
|
|
}
|
|
, _base64: {
|
|
atob: function (base64) {
|
|
// atob must be called from the global context
|
|
// http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome
|
|
return (exports.atob || require('atob'))(base64);
|
|
}
|
|
, btoa: function (b64) {
|
|
// for directive passing in .well-known/oauth3
|
|
// http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome
|
|
return (exports.btoa || require('btoa'))(b64);
|
|
}
|
|
, decodeUrlSafe: function (b64) {
|
|
// URL-safe Base64 to Base64
|
|
// https://en.wikipedia.org/wiki/Base64
|
|
// https://gist.github.com/catwell/3046205
|
|
var mod = b64.length % 4;
|
|
if (2 === mod) { b64 += '=='; }
|
|
if (3 === mod) { b64 += '='; }
|
|
b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
|
|
return OAUTH3._base64.atob(b64);
|
|
}
|
|
, encodeUrlSafe: function (b64) {
|
|
// for directive passing in .well-known/oauth3
|
|
// Base64 to URL-safe Base64
|
|
b64 = OAUTH3._base64.btoa(b64);
|
|
b64 = b64.replace(/\+/g, '-').replace(/\//g, '_');
|
|
b64 = b64.replace(/=+/g, '');
|
|
return b64;
|
|
}
|
|
, urlSafeToBuffer: function (str) {
|
|
return OAUTH3._binStr.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str));
|
|
}
|
|
, bufferToUrlSafe: function (buf) {
|
|
return OAUTH3._base64.encodeUrlSafe(OAUTH3._binStr.bufferToBinStr(buf));
|
|
}
|
|
}
|
|
, uri: {
|
|
normalize: function (uri) {
|
|
if ('string' !== typeof uri) {
|
|
throw new Error("attempted to normalize non-string URI: "+JSON.stringify(uri));
|
|
}
|
|
// tested with
|
|
// example.com
|
|
// example.com/
|
|
// http://example.com
|
|
// https://example.com/
|
|
return uri
|
|
.replace(/^(https?:\/\/)?/i, '')
|
|
.replace(/\/?$/, '')
|
|
;
|
|
}
|
|
}
|
|
, url: {
|
|
normalize: function (url) {
|
|
if ('string' !== typeof url) {
|
|
throw new Error("attempted to normalize non-string URL: "+JSON.stringify(url));
|
|
}
|
|
// tested with
|
|
// example.com
|
|
// example.com/
|
|
// http://example.com
|
|
// https://example.com/
|
|
return url
|
|
.replace(/^(https?:\/\/)?/i, 'https://')
|
|
.replace(/\/?$/, '')
|
|
;
|
|
}
|
|
, resolve: function (base, next) {
|
|
if (/^https:\/\//i.test(next)) {
|
|
return next;
|
|
}
|
|
return this.normalize(base) + '/' + this._normalizePath(next);
|
|
}
|
|
, _normalizePath: function (path) {
|
|
return path.replace(/^\//, '').replace(/\/$/, '');
|
|
}
|
|
}
|
|
, query: {
|
|
parse: function (search) {
|
|
// needed for .well-known/oauth3
|
|
// parse a query or a hash
|
|
if (-1 !== ['#', '?'].indexOf(search[0])) {
|
|
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;
|
|
}
|
|
else {
|
|
kvp = arg.split('=');
|
|
key = decodeURIComponent(kvp[0]).trim();
|
|
value = decodeURIComponent(kvp[1]).trim();
|
|
argsParsed[key] = value;
|
|
}
|
|
}
|
|
return argsParsed;
|
|
}
|
|
, stringify: function (params) {
|
|
var qs = [];
|
|
|
|
Object.keys(params).forEach(function (key) {
|
|
// TODO nullify instead?
|
|
if ('undefined' === typeof params[key]) {
|
|
return;
|
|
}
|
|
|
|
if ('scope' === key) {
|
|
params[key] = OAUTH3.scope.stringify(params[key]);
|
|
}
|
|
|
|
qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
|
|
});
|
|
|
|
return qs.join('&');
|
|
}
|
|
}
|
|
, scope: {
|
|
parse: function (scope) {
|
|
return (scope||'').toString().split(/[+, ]+/g);
|
|
}
|
|
, stringify: function (scope) {
|
|
if (Array.isArray(scope)) {
|
|
scope = scope.join(',');
|
|
}
|
|
return scope;
|
|
}
|
|
}
|
|
, randomState: function () {
|
|
// TODO put in different file for browser vs node
|
|
try {
|
|
return Array.prototype.slice.call(
|
|
OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(16))
|
|
).map(function (ch) { return (ch).toString(16); }).join('');
|
|
} catch(e) {
|
|
return OAUTH3.utils._insecureRandomState();
|
|
}
|
|
}
|
|
, _insecureRandomState: function () {
|
|
var i;
|
|
var ch;
|
|
var str;
|
|
// TODO use fisher-yates on 0..255 and select [0] 16 times
|
|
// [security] https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.5qx0bf95a
|
|
// https://github.com/v8/v8/blob/b0e4dce6091a8777bda80d962df76525dc6c5ea9/src/js/math.js#L135-L144
|
|
// Note: newer versions of v8 do not have this bug, but other engines may still
|
|
console.warn('[security] crypto.getRandomValues() failed, falling back to Math.random()');
|
|
str = '';
|
|
for (i = 0; i < 32; i += 1) {
|
|
ch = Math.round(Math.random() * 255).toString(16);
|
|
if (ch.length < 2) { ch = '0' + ch; }
|
|
str += ch;
|
|
}
|
|
return str;
|
|
}
|
|
, jwk: {
|
|
get: function (decoded) {
|
|
return OAUTH3.discover(decoded.payload.iss).then(function (directives) {
|
|
var urlObj = OAUTH3.urls.jwk(directives, decoded);
|
|
return OAUTH3.request(urlObj).catch(function (err) {
|
|
return PromiseA.reject({
|
|
message: 'failed to retrieve public key from token issuer'
|
|
, code: 'E_NO_PUB_KEY'
|
|
, url: 'https://oauth3.org/docs/errors#E_NO_PUB_KEY'
|
|
, subErr: err.toString()
|
|
});
|
|
});
|
|
}, function (err) {
|
|
return PromiseA.reject({
|
|
message: 'token issuer is not a valid OAuth3 provider'
|
|
, code: 'E_INVALID_ISS'
|
|
, url: 'https://oauth3.org/docs/errors#E_INVALID_ISS'
|
|
, subErr: err.toString()
|
|
});
|
|
}).then(function (res) {
|
|
if (res.data.error) {
|
|
return PromiseA.reject(res.data.error);
|
|
}
|
|
return res.data;
|
|
});
|
|
}
|
|
, verifyToken: function (token) {
|
|
var decoded;
|
|
|
|
if (!token) {
|
|
return PromiseA.reject({
|
|
message: 'no token provided'
|
|
, code: 'E_NO_TOKEN'
|
|
, url: 'https://oauth3.org/docs/errors#E_NO_TOKEN'
|
|
});
|
|
}
|
|
|
|
try {
|
|
decoded = OAUTH3.jwt.decode(token, {complete: true});
|
|
} catch (e) {}
|
|
if (!decoded) {
|
|
return PromiseA.reject({
|
|
message: 'provided token not a JSON Web Token'
|
|
, code: 'E_NOT_JWT'
|
|
, url: 'https://oauth3.org/docs/errors#E_NOT_JWT'
|
|
});
|
|
}
|
|
|
|
return OAUTH3.jwk.get(decoded).then(function (jwk) {
|
|
var opts = {};
|
|
if (Array.isArray(jwk.alg)) {
|
|
opts.algorithms = jwk.alg;
|
|
} else if (typeof jwk.alg === 'string') {
|
|
opts.algorithms = [ jwk.alg ];
|
|
}
|
|
|
|
try {
|
|
return OAUTH3.jwt.verify(token, jwk, opts);
|
|
} catch (err) {
|
|
return PromiseA.reject({
|
|
message: 'token verification failed'
|
|
, code: 'E_INVALID_TOKEN'
|
|
, url: 'https://oauth3.org/docs/errors#E_INVALID_TOKEN'
|
|
, subErr: err.toString()
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
, jwt: {
|
|
// decode only (no verification)
|
|
decode: function (token, opts) {
|
|
|
|
// 'abc.qrs.xyz'
|
|
// [ 'abc', 'qrs', 'xyz' ]
|
|
// {}
|
|
var parts = token.split(/\./g);
|
|
var err;
|
|
if (parts.length !== 3) {
|
|
err = new Error("Invalid JWT: required 3 '.' separated components not "+parts.length);
|
|
err.code = 'E_INVALID_JWT';
|
|
throw err;
|
|
}
|
|
|
|
if (!opts || !opts.complete) {
|
|
return JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1]));
|
|
}
|
|
return {
|
|
header: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[0]))
|
|
, payload: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1]))
|
|
};
|
|
}
|
|
, verify: function (token, jwk/*, opts*/) {
|
|
if (!OAUTH3.crypto) {
|
|
return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable"));
|
|
}
|
|
jwk = jwk.publicKey || jwk;
|
|
|
|
var parts = token.split(/\./g);
|
|
var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.'));
|
|
var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]);
|
|
var decoded = OAUTH3.jwt.decode(token, { complete: true });
|
|
|
|
// TODO disallow none and hmac algorithms
|
|
// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
|
if (!decoded.header.alg || 'none' === decoded.header.alg.toString() || /^HS/i.test(decoded.header.alg.toString())) {
|
|
throw new Error("token algorithm '" + decoded.header.alg + "' is not accepted");
|
|
}
|
|
return OAUTH3.crypto.core.verify(jwk, data, signature).then(function () {
|
|
return decoded;
|
|
});
|
|
}
|
|
, sign: function (payload, jwk) {
|
|
if (!OAUTH3.crypto) {
|
|
return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable"));
|
|
}
|
|
jwk = jwk.private_key || jwk.privateKey || jwk;
|
|
|
|
var prom;
|
|
if (jwk.kid) {
|
|
prom = OAUTH3.PromiseA.resolve(jwk.kid);
|
|
} else {
|
|
prom = OAUTH3.crypto.thumbprintJwk(jwk);
|
|
}
|
|
|
|
return prom.then(function (kid) {
|
|
// Currently the crypto part of the OAuth3 library only supports ES256
|
|
var header = {type: 'JWT', alg: 'ES256', kid: kid};
|
|
var input = [
|
|
OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null))
|
|
, OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null))
|
|
].join('.');
|
|
|
|
return OAUTH3.crypto.core.sign(jwk, OAUTH3._binStr.binStrToBuffer(input))
|
|
.then(OAUTH3._base64.bufferToUrlSafe)
|
|
.then(function (signature) {
|
|
return input + '.' + signature;
|
|
});
|
|
});
|
|
}
|
|
, freshness: function (tokenMeta, staletime, now) {
|
|
// If the token doesn't expire then it's always fresh.
|
|
if (!tokenMeta.exp) {
|
|
return 'fresh';
|
|
}
|
|
|
|
staletime = staletime || (15 * 60);
|
|
now = now || Date.now();
|
|
// This particular number used to check if time is in milliseconds or seconds will work
|
|
// for any date between the years 1973 and 5138.
|
|
if (now > 1e11) {
|
|
now = Math.round(now / 1000);
|
|
}
|
|
var exp = parseInt(tokenMeta.exp, 10) || 0;
|
|
if (exp < now) {
|
|
return 'expired';
|
|
} else if (exp < now + staletime) {
|
|
return 'stale';
|
|
} else {
|
|
return 'fresh';
|
|
}
|
|
}
|
|
}
|
|
, urls: {
|
|
rpc: function (providerUri, opts) {
|
|
if (!providerUri) {
|
|
throw new Error("cannot run rpc without providerUri");
|
|
}
|
|
if (!opts.client_id) {
|
|
throw new Error("cannot run rpc without options.client_id");
|
|
}
|
|
var clientId = OAUTH3.url.normalize(opts.client_id || opts.client_uri);
|
|
providerUri = OAUTH3.url.normalize(providerUri);
|
|
|
|
var params = {
|
|
state: opts.state || OAUTH3.utils.randomState()
|
|
, redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/')
|
|
, response_type: 'rpc'
|
|
, _method: 'GET'
|
|
, _scheme: opts._scheme
|
|
, _pathname: opts._pathname
|
|
, debug: opts.debug || undefined
|
|
};
|
|
|
|
var toRequest = {
|
|
url: providerUri + '/.well-known/oauth3/#/?' + OAUTH3.query.stringify(params)
|
|
, state: params.state
|
|
, method: 'GET'
|
|
, query: params
|
|
};
|
|
|
|
return toRequest;
|
|
}
|
|
, broker: function (providerUri, opts) {
|
|
opts._scheme = "localstorage:";
|
|
opts._pathname = "issuer";
|
|
return OAUTH3.urls.rpc(providerUri, opts);
|
|
}
|
|
, discover: function (providerUri, opts) {
|
|
return OAUTH3.urls.directives(providerUri, opts);
|
|
}
|
|
, directives: function (providerUri, opts) {
|
|
opts._pathname = ".well-known/oauth3/scopes.json";
|
|
return OAUTH3.urls.rpc(providerUri, opts);
|
|
}
|
|
, jwk: function (directives, decoded) {
|
|
var sub = decoded.payload.sub || decoded.payload.ppid || decoded.payload.appScopedId;
|
|
if (!sub) {
|
|
throw OAUTH3.error.create({
|
|
message: 'token missing sub'
|
|
, code: 'E_MISSING_SUB'
|
|
, url: 'https://oauth3.org/docs/errors#E_MISSING_SUB'
|
|
});
|
|
}
|
|
var kid = decoded.header.kid || decoded.payload.kid;
|
|
if (!kid) {
|
|
throw OAUTH3.error.create({
|
|
message: 'token missing kid'
|
|
, code: 'E_MISSING_KID'
|
|
, url: 'https://oauth3.org/docs/errors#E_MISSING_KID'
|
|
});
|
|
}
|
|
if (!decoded.payload.iss) {
|
|
throw OAUTH3.error.create({
|
|
message: 'token missing iss'
|
|
, code: 'E_MISSING_ISS'
|
|
, url: 'https://oauth3.org/docs/errors#E_MISSING_ISS'
|
|
});
|
|
}
|
|
|
|
var args = (directives || {}).retrieve_jwk;
|
|
if (typeof args === 'string') {
|
|
args = { url: args, method: 'GET' };
|
|
}
|
|
if (typeof (args || {}).url !== 'string') {
|
|
throw OAUTH3.error.create({
|
|
message: 'token issuer does not support retrieving JWKs'
|
|
, code: 'E_INVALID_ISS'
|
|
, url: 'https://oauth3.org/docs/errors#E_INVALID_ISS'
|
|
});
|
|
}
|
|
|
|
var params = {
|
|
sub: sub
|
|
, kid: kid
|
|
};
|
|
var url = args.url;
|
|
var body;
|
|
Object.keys(params).forEach(function (key) {
|
|
if (url.indexOf(':'+key) !== -1) {
|
|
url = url.replace(':'+key, params[key]);
|
|
delete params[key];
|
|
}
|
|
});
|
|
if (Object.keys(params).length > 0) {
|
|
if ('GET' === (args.method || 'GET').toUpperCase()) {
|
|
url += '?' + OAUTH3.query.stringify(params);
|
|
} else {
|
|
body = params;
|
|
}
|
|
}
|
|
|
|
return {
|
|
url: OAUTH3.url.resolve(directives.api, url)
|
|
, method: args.method
|
|
, data: body
|
|
};
|
|
}
|
|
, implicitGrant: function (directive, opts) {
|
|
//
|
|
// Example Implicit Grant Request
|
|
// (for generating a browser-only session, not a session on your server)
|
|
//
|
|
// GET https://example.com/api/issuer@oauth3.org/authorization_dialog
|
|
// ?response_type=token
|
|
// &scope=`encodeURIComponent('profile.login profile.email')`
|
|
// &state=`cryptoutil.random().toString('hex')`
|
|
// &client_id=xxxxxxxxxxx
|
|
// &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')`
|
|
//
|
|
// NOTE: `redirect_uri` itself may also contain URI-encoded components
|
|
//
|
|
|
|
opts = opts || {};
|
|
var type = 'authorization_dialog';
|
|
var responseType = 'token';
|
|
|
|
var scope = opts.scope || directive.authn_scope;
|
|
var args = directive[type];
|
|
var uri = args.url;
|
|
var state = opts.state || OAUTH3.utils.randomState();
|
|
var params = {
|
|
debug: opts.debug || undefined
|
|
, client_uri: opts.client_uri || opts.clientUri || undefined
|
|
, client_id: opts.client_id || opts.client_uri || undefined
|
|
, subject: opts.subject
|
|
, state: state
|
|
};
|
|
var result;
|
|
|
|
console.log('implicitGrant opts.subject: ', opts.subject);
|
|
|
|
params.response_type = responseType;
|
|
if (scope) {
|
|
params.scope = OAUTH3.scope.stringify(scope);
|
|
}
|
|
if (!opts.redirect_uri) {
|
|
// TODO consider making this optional
|
|
//console.warn("auto-generating redirect_uri from hard-coded callback.html"
|
|
// + " (should be configurable... but then redirect_uri could just be manually-generated)");
|
|
opts.redirect_uri = OAUTH3.url.resolve(
|
|
OAUTH3.url.normalize(params.client_uri)
|
|
, '.well-known/oauth3/callback.html'
|
|
);
|
|
}
|
|
params.redirect_uri = opts.redirect_uri;
|
|
|
|
uri += '?' + OAUTH3.query.stringify(params);
|
|
|
|
result = {
|
|
url: uri
|
|
, state: state
|
|
, method: args.method
|
|
, query: params
|
|
};
|
|
|
|
return result;
|
|
}
|
|
, refreshToken: function (directive, opts) {
|
|
// grant_type=refresh_token
|
|
|
|
// Example Refresh Token Request
|
|
// (generally for 1st or 3rd party server-side, mobile, and desktop apps)
|
|
//
|
|
// POST https://example.com/api/oauth3/access_token
|
|
// { "grant_type": "refresh_token", "client_id": "<<id>>", "scope": "<<scope>>"
|
|
// , "username": "<<username>>", "password": "password" }
|
|
//
|
|
opts = opts || {};
|
|
var refresh_token = opts.refresh_token || (opts.session && opts.session.refresh_token);
|
|
var err;
|
|
if (!refresh_token) {
|
|
err = new Error('refreshing a token requires a refresh token');
|
|
err.code = 'E_NO_TOKEN';
|
|
throw err;
|
|
}
|
|
if (OAUTH3.jwt.freshness(OAUTH3.jwt.decode(refresh_token)) === 'expired') {
|
|
err = new Error('refresh token has also expired, login required again');
|
|
err.code = 'E_EXPIRED_TOKEN';
|
|
throw err;
|
|
}
|
|
|
|
var scope = opts.scope || directive.authn_scope;
|
|
var args = directive.access_token;
|
|
var params = {
|
|
"grant_type": 'refresh_token'
|
|
, "refresh_token": refresh_token
|
|
, "response_type": 'token'
|
|
, "client_id": opts.client_id || opts.client_uri
|
|
, "client_uri": opts.client_uri
|
|
, debug: opts.debug || undefined
|
|
};
|
|
var uri = args.url;
|
|
var body;
|
|
|
|
if (opts.client_secret) {
|
|
// TODO not allowed in the browser
|
|
console.warn("if this is a browser, you must not use client_secret");
|
|
params.client_secret = opts.client_secret;
|
|
}
|
|
|
|
if (scope) {
|
|
params.scope = OAUTH3.scope.stringify(scope);
|
|
}
|
|
|
|
if ('GET' === args.method.toUpperCase()) {
|
|
uri += '?' + OAUTH3.query.stringify(params);
|
|
} else {
|
|
body = params;
|
|
}
|
|
|
|
return {
|
|
url: uri
|
|
, method: args.method
|
|
, data: body
|
|
};
|
|
}
|
|
, logout: function (directive, opts) {
|
|
// action=logout
|
|
|
|
// Example Logout Request
|
|
// (generally for 1st or 3rd party server-side, mobile, and desktop apps)
|
|
//
|
|
// GET https://example.com/#/logout/
|
|
// ?client_id=<<id>>
|
|
// &access_token=<<token>>
|
|
// &sub=<<ppid>>
|
|
//
|
|
// Note that the use of # keeps certain parameters from traveling across
|
|
// the network at all (and we use https anyway)
|
|
//
|
|
opts = opts || {};
|
|
var action = 'logout';
|
|
var args = directive[action];
|
|
var state = opts.state || OAUTH3.utils.randomState();
|
|
var params = {
|
|
action: action
|
|
//, response_type: 'confirmation'
|
|
, client_id: opts.client_id || opts.client_uri
|
|
, client_uri: opts.client_uri || opts.client_id
|
|
, state: state
|
|
, redirect_uri: opts.redirect_uri = OAUTH3.url.resolve(
|
|
OAUTH3.url.normalize(opts.client_uri || opts.client_id)
|
|
, '.well-known/oauth3/callback.html'
|
|
)
|
|
, debug: opts.debug
|
|
};
|
|
var uri = args.url;
|
|
var body;
|
|
|
|
if ('GET' === args.method.toUpperCase()) {
|
|
uri += '?' + OAUTH3.query.stringify(params);
|
|
} else {
|
|
body = params;
|
|
}
|
|
|
|
return {
|
|
url: OAUTH3.url.resolve(directive.issuer, uri)
|
|
, method: args.method
|
|
, state: state
|
|
, data: body
|
|
};
|
|
}
|
|
}
|
|
, hooks: {
|
|
_checkStorage: function (grpName, funcName) {
|
|
if (!OAUTH3._hooks) {
|
|
OAUTH3._hooks = {};
|
|
}
|
|
if (!OAUTH3._hooks[grpName]) {
|
|
console.log('using default storage for '+grpName+', set OAUTH3._hooks.'+grpName+' for custom storage');
|
|
OAUTH3._hooks[grpName] = OAUTH3._defaultStorage[grpName];
|
|
}
|
|
if (!OAUTH3._hooks[grpName][funcName]) {
|
|
throw new Error("'"+funcName+"' is not defined for custom "+grpName+" storage");
|
|
}
|
|
}
|
|
, directives: {
|
|
get: function (providerUri) {
|
|
OAUTH3.hooks._checkStorage('directives', 'get');
|
|
|
|
if (!providerUri) {
|
|
throw new Error("providerUri is not set");
|
|
}
|
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.get(OAUTH3.uri.normalize(providerUri)));
|
|
}
|
|
, set: function (providerUri, directives) {
|
|
OAUTH3.hooks._checkStorage('directives', 'set');
|
|
|
|
if (!providerUri) {
|
|
throw new Error("providerUri is not set");
|
|
}
|
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.set(OAUTH3.uri.normalize(providerUri), directives));
|
|
}
|
|
, all: function () {
|
|
OAUTH3.hooks._checkStorage('directives', 'all');
|
|
|
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.all());
|
|
}
|
|
, clear: function () {
|
|
OAUTH3.hooks._checkStorage('directives', 'clear');
|
|
|
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.clear());
|
|
}
|
|
}
|
|
, scopes: {
|
|
get: function(providerUri) {
|
|
//TODO: retrieve cached scopes
|
|
}
|
|
, set: function(providerUri, scopes) {
|
|
//TODO: cache scopes
|
|
}
|
|
}
|
|
, session: {
|
|
refresh: function (oldSession, newSession) {
|
|
var providerUri = oldSession.provider_uri;
|
|
var clientUri = oldSession.client_uri;
|
|
|
|
['access_token', 'token', 'client_uri', 'refresh', 'refresh_token', 'expires_in', 'provider_uri', 'scope', 'token_type'].forEach(function (key) {
|
|
oldSession[key] = undefined;
|
|
});
|
|
Object.keys(newSession).forEach(function (key) {
|
|
oldSession[key] = newSession[key];
|
|
});
|
|
|
|
// info about the session of this API call
|
|
oldSession.provider_uri = providerUri; // aud
|
|
oldSession.client_uri = clientUri; // azp
|
|
|
|
// info about the newly-discovered token
|
|
oldSession.token = OAUTH3.jwt.decode(oldSession.access_token);
|
|
|
|
oldSession.token.sub = oldSession.token.sub
|
|
|| (oldSession.token.acx||{}).id
|
|
|| ((oldSession.token.axs||[])[0]||{}).appScopedId
|
|
|| ((oldSession.token.axs||[])[0]||{}).id
|
|
;
|
|
oldSession.token.client_uri = clientUri;
|
|
oldSession.token.provider_uri = providerUri;
|
|
|
|
if (oldSession.refresh_token) {
|
|
oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token);
|
|
oldSession.refresh.sub = oldSession.refresh.sub
|
|
|| (oldSession.refresh.acx||{}).id
|
|
|| ((oldSession.refresh.axs||[])[0]||{}).appScopedId
|
|
|| ((oldSession.refresh.axs||[])[0]||{}).id
|
|
;
|
|
oldSession.refresh.provider_uri = providerUri;
|
|
}
|
|
|
|
// set for a set of audiences
|
|
return OAUTH3.hooks.session.set(providerUri, oldSession);
|
|
}
|
|
, check: function (preq, opts) {
|
|
opts = opts || {};
|
|
if (!preq.session) {
|
|
return OAUTH3.PromiseA.resolve(null);
|
|
}
|
|
var freshness = OAUTH3.jwt.freshness(preq.session.token, opts.staletime);
|
|
|
|
switch (freshness) {
|
|
case 'stale':
|
|
return OAUTH3.hooks.session.stale(preq.session);
|
|
case 'expired':
|
|
return OAUTH3.hooks.session.expired(preq.session).then(function (newSession) {
|
|
preq.session = newSession;
|
|
return newSession;
|
|
});
|
|
//case 'fresh':
|
|
default:
|
|
return OAUTH3.PromiseA.resolve(preq.session);
|
|
}
|
|
}
|
|
, stale: function (staleSession) {
|
|
if (OAUTH3.hooks.session._stalePromise) {
|
|
return OAUTH3.PromiseA.resolve(staleSession);
|
|
}
|
|
|
|
OAUTH3.hooks.session._stalePromise = OAUTH3._refreshToken(
|
|
staleSession.provider_uri
|
|
, { client_uri: staleSession.client_uri
|
|
, session: staleSession
|
|
, debug: staleSession.debug
|
|
}
|
|
).then(function (newSession) {
|
|
OAUTH3.hooks.session._stalePromise = null;
|
|
return newSession; // oauth3.hooks.refreshSession(staleSession, newSession);
|
|
}, function () {
|
|
OAUTH3.hooks.session._stalePromise = null;
|
|
});
|
|
|
|
return OAUTH3.PromiseA.resolve(staleSession);
|
|
}
|
|
, expired: function (expiredSession) {
|
|
return OAUTH3._refreshToken(
|
|
expiredSession.provider_uri
|
|
, { client_uri: expiredSession.client_uri
|
|
, session: expiredSession
|
|
, debug: expiredSession.debug
|
|
}
|
|
).then(function (newSession) {
|
|
return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession);
|
|
});
|
|
}
|
|
, set: function (providerUri, newSession, id) {
|
|
OAUTH3.hooks._checkStorage('sessions', 'set');
|
|
|
|
if (!providerUri) {
|
|
console.error(new Error('no providerUri').stack);
|
|
throw new Error("providerUri is not set");
|
|
}
|
|
providerUri = OAUTH3.uri.normalize(providerUri);
|
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.set(providerUri, newSession, id));
|
|
}
|
|
, get: function (providerUri, id) {
|
|
OAUTH3.hooks._checkStorage('sessions', 'get');
|
|
|
|
if (!providerUri) {
|
|
throw new Error("providerUri is not set");
|
|
}
|
|
providerUri = OAUTH3.uri.normalize(providerUri);
|
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id));
|
|
}
|
|
, all: function (providerUri) {
|
|
OAUTH3.hooks._checkStorage('sessions', 'all');
|
|
|
|
if (providerUri) {
|
|
providerUri = OAUTH3.uri.normalize(providerUri);
|
|
}
|
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.all(providerUri));
|
|
}
|
|
, clear: function (providerUri) {
|
|
OAUTH3.hooks._checkStorage('sessions', 'clear');
|
|
|
|
if (providerUri) {
|
|
providerUri = OAUTH3.uri.normalize(providerUri);
|
|
}
|
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.clear(providerUri));
|
|
}
|
|
}
|
|
}
|
|
, discoverScopes: function (providerUri, opts) {
|
|
return OAUTH.scopes(providerUri, opts);
|
|
}
|
|
, scopes: function (providerUri, opts) {
|
|
if (!providerUri) {
|
|
throw new Error('oauth3.discoverScopes(providerUri, opts) received providerUri as :', providerUri);
|
|
}
|
|
|
|
opts = opts || {};
|
|
opts._pathname = ".well-known/oauth3/scopes.json";
|
|
|
|
//TODO: add caching
|
|
|
|
return OAUTH3._rpcHelper(providerUri, opts).then(function(scopes) {
|
|
return scopes;
|
|
});
|
|
}
|
|
, discover: function (providerUri, opts) {
|
|
return OAUTH3.directives(providerUri, opts);
|
|
}
|
|
, directives: function (providerUri, opts) {
|
|
if (!providerUri) {
|
|
throw new Error('oauth3.discover(providerUri, opts) received providerUri as :', providerUri);
|
|
}
|
|
|
|
return OAUTH3.hooks.directives.get(providerUri).then(function (directives) {
|
|
if (directives && directives.issuer) {
|
|
return directives;
|
|
}
|
|
|
|
opts._pathname = ".well-known/oauth3/directives.json";
|
|
return OAUTH3._rpcHelper(providerUri, opts).then(function (directives) {
|
|
directives.azp = directives.azp || OAUTH3.url.normalize(providerUri);
|
|
directives.issuer = directives.issuer || OAUTH3.url.normalize(providerUri);
|
|
directives.api = OAUTH3.url.normalize((directives.api||':hostname').replace(/:hostname/, OAUTH3.uri.normalize(directives.issuer) || OAUTH3.uri.normalize(providerUri)));
|
|
// OAUTH3.PromiseA.resolve() is taken care of because this is wrapped
|
|
return OAUTH3.hooks.directives.set(providerUri, directives);
|
|
});
|
|
});
|
|
}
|
|
, _rpcHelper: function(providerUri, opts) {
|
|
return OAUTH3._browser.rpc(providerUri, opts);
|
|
}
|
|
, request: function (preq, opts) {
|
|
function fetch() {
|
|
if (preq.session) {
|
|
// TODO check session.token.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);
|
|
}
|
|
|
|
return OAUTH3._requestHelper(preq, opts);
|
|
}
|
|
|
|
if (!preq.session) {
|
|
return fetch();
|
|
}
|
|
|
|
return OAUTH3.hooks.session.check(preq, opts).then(fetch);
|
|
}
|
|
, _requestHelper: function (preq, opts) {
|
|
/*
|
|
if (opts && opts.directives) {
|
|
preq.url = OAUTH3.url.resolve(opts.directives.issuer, preq.url);
|
|
}
|
|
*/
|
|
return OAUTH3._browser.request(preq, opts);
|
|
}
|
|
, issuer: function (opts) {
|
|
if (!opts) { opts = {}; }
|
|
|
|
// TODO this will default to browserlogin.org
|
|
var broker = opts.broker || 'https://new.oauth3.org';
|
|
//var broker = opts.broker || 'https://broker.oauth3.org';
|
|
|
|
opts._rpc = "broker";
|
|
opts._scheme = "localstorage:";
|
|
opts._pathname = "issuer";
|
|
|
|
return OAUTH3._rpcHelper(broker, opts).then(function(issuer) {
|
|
return issuer;
|
|
});
|
|
}
|
|
, implicitGrant: function (directives, opts) {
|
|
var promise;
|
|
var providerUri = directives.azp || directives.issuer || directives;
|
|
|
|
if (opts.broker) {
|
|
// Discovery can happen in-flow because we know that this is
|
|
// a valid oauth3 provider
|
|
promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts);
|
|
}
|
|
else {
|
|
// Discovery must take place before calling implicitGrant
|
|
promise = OAUTH3.hooks.directives.get(providerUri).then(function (directives) {
|
|
return OAUTH3._implicitGrant(directives, opts);
|
|
});
|
|
}
|
|
|
|
return promise.then(function (tokens) {
|
|
// TODO abstract browser bits away
|
|
try {
|
|
OAUTH3._browser.closeFrame(tokens.state || opts._state, opts);
|
|
} catch(e) {
|
|
console.warn("[implicitGrant] TODO abstract browser bits away");
|
|
}
|
|
opts._state = undefined;
|
|
|
|
return OAUTH3.hooks.session.refresh(
|
|
opts.session || {
|
|
provider_uri: providerUri
|
|
, client_id: opts.client_id
|
|
, client_uri: opts.client_uri || opts.clientUri
|
|
}
|
|
, tokens
|
|
).then(function (session) {
|
|
// TODO set cookie with JWT and TTL
|
|
return OAUTH3.request({
|
|
method: 'POST'
|
|
, url: OAUTH3.url.normalize(
|
|
(directives.assets || 'https://assets.:hostname/assets/issuer@oauth3.org/session')
|
|
.replace(/:hostname/, OAUTH3.uri.normalize(directives.issuer) || OAUTH3.uri.normalize(providerUri))
|
|
)
|
|
, session: session
|
|
}).then(function () {
|
|
return session;
|
|
}, function (/*err*/) {
|
|
return session;
|
|
});
|
|
});
|
|
});
|
|
}
|
|
, _discoverThenImplicitGrant: function(providerUri, opts) {
|
|
opts.windowType = opts.windowType || 'popup';
|
|
return OAUTH3.discover(providerUri, opts).then(function (directives) {
|
|
return OAUTH3._implicitGrant(directives, opts).then(function (tokens) {
|
|
return tokens;
|
|
});
|
|
});
|
|
}
|
|
, _implicitGrant: function(directives, opts) {
|
|
// TODO this may need to be synchronous for browser security policy
|
|
// Do some stuff
|
|
var authReq = OAUTH3.urls.implicitGrant(
|
|
directives
|
|
, { redirect_uri: opts.redirect_uri
|
|
, client_id: opts.client_id || opts.client_uri
|
|
, client_uri: opts.client_uri || opts.client_id
|
|
, scope: opts.scope
|
|
, subject: opts.subject
|
|
, state: opts._state || undefined
|
|
, debug: opts.debug
|
|
}
|
|
);
|
|
|
|
if (opts.debug) {
|
|
window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!");
|
|
}
|
|
|
|
return OAUTH3._browser.frameRequest(
|
|
OAUTH3.url.resolve(directives.issuer, authReq.url)
|
|
, authReq.state // state should recycle params
|
|
, { windowType: opts.windowType
|
|
, reuseWindow: opts.broker && '-broker'
|
|
, debug: opts.debug
|
|
}
|
|
).then(function (tokens) {
|
|
if (tokens.error) {
|
|
// TODO directives.audience
|
|
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, tokens));
|
|
}
|
|
|
|
return tokens;
|
|
});
|
|
}
|
|
, _refreshToken: function (providerUri, opts) {
|
|
return OAUTH3.discover(providerUri, opts).then(function (directives) {
|
|
var prequest = OAUTH3.urls.refreshToken(directives, opts);
|
|
|
|
prequest.url = OAUTH3.url.resolve(directives.api, prequest.url);
|
|
return OAUTH3.request(prequest/*, { directives: directive }*/).then(function (req) {
|
|
var data = req.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
|
|
);
|
|
});
|
|
});
|
|
}
|
|
, logout: function(issuerUri, opts) {
|
|
var directives;
|
|
if ('string' !== typeof issuerUri) {
|
|
directives = issuerUri;
|
|
return OAUTH3._logoutHelper(directives, opts);
|
|
}
|
|
|
|
return OAUTH3.hooks.directives.get(issuerUri).then(function (directives) {
|
|
return OAUTH3._logoutHelper(directives, opts);
|
|
});
|
|
}
|
|
, _logoutHelper: function(directives, opts) {
|
|
var issuerUri = directives.issuer_uri || directives.provider_uri;
|
|
var logoutReq = OAUTH3.urls.logout(
|
|
directives
|
|
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
|
|
, windowType: 'popup' // TODO: figure out background later
|
|
, broker: opts.broker
|
|
//, state: opts._state
|
|
, debug: opts.debug
|
|
}
|
|
);
|
|
|
|
return OAUTH3._browser.frameRequest(
|
|
OAUTH3.url.resolve(directives.issuer, logoutReq.url)
|
|
, logoutReq.state // state should recycle params
|
|
, { windowType: 'popup'
|
|
, reuseWindow: opts.broker && '-broker'
|
|
, debug: opts.debug
|
|
}
|
|
).then(function (params) {
|
|
OAUTH3._browser.closeFrame(params.state || opts._state, opts);
|
|
|
|
if (params.error) {
|
|
// TODO directives.audience
|
|
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*issuerUri*/, params));
|
|
}
|
|
|
|
OAUTH3.hooks.session.clear(issuerUri);
|
|
return params;
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
// Let the Code Waste begin!!
|
|
//
|
|
, _browser: {
|
|
window: 'undefined' !== typeof window ? window : null
|
|
, rpc: function(providerUri, opts) {
|
|
opts = opts || {};
|
|
providerUri = OAUTH3.url.normalize(providerUri);
|
|
|
|
// TODO SECURITY should we whitelist our own self?
|
|
if (OAUTH3.uri.normalize(providerUri).replace(/\/.*/, '') === OAUTH3.uri.normalize(OAUTH3._browser.window.location.hostname)) {
|
|
console.warn("It looks like you're a provider trying to run rpc on yourself,"
|
|
+ " so we we're just gonna use"
|
|
+ " OAUTH3.request({ method: 'GET', url: "
|
|
+ "'" + opts._pathname + "' })");
|
|
|
|
if (/localstorage/i.test(opts._scheme)) {
|
|
return OAUTH3.PromiseA.resolve(localStorage.getItem(opts._pathname));
|
|
}
|
|
else {
|
|
return OAUTH3.request({
|
|
method: 'GET'
|
|
, url: OAUTH3.url.normalize(providerUri) + '/' + opts._pathname // '/.well-known/oauth3/' + discoverFile
|
|
}).then(function (resp) {
|
|
return resp.data;
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!(opts.client_id || opts.client_uri || '').match(OAUTH3._browser.window.location.hostname)) {
|
|
console.warn("It looks like your client_id doesn't match your current window..."
|
|
+ " this probably won't end well");
|
|
console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname);
|
|
}
|
|
|
|
var discReq = OAUTH3.urls[opts._rpc || 'rpc'](
|
|
providerUri
|
|
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
|
|
, windowType: opts.broker && opts.windowType || 'background'
|
|
, broker: opts.broker
|
|
, state: opts._state || undefined
|
|
, debug: opts.debug
|
|
, _scheme: opts._scheme
|
|
, _pathname: opts._pathname
|
|
, _method: opts._method
|
|
}
|
|
);
|
|
opts._state = discReq.state;
|
|
//var discReq = OAUTH3.urls.rpc(providerUri, opts);
|
|
|
|
// hmm... we're gonna need a broker for this since switching windows is distracting,
|
|
// popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS
|
|
// eventually it should be the browser (and postMessage may be a viable option now), but whatever...
|
|
|
|
// TODO allow postMessage from providerUri in addition to callback
|
|
// TODO allow node to open a desktop browser window
|
|
opts._windowType = opts.windowType;
|
|
opts.windowType = opts.windowType || 'background';
|
|
return OAUTH3._browser.testPixel(providerUri).then(function () {
|
|
return OAUTH3._browser.frameRequest(
|
|
OAUTH3.url.resolve(providerUri, discReq.url)
|
|
, discReq.state
|
|
// why not just pass opts whole?
|
|
, { windowType: opts.windowType
|
|
, reuseWindow: opts.broker && '-broker'
|
|
, debug: opts.debug
|
|
}
|
|
).then(function (params) {
|
|
opts.windowType = opts._windowType;
|
|
|
|
// caller will call OAUTH3._browser.closeFrame(discReq.state, { debug: opts.debug || params.debug });
|
|
if (params.error) {
|
|
// TODO directives.issuer || directives.audience
|
|
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, params));
|
|
}
|
|
|
|
// TODO params should have response_type indicating json, binary, etc
|
|
var result;
|
|
try {
|
|
result = JSON.parse(OAUTH3._base64.decodeUrlSafe(params.data || params.result || params.directives));
|
|
} catch(e) {
|
|
result = params.data || params.result;
|
|
}
|
|
|
|
console.log('result:', result);
|
|
// caller will call OAUTH3.hooks.directives.set(providerUri, directives);
|
|
return result;
|
|
});
|
|
});
|
|
}
|
|
, request: function (preq, _sys) {
|
|
return new OAUTH3.PromiseA(function (resolve, reject) {
|
|
var xhr;
|
|
var headers = preq.headers || {};
|
|
var multipart;
|
|
|
|
try {
|
|
xhr = new XMLHttpRequest(_sys);
|
|
} catch(e) {
|
|
xhr = new XMLHttpRequest();
|
|
}
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
|
// nothing to do here
|
|
return;
|
|
}
|
|
|
|
var data, err;
|
|
if (xhr.status !== 200) {
|
|
err = new Error('bad status code: ' + xhr.status);
|
|
}
|
|
|
|
try {
|
|
data = JSON.parse(xhr.responseText);
|
|
} catch(e) {
|
|
data = xhr.responseText;
|
|
}
|
|
|
|
if (data.error) {
|
|
err = new Error(data.error.message || data.error_description || JSON.stringify(data.error));
|
|
Object.assign(err, data.error);
|
|
}
|
|
if (err) {
|
|
err._request = xhr;
|
|
err.status = xhr.status;
|
|
err.data = data;
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve({
|
|
_request: xhr
|
|
, headers: null // TODO
|
|
, data: data
|
|
, status: xhr.status
|
|
});
|
|
};
|
|
xhr.ontimeout = function () {
|
|
var err = new Error('ETIMEDOUT');
|
|
err.code = 'ETIMEDOUT';
|
|
reject(err);
|
|
};
|
|
|
|
if (preq.progress) {
|
|
xhr.upload.onprogress = function (ev) {
|
|
preq.progress({
|
|
loaded: ev.loaded
|
|
, total: ev.total
|
|
});
|
|
if (OAUTH3._digest) {
|
|
// $rootScope.$digest();
|
|
OAUTH3._digest();
|
|
}
|
|
};
|
|
}
|
|
xhr.open(preq.method || 'GET', preq.url, true);
|
|
// For assets.example.com/assets
|
|
xhr.withCredentials = true;
|
|
|
|
if (preq.timeout) {
|
|
xhr.timeout = preq.timeout;
|
|
}
|
|
if (preq.data) {
|
|
headers['Content-Type'] = 'application/json'; // TODO XXX TODO utf8
|
|
}
|
|
Object.keys(headers).forEach(function (key) {
|
|
xhr.setRequestHeader(key, headers[key]);
|
|
});
|
|
if (preq.multipart && !(preq.multipart instanceof window.FormData)) {
|
|
multipart = new window.FormData();
|
|
Object.keys(preq.multipart).forEach(function (key) {
|
|
multipart.append(key, preq.multipart[key]);
|
|
});
|
|
}
|
|
else {
|
|
multipart = preq.multipart;
|
|
}
|
|
|
|
if (multipart) {
|
|
xhr.send(multipart);
|
|
}
|
|
else {
|
|
xhr.send(JSON.stringify(preq.data));
|
|
}
|
|
});
|
|
}
|
|
, testPixel: function (targetUri) {
|
|
var url = OAUTH3.url.resolve(OAUTH3.url.normalize(targetUri), '.well-known/oauth3/clear.gif');
|
|
return new OAUTH3.PromiseA(function (resolve, reject) {
|
|
var img = document.createElement('img');
|
|
img.addEventListener('load', function () {
|
|
resolve();
|
|
});
|
|
img.addEventListener('error', function () {
|
|
var err = new Error("OAuth3 support not detected: '" + url + "' not found");
|
|
err.code = 'E_NOT_SUPPORTED';
|
|
reject(err);
|
|
});
|
|
// works with CSP
|
|
img.style.position = 'absolute';
|
|
img.style.left = '-2px';
|
|
img.style.bottom = '-2px';
|
|
img.className = 'js-oauth3-discover';
|
|
img.src = url;
|
|
document.body.appendChild(img);
|
|
console.log('img', img);
|
|
});
|
|
}
|
|
, frameRequest: function (url, state, opts) {
|
|
opts = opts || {};
|
|
var previousFrame = OAUTH3._browser._frames[state];
|
|
|
|
var windowType = opts.windowType;
|
|
if (!windowType) {
|
|
windowType = 'popup';
|
|
}
|
|
|
|
var timeout = opts.timeout;
|
|
if ('background' === windowType) {
|
|
if (!timeout) {
|
|
timeout = 7 * 1000;
|
|
}
|
|
}
|
|
|
|
return new OAUTH3.PromiseA(function (resolve, reject) {
|
|
// TODO periodically garbage collect expired handlers from window object
|
|
var tok;
|
|
|
|
function cleanup() {
|
|
delete window['--oauth3-callback-' + state];
|
|
clearTimeout(tok);
|
|
tok = null;
|
|
// the actual close is done later (by the caller) so that the window/frame
|
|
// can be reused or self-closes synchronously itself / by parent
|
|
// (probably won't ever happen, but that's a negotiable implementation detail)
|
|
}
|
|
|
|
|
|
window['--oauth3-callback-' + state] = function (params) {
|
|
resolve(params);
|
|
cleanup();
|
|
};
|
|
|
|
if (timeout) {
|
|
tok = setTimeout(function () {
|
|
var err = new Error(
|
|
"the '" + windowType + "' request did not complete within " + Math.round(timeout / 1000) + "s"
|
|
);
|
|
err.code = "E_TIMEOUT";
|
|
reject(err);
|
|
cleanup();
|
|
}, timeout);
|
|
}
|
|
|
|
setTimeout(function () {
|
|
if (!OAUTH3._browser._frames[state]) {
|
|
reject(new Error("TODO: open the iframe first and discover oauth3 directives before popup"));
|
|
cleanup();
|
|
}
|
|
}, 0);
|
|
|
|
if ('background' === windowType) {
|
|
if (previousFrame) {
|
|
previousFrame.location = url;
|
|
//promise = previousFrame.promise;
|
|
}
|
|
else {
|
|
OAUTH3._browser._frames[state] = OAUTH3._browser.iframe(url, state, opts);
|
|
}
|
|
} else if ('popup' === windowType) {
|
|
if (previousFrame) {
|
|
previousFrame.location = url;
|
|
if (opts.debug) {
|
|
previousFrame.focus();
|
|
}
|
|
}
|
|
else {
|
|
OAUTH3._browser._frames[state] = OAUTH3._browser.frame(url, state, opts);
|
|
}
|
|
} else if ('inline' === windowType) {
|
|
// callback function will never execute and would need to redirect back to current page
|
|
// rather than the callback.html
|
|
url += '&original_url=' + OAUTH3._browser.window.location.href;
|
|
OAUTH3._browser.window.location = url;
|
|
//promise = OAUTH3.PromiseA.resolve({ url: url });
|
|
return;
|
|
} else {
|
|
throw new Error("login framing method options.windowType="
|
|
+ opts.windowType + " not type yet implemented");
|
|
}
|
|
|
|
}).then(function (params) {
|
|
if (params.error) {
|
|
// TODO directives.issuer || directives.audience
|
|
return OAUTH3.PromiseA.reject(OAUTH3.error.parse('https://oauth3.org', params));
|
|
}
|
|
return params;
|
|
});
|
|
}
|
|
, closeFrame: function (state, opts) {
|
|
opts = opts || {};
|
|
function close() {
|
|
try {
|
|
OAUTH3._browser._frames[state].close();
|
|
} catch(e) {
|
|
try {
|
|
OAUTH3._browser._frames[state].remove();
|
|
} catch(e) {
|
|
console.error(new Error("Could not clase window/iframe. closeFrame may have been called twice."));
|
|
}
|
|
}
|
|
|
|
delete OAUTH3._browser._frames[state];
|
|
}
|
|
|
|
if (opts.debug) {
|
|
if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) {
|
|
close();
|
|
}
|
|
}
|
|
else {
|
|
close();
|
|
}
|
|
}
|
|
, _frames: {}
|
|
, iframe: function (url, state, opts) {
|
|
var framesrc = '<iframe class="js-oauth3-iframe" src="' + url + '" ';
|
|
if (opts.debug) {
|
|
framesrc += ' width="' + (opts.height || 600) + 'px"'
|
|
+ ' height="' + (opts.width || 720) + 'px"'
|
|
+ ' style="opacity: 0.8;" frameborder="1"';
|
|
}
|
|
else {
|
|
framesrc += ' width="1px" height="1px" frameborder="0"';
|
|
}
|
|
framesrc += '></iframe>';
|
|
|
|
var frame = OAUTH3._browser.window.document.createElement('div');
|
|
frame.innerHTML = framesrc;
|
|
OAUTH3._browser.window.document.body.appendChild(frame);
|
|
|
|
return frame;
|
|
}
|
|
, frame: function (url, state, opts) {
|
|
|
|
// TODO allow size changes (via directive even)
|
|
return window.open(
|
|
url
|
|
, 'oauth3-login-' + (opts.reuseWindow || state)
|
|
, 'height=' + (opts.height || 720) + ',width=' + (opts.width || 620)
|
|
);
|
|
}
|
|
}
|
|
, api: function (providerUri, opts) {
|
|
if (!OAUTH3.api[opts.api]) {
|
|
return OAUTH3.PromiseA.reject(new Error("No API for '" + opts.api + "'"));
|
|
}
|
|
|
|
return OAUTH3.api[opts.api](providerUri, opts);
|
|
}
|
|
, _pkgs: {}
|
|
, pkg: function (providerUri, pkgname, method, opts) {
|
|
if (!OAUTH3._pkgs[pkgname]) {
|
|
return OAUTH3.PromiseA.reject(new Error("No Package for '" + pkgname + "'"));
|
|
}
|
|
|
|
if (!OAUTH3._pkgs[pkgname][method]) {
|
|
return OAUTH3.PromiseA.reject(new Error("No method '" + method + "' in package '" + pkgname + "'"));
|
|
}
|
|
// opts = { audience: providerUri }
|
|
return OAUTH3._pkgs[pkgname][method](opts);
|
|
}
|
|
};
|
|
OAUTH3.login = OAUTH3.implicitGrant;
|
|
|
|
OAUTH3._defaultStorage = {
|
|
_getStorageKeys: function (prefix, storage) {
|
|
storage = storage || window.localStorage;
|
|
var matching = [];
|
|
var ind, key;
|
|
for (ind = 0; ind < storage.length; ind++) {
|
|
key = storage.key(ind);
|
|
if (key.indexOf(prefix || '') === 0) {
|
|
matching.push(key);
|
|
}
|
|
}
|
|
return matching;
|
|
}
|
|
, directives: {
|
|
prefix: 'directives-'
|
|
, get: function (providerUri) {
|
|
var result = JSON.parse(window.localStorage.getItem(this.prefix + providerUri) || '{}');
|
|
return OAUTH3.PromiseA.resolve(result);
|
|
}
|
|
, set: function (providerUri, directives) {
|
|
window.localStorage.setItem(this.prefix + providerUri, JSON.stringify(directives));
|
|
return this.get(providerUri);
|
|
}
|
|
, all: function () {
|
|
var prefix = this.prefix;
|
|
var result = {};
|
|
OAUTH3._defaultStorage._getStorageKeys(prefix).forEach(function (key) {
|
|
result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || '{}');
|
|
});
|
|
return OAUTH3.PromiseA.resolve(result);
|
|
}
|
|
, clear: function () {
|
|
OAUTH3._defaultStorage._getStorageKeys(this.prefix).forEach(function (key) {
|
|
window.localStorage.removeItem(key);
|
|
});
|
|
return OAUTH3.PromiseA.resolve();
|
|
}
|
|
}
|
|
, sessions: {
|
|
prefix: 'session-'
|
|
, get: function (providerUri, id) {
|
|
var result;
|
|
if (id) {
|
|
result = JSON.parse(window.sessionStorage.getItem(this.prefix + providerUri+id) || 'null');
|
|
} else {
|
|
result = JSON.parse(window.sessionStorage.getItem(this.prefix + providerUri) || 'null');
|
|
}
|
|
return OAUTH3.PromiseA.resolve(result);
|
|
}
|
|
, set: function (providerUri, newSession, id) {
|
|
var str = JSON.stringify(newSession);
|
|
window.sessionStorage.setItem(this.prefix + providerUri, str);
|
|
id = id || newSession.id || newSession.token.sub || newSession.token.id;
|
|
if (id) {
|
|
window.sessionStorage.setItem(this.prefix + providerUri + id, str);
|
|
}
|
|
return this.get(providerUri, id);
|
|
}
|
|
, all: function (providerUri) {
|
|
var prefix = this.prefix + (providerUri || '');
|
|
var result = {};
|
|
OAUTH3._defaultStorage._getStorageKeys(prefix, window.sessionStorage).forEach(function (key) {
|
|
result[key.replace(prefix, '')] = JSON.parse(window.sessionStorage.getItem(key) || 'null');
|
|
});
|
|
return OAUTH3.PromiseA.resolve(result);
|
|
}
|
|
, clear: function (providerUri) {
|
|
var prefix = this.prefix + (providerUri || '');
|
|
OAUTH3._defaultStorage._getStorageKeys(prefix, window.sessionStorage).forEach(function (key) {
|
|
window.sessionStorage.removeItem(key);
|
|
});
|
|
return OAUTH3.PromiseA.resolve();
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO get rid of these
|
|
OAUTH3.utils = {
|
|
clientUri: OAUTH3.clientUri
|
|
, query: OAUTH3.query
|
|
, parseSubject: function (sub) {
|
|
var parts = sub.split('@');
|
|
var issuer;
|
|
var subject;
|
|
|
|
if (/@/.test(sub)) {
|
|
// The username may have a single @, the provider may not
|
|
// user@thing.com@whatever.com -> user@thing.com, whatever.com
|
|
issuer = parts.pop();
|
|
subject = parts.join('@');
|
|
} else {
|
|
//subject = '';
|
|
issuer = parts.join('@');
|
|
}
|
|
|
|
return { subject: subject, issuer: issuer };
|
|
}
|
|
, scope: OAUTH3.scope
|
|
, uri: OAUTH3.uri
|
|
, url: OAUTH3.url
|
|
, _error: OAUTH3.error
|
|
, _formatError: OAUTH3.error
|
|
, _urlSafeBase64ToBase64: OAUTH3._urlSafeBase64ToBase64
|
|
, randomState: OAUTH3.randomState
|
|
, _insecureRandomState: OAUTH3._insecureRandomState
|
|
};
|
|
|
|
if ('undefined' !== typeof Promise) {
|
|
OAUTH3.PromiseA = Promise;
|
|
}
|
|
|
|
// this is not necessary, but it's relatively small
|
|
// and gives people the 3-line examples they love so much
|
|
OAUTH3.create = function (location, opts) {
|
|
if (!location) {
|
|
location = OAUTH3._browser.window.location;
|
|
}
|
|
|
|
var result = {
|
|
_clientUri: OAUTH3.clientUri(location)
|
|
, _identityProviderUri: null
|
|
, _resourceProviderUri: null
|
|
, _identityProviderDirectives: null
|
|
, _resourceProviderDirectives: null
|
|
//, _resourceProviderMap: null // map between xyz.com and domains@oauth3.org
|
|
, _init: function (location, opts) {
|
|
var me = this;
|
|
if (!opts) {
|
|
opts = location;
|
|
}
|
|
if (location && location.location) {
|
|
location = location.location;
|
|
}
|
|
if (opts && opts.location) {
|
|
me._clientUri = OAUTH3.clientUri(opts.location);
|
|
}
|
|
if (location && (location.host || location.hostname)) {
|
|
me._clientUri = OAUTH3.clientUri(location);
|
|
}
|
|
if (opts) {
|
|
if (opts.providerUri) {
|
|
me._identityProviderUri = opts.providerUri;
|
|
me._resourceProviderUri = opts.providerUri;
|
|
}
|
|
if (opts.issuer || opts.identityProviderUri) {
|
|
me._identityProviderUri = opts.issuer || opts.identityProviderUri;
|
|
}
|
|
if (opts.audience || opts.resourceProviderUri) {
|
|
me._resourceProviderUri = opts.audience || opts.resourceProviderUri;
|
|
}
|
|
if (opts.session) {
|
|
if (!me._identityProviderUri) {
|
|
throw new Error("'providerUri' was not supplied");
|
|
}
|
|
opts.session.provider_uri = me._identityProviderUri;
|
|
opts.session.client_uri = me._clientUri;
|
|
me.session(opts.session, opts.sessionId);
|
|
}
|
|
}
|
|
}
|
|
, _initClient: function () {
|
|
var me = this;
|
|
return OAUTH3.discover(me._clientUri, { client_id: me._clientUri }).then(function (clientDirectives) {
|
|
me._clientDirectives = clientDirectives;
|
|
return clientDirectives;
|
|
});
|
|
}
|
|
, init: function (location/*, opts*/) {
|
|
var me = this;
|
|
var p1 = OAUTH3.PromiseA.resolve();
|
|
var p2 = OAUTH3.PromiseA.resolve();
|
|
|
|
me._init(location, opts);
|
|
|
|
if (me._identityProviderUri) {
|
|
// returns directives
|
|
p1 = me.setIssuer(me._identityProviderUri);
|
|
}
|
|
if (me._resourceProviderUri) {
|
|
// returns directives
|
|
p2 = me.setAudience(me._resourceProviderUri);
|
|
}
|
|
|
|
return p1.then(function () {
|
|
return p2.then(function () {
|
|
return me._initClient();
|
|
});
|
|
});
|
|
}
|
|
, setProvider: function (providerUri) {
|
|
var me = this;
|
|
return me._initClient().then(function () {
|
|
return me.setIdentityProvider(providerUri).then(function () {
|
|
// TODO how to say "Use xyz.com for domains@oauth3.org, but abc.com for dns@oauth3.org"?
|
|
return me.setResourceProvider(providerUri);
|
|
});
|
|
});
|
|
}
|
|
, setIdentityProvider: function (providerUri) {
|
|
var me = this;
|
|
me._identityProviderUri = providerUri;
|
|
return me._initClient().then(function () {
|
|
// this should be synchronous the second time around
|
|
return OAUTH3.discover(me._identityProviderUri, { client_id: me._clientUri }).then(function (directives) {
|
|
me._identityProviderDirectives = directives;
|
|
return directives;
|
|
});
|
|
});
|
|
}
|
|
, setResourceProvider: function (providerUri) {
|
|
var me = this;
|
|
me._resourceProviderUri = providerUri;
|
|
return me._initClient().then(function () {
|
|
// this should be synchronous the second time around
|
|
return OAUTH3.discover(me._resourceProviderUri, { client_id: me._clientUri }).then(function (directives) {
|
|
me._resourceProviderDirectives = directives;
|
|
return directives;
|
|
});
|
|
});
|
|
}
|
|
, checkSession: function () {
|
|
return OAUTH3.hooks.session.get(this._identityProviderUri);
|
|
}
|
|
, login: function (opts) {
|
|
var me = this;
|
|
return OAUTH3.hooks.session.get(me._identityProviderUri).then(function (session) {
|
|
if (session) {
|
|
me._session = true;
|
|
return session;
|
|
}
|
|
|
|
opts = opts || {};
|
|
opts.client_uri = me._clientUri;
|
|
|
|
return OAUTH3.implicitGrant(me._identityProviderDirectives, opts).then(function (session) {
|
|
me._session = true;
|
|
return session;
|
|
});
|
|
});
|
|
}
|
|
, session: function (session, id) {
|
|
if (!session) {
|
|
return OAUTH3.hooks.session.get(this._identityProviderUri);
|
|
}
|
|
return OAUTH3.hooks.session.set(this._identityProviderUri, session, id);
|
|
}
|
|
, request: function (preq, opts) {
|
|
opts = opts || {};
|
|
preq.client_uri = this._clientUri;
|
|
preq.client_id = this._clientUri;
|
|
preq.method = preq.method || 'GET';
|
|
// TODO maybe use a baseUrl from the directives file?
|
|
preq.url = OAUTH3.url.resolve(this._resourceProviderUri, preq.url);
|
|
|
|
if (preq.session || !this._session) {
|
|
return OAUTH3.request(preq, opts);
|
|
}
|
|
|
|
return this.session().then(function (session) {
|
|
preq.session = session;
|
|
return OAUTH3.request(preq, opts);
|
|
});
|
|
}
|
|
, logout: function (opts) {
|
|
var me = this;
|
|
me._session = false;
|
|
opts = opts || {};
|
|
return OAUTH3.hooks.session.get(me._identityProviderUri).then(function (session) {
|
|
opts.client_uri = me._clientUri;
|
|
opts.client_id = me._clientUri;
|
|
opts.session = session;
|
|
|
|
return OAUTH3.logout(me._identityProviderUri, opts);
|
|
});
|
|
}
|
|
, api: function (api, opts) {
|
|
var me = this;
|
|
opts = opts || {};
|
|
return OAUTH3.hooks.session.get(me._identityProviderUri).then(function (session) {
|
|
opts.api = api;
|
|
opts.session = session;
|
|
|
|
return OAUTH3.api(me._resourceProviderDirectives.api, opts);
|
|
});
|
|
}
|
|
, pkg: function (pkgname) {
|
|
var me = this;
|
|
var issuer = me._identityProviderUri;
|
|
var audience = me._resourceProviderDirectives.api;
|
|
var pkg;
|
|
var result = {};
|
|
|
|
// TODO dynamically load package? https://publisher.com/.well-known/packages.js@oauth3.org/pkg@publisher.com.json
|
|
if (!OAUTH3._pkgs[pkgname]) {
|
|
return OAUTH3.PromiseA.reject(new Error("No Package for '" + pkgname + "'"));
|
|
}
|
|
|
|
return OAUTH3.hooks.session.get(issuer).then(function (session) {
|
|
pkg = OAUTH3._pkgs[pkgname];
|
|
Object.keys(pkg).forEach(function (key) {
|
|
result[key] = function (opts) {
|
|
opts = opts || {};
|
|
opts.session = session;
|
|
opts.audience = audience;
|
|
return pkg[key](opts);
|
|
};
|
|
});
|
|
|
|
return result;
|
|
});
|
|
}
|
|
};
|
|
result.setIssuer = result.setIdentityProvider;
|
|
result.setAudience = result.setResourceProvider;
|
|
result.authenticate = result.login;
|
|
result.authorize = result.login;
|
|
result.expire = result.logout;
|
|
|
|
result._init(location, opts);
|
|
|
|
return result;
|
|
};
|
|
|
|
}('undefined' !== typeof exports ? exports : window));
|