This commit is contained in:
AJ ONeal 2017-11-09 15:25:19 -07:00
parent db284fbf91
commit dedd851ff9
10 changed files with 774 additions and 429 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
prefactor
.well-known
node_modules/ node_modules/
DS_Store DS_Store
.vscode .vscode

View File

@ -19,13 +19,13 @@ If you have no idea what you're doing
1. Create a folder for your project named after your app, such as `example.com/` 1. Create a folder for your project named after your app, such as `example.com/`
2. Inside of the folder `example.com/` a folder called `assets/` 2. Inside of the folder `example.com/` a folder called `assets/`
3. Inside of the folder `example.com/assets` a folder called `org.oauth3/` 3. Inside of the folder `example.com/assets` a folder called `oauth3.org/`
4. Download [oauth3.js-v1.zip](https://git.daplie.com/OAuth3/oauth3.js/repository/archive.zip?ref=v1) 4. Download [oauth3.js-v1.zip](https://git.daplie.com/OAuth3/oauth3.js/repository/archive.zip?ref=v1)
5. Double-click to unzip the folder. 5. Double-click to unzip the folder.
6. Copy the file `oauth3.core.js` into the folder `example.com/assets/org.oauth3/` 6. Copy the file `oauth3.core.js` into the folder `example.com/assets/oauth3.org/`
7. Copy the folder `well-known` into the folder `example.com/` 7. Copy the folder `well-known` into the folder `example.com/`
8. Rename the folder `well-known` to `.well-known` (when you do this, it become invisible, that's okay) 8. Rename the folder `well-known` to `.well-known` (when you do this, it become invisible, that's okay)
9. Add `<script src="assets/org.oauth3/oauth3.core.js"></script>` to your `index.html` 9. Add `<script src="assets/oauth3.org/oauth3.core.js"></script>` to your `index.html`
9. Add `<script src="app.js"></script>` to your `index.html` 9. Add `<script src="app.js"></script>` to your `index.html`
10. Create files in `example.com` called `app.js` and `index.html` and put this in it: 10. Create files in `example.com` called `app.js` and `index.html` and put this in it:
@ -44,7 +44,7 @@ If you have no idea what you're doing
<script src="https://code.jquery.com/jquery-3.1.1.js" <script src="https://code.jquery.com/jquery-3.1.1.js"
integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA=" integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA="
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="assets/org.oauth3/oauth3.core.js"></script> <script src="assets/oauth3.org/oauth3.core.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
</html> </html>
@ -81,7 +81,7 @@ function onClickLogin() {
console.info('Secure PPID (aka subject):', session.token.sub); console.info('Secure PPID (aka subject):', session.token.sub);
return oauth3.request({ return oauth3.request({
url: 'https://oauth3.org/api/issuer@oauth3.org/inspect_token' url: 'https://oauth3.org/api/issuer@oauth3.org/inspect'
, session: session , session: session
}).then(function (resp) { }).then(function (resp) {

View File

@ -131,8 +131,8 @@ parseArgs(process.argv, {
// authn / authz // authn / authz
, [ 'devices', 'manages devices for your account(s)' ] , [ 'devices', 'manages devices for your account(s)' ]
, [ 'devices:new', 'create a new device (default name is hostname, default ip is the result of :provider/api/org.oauth3.tunnel/checkip)'.replace(/\b:provider\b/, defaults.provider) ] , [ 'devices:new', 'create a new device (default name is hostname, default ip is the result of :provider/api/tunnel@oauth3.org/checkip)'.replace(/\b:provider\b/, defaults.provider) ]
, [ 'devices:set', 'set the ip address of the device (defaults ip is the result of :provider/api/org.oauth3.tunnel/checkip)'.replace(/\b:provider\b/, defaults.provider) ] , [ 'devices:set', 'set the ip address of the device (defaults ip is the result of :provider/api/tunnel@oauth3.org/checkip)'.replace(/\b:provider\b/, defaults.provider) ]
, [ 'devices:attach', "attach a device to a domain's DNS record" ] , [ 'devices:attach', "attach a device to a domain's DNS record" ]
, [ 'devices:detach', "detach an account from a domain's DNS record" ] , [ 'devices:detach', "detach an account from a domain's DNS record" ]
, [ 'devices:select', '(re)claim the specified device as this device (i.e. you re-installed your OS or deleted your ~/.oauth3)' ] , [ 'devices:select', '(re)claim the specified device as this device (i.e. you re-installed your OS or deleted your ~/.oauth3)' ]

View File

@ -78,7 +78,7 @@
, uri: { , uri: {
normalize: function (uri) { normalize: function (uri) {
if ('string' !== typeof uri) { if ('string' !== typeof uri) {
console.error((new Error('stack')).stack); throw new Error("attempted to normalize non-string URI: "+JSON.stringify(uri));
} }
// tested with // tested with
// example.com // example.com
@ -94,7 +94,7 @@
, url: { , url: {
normalize: function (url) { normalize: function (url) {
if ('string' !== typeof url) { if ('string' !== typeof url) {
console.error((new Error('stack')).stack); throw new Error("attempted to normalize non-string URL: "+JSON.stringify(url));
} }
// tested with // tested with
// example.com // example.com
@ -168,9 +168,12 @@
} }
} }
, scope: { , scope: {
stringify: function (scope) { parse: function (scope) {
return (scope||'').split(/[+, ]+/g);
}
, stringify: function (scope) {
if (Array.isArray(scope)) { if (Array.isArray(scope)) {
scope = scope.join(' '); scope = scope.join(',');
} }
return scope; return scope;
} }
@ -204,40 +207,90 @@
} }
, jwt: { , jwt: {
// decode only (no verification) // decode only (no verification)
decode: function (str) { decode: function (token, opts) {
// 'abc.qrs.xyz' // 'abc.qrs.xyz'
// [ 'abc', 'qrs', 'xyz' ] // [ 'abc', 'qrs', 'xyz' ]
// [ {}, {}, 'foo' ] // {}
// { header: {}, payload: {}, signature: '' } var parts = token.split(/\./g);
var parts = str.split(/\./g); var err;
var jsons = parts.slice(0, 2).map(function (urlsafe64) { if (parts.length !== 3) {
return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64)); err = new Error("Invalid JWT: required 3 '.' separated components not "+parts.length);
}); err.code = 'E_INVALID_JWT';
throw err;
}
return { header: jsons[0], payload: jsons[1] }; 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 (jwk, token) { , verify: function (token, jwk) {
if (!OAUTH3.crypto) {
return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable"));
}
jwk = jwk.publicKey || jwk;
var parts = token.split(/\./g); var parts = token.split(/\./g);
var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.')); var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.'));
var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]); var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]);
return OAUTH3.crypto.core.verify(jwk, data, signature); return OAUTH3.crypto.core.verify(jwk, data, signature).then(function () {
return OAUTH3.jwt.decode(token);
});
} }
, freshness: function (tokenMeta, staletime, _now) { , sign: function (payload, jwk) {
staletime = staletime || (15 * 60); if (!OAUTH3.crypto) {
var now = _now || Date.now(); return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable"));
var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000)); }
jwk = jwk.private_key || jwk.privateKey || jwk;
if (fresh >= staletime) { 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'; return 'fresh';
} }
if (fresh <= 0) { staletime = staletime || (15 * 60);
return 'expired'; 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';
} }
return 'stale';
} }
} }
, urls: { , urls: {
@ -275,7 +328,7 @@
// Example Implicit Grant Request // Example Implicit Grant Request
// (for generating a browser-only session, not a session on your server) // (for generating a browser-only session, not a session on your server)
// //
// GET https://example.com/api/org.oauth3.provider/authorization_dialog // GET https://example.com/api/issuer@oauth3.org/authorization_dialog
// ?response_type=token // ?response_type=token
// &scope=`encodeURIComponent('profile.login profile.email')` // &scope=`encodeURIComponent('profile.login profile.email')`
// &state=`cryptoutil.random().toString('hex')` // &state=`cryptoutil.random().toString('hex')`
@ -341,29 +394,36 @@
// , "username": "<<username>>", "password": "password" } // , "username": "<<username>>", "password": "password" }
// //
opts = opts || {}; opts = opts || {};
var type = 'access_token'; var refresh_token = opts.refresh_token || (opts.session && opts.session.refresh_token);
var grantType = '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 scope = opts.scope || directive.authn_scope;
var clientSecret = opts.client_secret; var args = directive.access_token;
var args = directive[type];
var params = { var params = {
"grant_type": grantType "grant_type": 'refresh_token'
, "refresh_token": opts.refresh_token || (opts.session && opts.session.refresh_token) , "refresh_token": refresh_token
, "response_type": 'token' , "response_type": 'token'
, "client_id": opts.client_id || opts.client_uri , "client_id": opts.client_id || opts.client_uri
, "client_uri": opts.client_uri , "client_uri": opts.client_uri
//, "scope": undefined
//, "client_secret": undefined
, debug: opts.debug || undefined , debug: opts.debug || undefined
}; };
var uri = args.url; var uri = args.url;
var body; var body;
if (clientSecret) { if (opts.client_secret) {
// TODO not allowed in the browser // TODO not allowed in the browser
console.warn("if this is a browser, you must not use client_secret"); console.warn("if this is a browser, you must not use client_secret");
params.client_secret = clientSecret; params.client_secret = opts.client_secret;
} }
if (scope) { if (scope) {
@ -430,42 +490,44 @@
} }
} }
, hooks: { , hooks: {
directives: { _checkStorage: function (grpName, funcName) {
get: function (providerUri) { if (!OAUTH3._hooks) {
providerUri = OAUTH3.uri.normalize(providerUri); OAUTH3._hooks = {};
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._getCached(providerUri)
|| OAUTH3.hooks.directives._get(providerUri))
.then(function (directives) {
// or do .then(this._set) to keep DRY?
OAUTH3.hooks.directives._cache[providerUri] = directives;
return directives;
});
} }
, _getCached: function (providerUri) { if (!OAUTH3._hooks[grpName]) {
providerUri = OAUTH3.uri.normalize(providerUri); console.log('using default storage for '+grpName+', set OAUTH3._hooks.'+grpName+' for custom storage');
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } OAUTH3._hooks[grpName] = OAUTH3._defaultStorage[grpName];
return OAUTH3.hooks.directives._cache[providerUri]; }
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) { , set: function (providerUri, directives) {
providerUri = OAUTH3.uri.normalize(providerUri); OAUTH3.hooks._checkStorage('directives', 'set');
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
OAUTH3.hooks.directives._cache[providerUri] = directives; if (!providerUri) {
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._set(providerUri, directives)); throw new Error("providerUri is not set");
}
, _get: function (providerUri) {
if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.get) {
console.warn('[Warn] Please implement OAUTH3._hooks.directives.get = function (providerUri) { return PromiseA<directives>; }');
return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}');
} }
return OAUTH3._hooks.directives.get(providerUri); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.set(OAUTH3.uri.normalize(providerUri), directives));
} }
, _set: function (providerUri, directives) { , all: function () {
if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.set) { OAUTH3.hooks._checkStorage('directives', 'all');
console.warn('[Warn] Please implement OAUTH3._hooks.directives.set = function (providerUri, directives) { return PromiseA<directives>; }');
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives)); return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.all());
return directives; }
} , clear: function () {
return OAUTH3._hooks.directives.set(providerUri, directives); OAUTH3.hooks._checkStorage('directives', 'clear');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.clear());
} }
} }
, session: { , session: {
@ -485,7 +547,7 @@
oldSession.client_uri = clientUri; // azp oldSession.client_uri = clientUri; // azp
// info about the newly-discovered token // info about the newly-discovered token
oldSession.token = OAUTH3.jwt.decode(oldSession.access_token).payload; oldSession.token = OAUTH3.jwt.decode(oldSession.access_token);
oldSession.token.sub = oldSession.token.sub oldSession.token.sub = oldSession.token.sub
|| (oldSession.token.acx||{}).id || (oldSession.token.acx||{}).id
@ -496,7 +558,7 @@
oldSession.token.provider_uri = providerUri; oldSession.token.provider_uri = providerUri;
if (oldSession.refresh_token) { if (oldSession.refresh_token) {
oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token).payload; oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token);
oldSession.refresh.sub = oldSession.refresh.sub oldSession.refresh.sub = oldSession.refresh.sub
|| (oldSession.refresh.acx||{}).id || (oldSession.refresh.acx||{}).id
|| ((oldSession.refresh.axs||[])[0]||{}).appScopedId || ((oldSession.refresh.axs||[])[0]||{}).appScopedId
@ -506,7 +568,7 @@
} }
// set for a set of audiences // set for a set of audiences
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.set(providerUri, oldSession)); return OAUTH3.hooks.session.set(providerUri, oldSession);
} }
, check: function (preq, opts) { , check: function (preq, opts) {
opts = opts || {}; opts = opts || {};
@ -559,62 +621,40 @@
return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession); return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession);
}); });
} }
, _getCached: function (providerUri, id) {
providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; }
if (id) {
return OAUTH3.hooks.session._cache[providerUri + id];
}
return OAUTH3.hooks.session._cache[providerUri];
}
, set: function (providerUri, newSession, id) { , set: function (providerUri, newSession, id) {
OAUTH3.hooks._checkStorage('sessions', 'set');
if (!providerUri) { if (!providerUri) {
console.error(new Error('no providerUri').stack); console.error(new Error('no providerUri').stack);
throw new Error("providerUri is not set"); throw new Error("providerUri is not set");
} }
providerUri = OAUTH3.uri.normalize(providerUri); providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; } return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.set(providerUri, newSession, id));
OAUTH3.hooks.session._cache[providerUri + (id || newSession.id || newSession.token.id || '')] = newSession;
if (!id) {
OAUTH3.hooks.session._cache[providerUri] = newSession;
}
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session._set(providerUri, newSession));
} }
, get: function (providerUri, id) { , get: function (providerUri, id) {
providerUri = OAUTH3.uri.normalize(providerUri); OAUTH3.hooks._checkStorage('sessions', 'get');
if (!providerUri) { if (!providerUri) {
throw new Error("providerUri is not set"); 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');
return OAUTH3.PromiseA.resolve( if (providerUri) {
OAUTH3.hooks.session._getCached(providerUri, id) || OAUTH3.hooks.session._get(providerUri, id) providerUri = OAUTH3.uri.normalize(providerUri);
).then(function (session) { }
var s = session || { token: {} }; return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.all(providerUri));
OAUTH3.hooks.session._cache[providerUri + (id || s.id || s.token.id || '')] = session;
if (!id) {
OAUTH3.hooks.session._cache[providerUri] = session;
}
return session;
});
} }
, _get: function (providerUri, id) { , clear: function (providerUri) {
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.all) { OAUTH3.hooks._checkStorage('sessions', 'clear');
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.all = function ([providerUri]) { return PromiseA<sessions>; }');
if (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
} }
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.get) { return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.clear(providerUri));
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.get = function (providerUri[, id]) { return PromiseA<session>; }');
return JSON.parse(window.sessionStorage.getItem('session-' + providerUri + (id || '')) || 'null');
}
return OAUTH3._hooks.sessions.get(providerUri, id);
}
, _set: function (providerUri, newSession, id) {
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.set) {
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.set = function (providerUri, newSession[, id]) { return PromiseA<newSession>; }');
window.sessionStorage.setItem('session-' + providerUri, JSON.stringify(newSession));
window.sessionStorage.setItem('session-' + providerUri + (id || newSession.id || newSession.token.id || ''), JSON.stringify(newSession));
return newSession;
}
return OAUTH3._hooks.sessions.set(providerUri, newSession, id);
} }
} }
} }
@ -803,7 +843,7 @@
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params)); return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params));
} }
OAUTH3.hooks.session._cache = {}; OAUTH3.hooks.session.clear();
return params; return params;
}); });
} }
@ -1123,6 +1163,82 @@
}; };
OAUTH3.login = OAUTH3.implicitGrant; 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 // TODO get rid of these
OAUTH3.utils = { OAUTH3.utils = {
clientUri: OAUTH3.clientUri clientUri: OAUTH3.clientUri
@ -1154,7 +1270,7 @@
, _resourceProviderUri: null , _resourceProviderUri: null
, _identityProviderDirectives: null , _identityProviderDirectives: null
, _resourceProviderDirectives: null , _resourceProviderDirectives: null
//, _resourceProviderMap: null // map between xyz.com and org.oauth3.domains //, _resourceProviderMap: null // map between xyz.com and domains@oauth3.org
, _init: function (location, opts) { , _init: function (location, opts) {
var me = this; var me = this;
if (!opts) { if (!opts) {
@ -1223,7 +1339,7 @@
var me = this; var me = this;
return me._initClient().then(function () { return me._initClient().then(function () {
return me.setIdentityProvider(providerUri).then(function () { return me.setIdentityProvider(providerUri).then(function () {
// TODO how to say "Use xyz.com for org.oauth3.domains, but abc.com for org.oauth3.dns"? // TODO how to say "Use xyz.com for domains@oauth3.org, but abc.com for dns@oauth3.org"?
return me.setResourceProvider(providerUri); return me.setResourceProvider(providerUri);
}); });
}); });

View File

@ -1,5 +1,5 @@
;(function (exports) { ;(function (exports) {
'use strict'; 'use strict';
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
@ -82,6 +82,7 @@
}; };
function checkWebCrypto() { function checkWebCrypto() {
/* global OAUTH3_crypto_fallback */
var loadFallback = function() { var loadFallback = function() {
var prom; var prom;
loadFallback = function () { return prom; }; loadFallback = function () { return prom; };
@ -96,17 +97,16 @@
resolve(); resolve();
} }
}; };
script.src = '/assets/org.oauth3/oauth3.crypto.fallback.js'; script.src = '/assets/oauth3.org/oauth3.crypto.fallback.js';
body.appendChild(script); body.appendChild(script);
}); });
return prom; return prom;
}; };
function checkException(name, func) { function checkException(name, func) {
new OAUTH3.PromiseA(function (resolve) { resolve(func()); }) OAUTH3.PromiseA.resolve().then(func)
.then(function () { .then(function () {
OAUTH3.crypto.core[name] = webCrypto[name]; OAUTH3.crypto.core[name] = webCrypto[name];
}) }, function (err) {
.catch(function (err) {
console.warn('error with WebCrypto', name, '- using fallback', err); console.warn('error with WebCrypto', name, '- using fallback', err);
loadFallback().then(function () { loadFallback().then(function () {
OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name]; OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
@ -195,101 +195,61 @@
.then(OAUTH3._base64.bufferToUrlSafe); .then(OAUTH3._base64.bufferToUrlSafe);
}; };
OAUTH3.crypto._createKey = function (ppid) { OAUTH3.crypto.createKeyPair = function () {
var saltProm = OAUTH3.crypto.core.randomBytes(16); // TODO: maybe support other types of key pairs, not just ECDSA P-256
var kekProm = saltProm.then(function (salt) { return OAUTH3.crypto.core.genEcdsaKeyPair().then(function (keyPair) {
return OAUTH3.crypto.core.pbkdf2(ppid, salt);
});
var ecdsaProm = OAUTH3.crypto.core.genEcdsaKeyPair()
.then(function (keyPair) {
return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) { return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) {
keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256'; keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256';
keyPair.privateKey.kid = keyPair.publicKey.kid = kid; keyPair.privateKey.kid = keyPair.publicKey.kid = kid;
return keyPair; return keyPair;
}); });
}); });
};
OAUTH3.crypto.encryptKeyPair = function (keyPair, password) {
var saltProm = OAUTH3.crypto.core.randomBytes(16);
var kekProm = saltProm.then(function (salt) {
return OAUTH3.crypto.core.pbkdf2(password, salt);
});
return OAUTH3.PromiseA.all([ return OAUTH3.PromiseA.all([
kekProm kekProm
, ecdsaProm
, saltProm , saltProm
, OAUTH3.crypto.core.randomBytes(16)
, OAUTH3.crypto.core.randomBytes(12) , OAUTH3.crypto.core.randomBytes(12)
, OAUTH3.crypto.core.randomBytes(12) , ]).then(function (results) {
]).then(function (results) {
var kek = results[0]; var kek = results[0];
var keyPair = results[1]; var salt = results[1];
var salt = results[2]; var ecdsaIv = results[2];
var userSecret = results[3];
var ecdsaIv = results[4];
var secretIv = results[5];
return OAUTH3.PromiseA.all([ var privKeyBuf = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey));
OAUTH3.crypto.core.encrypt(kek, ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey))) return OAUTH3.crypto.core.encrypt(kek, ecdsaIv, privKeyBuf).then(function (encrypted) {
, OAUTH3.crypto.core.encrypt(kek, secretIv, userSecret)
])
.then(function (encrypted) {
return { return {
publicKey: keyPair.publicKey publicKey: keyPair.publicKey
, privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0]) , privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted)
, userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1])
, salt: OAUTH3._base64.bufferToUrlSafe(salt) , salt: OAUTH3._base64.bufferToUrlSafe(salt)
, ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv) , ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv)
, secretIv: OAUTH3._base64.bufferToUrlSafe(secretIv) , };
};
}); });
}); });
}; };
OAUTH3.crypto._decryptKey = function (ppid, storedObj) { OAUTH3.crypto.decryptKeyPair = function (storedObj, password) {
var salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt); var salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt);
var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey); var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey);
var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv); var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv);
return OAUTH3.crypto.core.pbkdf2(ppid, salt) return OAUTH3.crypto.core.pbkdf2(password, salt)
.then(function (key) { .then(function (key) {
return OAUTH3.crypto.core.decrypt(key, iv, encJwk); return OAUTH3.crypto.core.decrypt(key, iv, encJwk);
}) })
.then(OAUTH3._binStr.bufferToBinStr) .then(OAUTH3._binStr.bufferToBinStr)
.then(JSON.parse); .then(JSON.parse)
}; .then(function (privateKey) {
return {
OAUTH3.crypto._getKey = function (ppid) { privateKey: privateKey
return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(ppid)) , publicKey: storedObj.publicKey
.then(function (hash) { , };
var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash);
var promise;
if (window.localStorage.getItem(name) === null) {
promise = OAUTH3.crypto._createKey(ppid).then(function (key) {
window.localStorage.setItem(name, JSON.stringify(key));
return key;
});
} else {
promise = OAUTH3.PromiseA.resolve(JSON.parse(window.localStorage.getItem(name)));
}
return promise.then(function (storedObj) {
return OAUTH3.crypto._decryptKey(ppid, storedObj);
}); });
});
};
OAUTH3.crypto._signPayload = function (payload) {
return OAUTH3.crypto._getKey('some PPID').then(function (key) {
var header = {type: 'JWT', alg: key.alg, kid: key.kid};
var input = [
OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null))
, OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null))
].join('.');
return OAUTH3.crypto.core.sign(key, OAUTH3._binStr.binStrToBuffer(input))
.then(OAUTH3._base64.bufferToUrlSafe)
.then(function (signature) {
return input + '.' + signature;
});
});
}; };
}('undefined' !== typeof exports ? exports : window)); }('undefined' !== typeof exports ? exports : window));

View File

@ -3,39 +3,6 @@
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
OAUTH3.query.parse = function (search) {
// 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;
};
OAUTH3.scope.parse = function (scope) {
return (scope||'').split(/[, ]/g);
};
OAUTH3.url.parse = function (url) { OAUTH3.url.parse = function (url) {
// TODO browser // TODO browser
// Node should replace this // Node should replace this
@ -58,8 +25,16 @@ OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) {
}; };
OAUTH3.url.checkRedirect = function (client, query) { OAUTH3.url.checkRedirect = function (client, query) {
console.warn("[security] URL path checking not yet implemented"); console.warn("[security] URL path checking not yet implemented");
if (!query) {
query = client;
client = query.client_uri;
}
client = client.url || client;
var clientUrl = OAUTH3.url.normalize(client.url); // it doesn't matter who the referrer is as long as the destination
// is an authorized destination for the client in question
// (though it may not hurt to pass the referrer's info on to the client)
var clientUrl = OAUTH3.url.normalize(client);
var redirectUrl = OAUTH3.url.normalize(query.redirect_uri); var redirectUrl = OAUTH3.url.normalize(query.redirect_uri);
// General rule: // General rule:
@ -72,6 +47,18 @@ OAUTH3.url.checkRedirect = function (client, query) {
return true; return true;
} }
var callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK?'+OAUTH3.query.stringify({
'redirect_uri': redirectUrl
, 'allowed_urls': clientUrl
, 'client_id': client
, 'referrer_uri': OAUTH3.uri.normalize(window.document.referrer)
});
if (query.debug) {
console.log('Redirect Attack');
console.log(query);
window.alert("DEBUG MODE checkRedirect error encountered. Check the console.");
}
location.href = callbackUrl;
return false; return false;
}; };
OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) { OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) {
@ -110,13 +97,11 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
// Example Resource Owner Password Request // Example Resource Owner Password Request
// (generally for 1st party and direct-partner mobile apps, and webapps) // (generally for 1st party and direct-partner mobile apps, and webapps)
// //
// POST https://example.com/api/org.oauth3.provider/access_token // POST https://example.com/api/issuer@oauth3.org/access_token
// { "grant_type": "password", "client_id": "<<id>>", "scope": "<<scope>>" // { "grant_type": "password", "client_id": "<<id>>", "scope": "<<scope>>"
// , "username": "<<username>>", "password": "password" } // , "username": "<<username>>", "password": "password" }
// //
opts = opts || {}; opts = opts || {};
var type = 'access_token';
var grantType = 'password';
if (!opts.password) { if (!opts.password) {
if (opts.otp) { if (opts.otp) {
@ -125,16 +110,13 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
} }
} }
var scope = opts.scope || directive.authn_scope; var args = directive.access_token;
var clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos;
var clientUri = opts.client_uri;
var args = directive[type];
var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined; var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined;
// TODO require user agent // TODO require user agent
var params = { var params = {
client_id: opts.client_id || opts.client_uri client_id: opts.client_id || opts.client_uri
, client_uri: opts.client_uri , client_uri: opts.client_uri
, grant_type: grantType , grant_type: 'password'
, username: opts.username , username: opts.username
, password: opts.password || otpCode || undefined , password: opts.password || otpCode || undefined
, totp: opts.totp || opts.totpToken || opts.totp_token || undefined , totp: opts.totp || opts.totpToken || opts.totp_token || undefined
@ -149,23 +131,21 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
//, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key //, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key
, debug: opts.debug || undefined , debug: opts.debug || undefined
}; };
var uri = args.url;
var body;
if (opts.totp) {
params.totp = opts.totp;
}
if (clientUri) { if (opts.client_uri) {
params.clientAgreeTos = clientAgreeTos; params.clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos;
if (!clientAgreeTos) { if (!params.clientAgreeTos) {
throw new Error('Developer Error: missing clientAgreeTos uri'); throw new Error('Developer Error: missing clientAgreeTos uri');
} }
} }
var scope = opts.scope || directive.authn_scope;
if (scope) { if (scope) {
params.scope = OAUTH3.scope.stringify(scope); params.scope = OAUTH3.scope.stringify(scope);
} }
var uri = args.url;
var body;
if ('GET' === args.method.toUpperCase()) { if ('GET' === args.method.toUpperCase()) {
uri += '?' + OAUTH3.query.stringify(params); uri += '?' + OAUTH3.query.stringify(params);
} else { } else {
@ -181,6 +161,10 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
OAUTH3.urls.grants = function (directive, opts) { OAUTH3.urls.grants = function (directive, opts) {
// directive = { issuer, authorization_decision } // directive = { issuer, authorization_decision }
// opts = { response_type, scopes{ granted, requested, pending, accepted } } // opts = { response_type, scopes{ granted, requested, pending, accepted } }
var grantsDir = directive.grants;
if (!grantsDir) {
throw new Error("provider doesn't support grants");
}
if (!opts) { if (!opts) {
throw new Error("You must supply a directive and an options object."); throw new Error("You must supply a directive and an options object.");
@ -195,18 +179,19 @@ OAUTH3.urls.grants = function (directive, opts) {
console.warn("You should supply options.referrer"); console.warn("You should supply options.referrer");
} }
if (!opts.method) { if (!opts.method) {
console.warn("You must supply options.method as either 'GET', or 'POST'"); console.warn("You should supply options.method as either 'GET', or 'POST'");
opts.method = grantsDir.method || 'GET';
} }
if ('POST' === opts.method) { if ('POST' === opts.method) {
if ('string' !== typeof opts.scope) { if ('string' !== typeof opts.scope) {
console.warn("You should supply options.scope as a space-delimited string of scopes"); throw new Error("You must supply options.scope as a comma-delimited string of scopes");
} }
if (-1 === ['token', 'code'].indexOf(opts.response_type)) { if ('string' !== typeof opts.sub) {
throw new Error("You must supply options.response_type as 'token' or 'code'"); console.log("provide 'sub' to urls.grants to specify the PPID for the client");
} }
} }
var url = OAUTH3.url.resolve(directive.api, directive.grants.url) var url = OAUTH3.url.resolve(directive.api, grantsDir.url)
.replace(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri)) .replace(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri))
.replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED') .replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED')
; ;
@ -214,16 +199,14 @@ OAUTH3.urls.grants = function (directive, opts) {
client_id: opts.client_id client_id: opts.client_id
, client_uri: opts.client_uri , client_uri: opts.client_uri
, referrer: opts.referrer , referrer: opts.referrer
, response_type: opts.response_type
, scope: opts.scope , scope: opts.scope
, tenant_id: opts.tenant_id , sub: opts.sub
}; };
var body;
var body;
if ('GET' === opts.method) { if ('GET' === opts.method) {
url += '?' + OAUTH3.query.stringify(data); url += '?' + OAUTH3.query.stringify(data);
} } else {
else {
body = data; body = data;
} }
@ -234,6 +217,76 @@ OAUTH3.urls.grants = function (directive, opts) {
, session: opts.session , session: opts.session
}; };
}; };
OAUTH3.urls.clientToken = function (directive, opts) {
var tokenDir = directive.access_token;
if (!tokenDir) {
throw new Error("provider doesn't support getting access tokens");
}
if (!opts) {
throw new Error("You must supply a directive and an options object.");
}
if (!(opts.azp || opts.client_id)) {
throw new Error("You must supply options.client_id.");
}
if (!opts.session) {
throw new Error("You must supply options.session.");
}
if (!opts.method) {
opts.method = tokenDir.method || 'POST';
}
var params = {
grant_type: 'issuer_token'
, client_id: opts.azp || opts.client_id
, azp: opts.azp || opts.client_id
, aud: opts.aud
, exp: opts.exp
, refresh_token: opts.refresh_token
, refresh_exp: opts.refresh_exp
};
var url = OAUTH3.url.resolve(directive.api, tokenDir.url);
var body;
if ('GET' === opts.method) {
url += '?' + OAUTH3.query.stringify(params);
} else {
body = params;
}
return {
method: opts.method
, url: url
, data: body
, session: opts.session
};
};
OAUTH3.urls.publishKey = function (directive, opts) {
var jwkDir = directive.publish_jwk;
if (!jwkDir) {
throw new Error("provider doesn't support publishing public keys");
}
if (!opts) {
throw new Error("You must supply a directive and an options object.");
}
if (!opts.session) {
throw new Error("You must supply 'options.session'.");
}
if (!(opts.public_key || opts.publicKey)) {
throw new Error("You must supply 'options.public_key'.");
}
var url = OAUTH3.url.resolve(directive.api, jwkDir.url)
.replace(/(:sub|:account_id)/g, opts.session.token.sub)
;
return {
method: jwkDir.method || opts.method || 'POST'
, url: url
, data: opts.public_key || opts.publicKey
, session: opts.session
};
};
OAUTH3.authn = {}; OAUTH3.authn = {};
OAUTH3.authn.loginMeta = function (directive, opts) { OAUTH3.authn.loginMeta = function (directive, opts) {
@ -267,112 +320,95 @@ OAUTH3.authn.otp = function (directive, opts) {
OAUTH3.authn.resourceOwnerPassword = function (directive, opts) { OAUTH3.authn.resourceOwnerPassword = function (directive, opts) {
var providerUri = directive.issuer; var providerUri = directive.issuer;
//var scope = opts.scope; return OAUTH3.request(OAUTH3.urls.resourceOwnerPassword(directive, opts)).then(function (resp) {
//var appId = opts.appId; var data = resp.data;
return OAUTH3.discover(providerUri, opts).then(function (directive) { data.provider_uri = providerUri;
var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts); if (data.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data));
}
// TODO return not the raw request? return OAUTH3.hooks.session.refresh(
return OAUTH3.request(prequest).then(function (req) { opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
var data = req.data; , data
data.provider_uri = providerUri; );
if (data.error) { }).then(function (session) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data)); if (!opts.rememberDevice && !opts.remember_device) {
return session;
}
return OAUTH3.PromiseA.resolve().then(function () {
if (!OAUTH3.crypto) {
throw new Error("OAuth3 crypto library unavailable");
} }
return OAUTH3.hooks.session.refresh( return OAUTH3.crypto.createKeyPair().then(function (keyPair) {
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } return OAUTH3.request(OAUTH3.urls.publishKey(directive, {
, data session: session
); , publicKey: keyPair.publicKey
})).then(function () {
return OAUTH3.hooks.keyPairs.set(session.token.sub, keyPair);
});
});
}).then(function () {
return session;
}, function (err) {
console.error('failed to save keys to remember device', err);
window.alert('Failed to remember device');
return session;
}); });
}); });
}; };
OAUTH3.authz = {}; OAUTH3.authz = {};
OAUTH3.authz.scopes = function (providerUri, session, clientParams) { OAUTH3.authz.scopes = function (providerUri, session, clientParams) {
// OAuth3.requests.grants(providerUri, {}); // return list of grants
// OAuth3.checkGrants(providerUri, {}); //
var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer); var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer);
var scope = clientParams.scope || ''; var scope = clientParams.scope || 'oauth3_authn';
var clientObj = clientParams; if ('oauth3_authn' === scope) {
// implicit ppid grant is automatic
if (!scope) { console.warn('[security] fix scope checking on backend so that we can do automatic grants');
scope = 'oauth3_authn'; // TODO check user preference if implicit ppid grant is allowed
//return generateToken(session, clientObj);
} }
return OAUTH3.authz.grants(providerUri, { return OAUTH3.hooks.grants.get(session.token.sub, clientUri).then(function (granted) {
method: 'GET' if (granted) {
, client_id: clientUri if (typeof granted.scope === 'string') {
, client_uri: clientUri return OAUTH3.scope.parse(granted.scope);
, session: session } else if (Array.isArray(granted.scope)) {
}).then(function (grantResults) { return granted.scope;
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.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.uri.normalize(window.document.referrer)
;
if (clientParams.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) { return OAUTH3.authz.grants(providerUri, {
// implicit ppid grant is automatic method: 'GET'
console.warn('[security] fix scope checking on backend so that we can do automatic grants'); , client_id: clientUri
// TODO check user preference if implicit ppid grant is allowed , client_uri: clientUri
//return generateToken(session, clientObj); , session: session
} }).then(function (results) {
return results.grants;
grants = (grantResults).grants.filter(function (grant) { }, function (err) {
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) { if (!/no .*grants .*found/i.test(err.message)) {
return true; throw err;
}
return [];
});
}).then(function (granted) {
var requested = OAUTH3.scope.parse(scope);
var accepted = [];
var pending = [];
requested.forEach(function (scp) {
if (granted.indexOf(scp) < 0) {
pending.push(scp);
} else {
accepted.push(scp);
} }
}); });
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 { return {
pending: pendingScopes // not yet accepted requested: requested // all requested, now
, granted: grantedScopes // all granted, ever , granted: granted // all granted, ever
, requested: scopes // all requested, now , accepted: accepted // intersection of granted (ever) and requested (now)
, accepted: acceptedScopes // granted (ever) and requested (now) , pending: pending // not yet accepted
}; };
}); });
}; };
@ -381,73 +417,153 @@ OAUTH3.authz.grants = function (providerUri, opts) {
client_id: providerUri client_id: providerUri
, debug: opts.debug , debug: opts.debug
}).then(function (directive) { }).then(function (directive) {
return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts);
}).then(function (grantsResult) {
var grants = grantsResult.originalData || grantsResult.data;
if (grants.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants));
}
// the responses for GET and POST requests are now the same, so we should alway be able to
// use the response and save it the same way.
if ('GET' !== opts.method && 'POST' !== opts.method) {
return grants;
}
return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts).then(function (grantsResult) { OAUTH3.hooks.grants.set(grants.sub, grants.azp, grants);
if ('POST' === opts.method) { return {
// TODO this is clientToken client: grants.azp
return grantsResult.originalData || grantsResult.data; , clientSub: grants.azpSub
} , grants: OAUTH3.scope.parse(grants.scope)
};
var grants = grantsResult.originalData || grantsResult.data;
// TODO
if (grants.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants));
}
OAUTH3.hooks.grants.set(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.grants.set(clientId, [ grant ]);
});
return {
client: OAUTH3.hooks.grants.get(opts.client_id + '-client')
, grants: OAUTH3.hooks.grants.get(opts.client_id) || []
};
});
}); });
}; };
function calcExpiration(exp, now) {
if (!exp) {
return;
}
if (typeof exp === 'string') {
var match = /^(\d+\.?\d*) *(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(exp);
if (!match) {
return now;
}
var num = parseFloat(match[1]);
var type = (match[2] || 's').toLowerCase()[0];
switch (type) {
case 'y': num *= 365.25; /* falls through */
case 'd': num *= 24; /* falls through */
case 'h': num *= 60; /* falls through */
case 'm': num *= 60; /* falls through */
case 's': exp = num;
}
}
if (typeof exp !== 'number') {
throw new Error('invalid expiration provided: '+exp);
}
now = now || Math.floor(Date.now() / 1000);
if (exp > now) {
return exp;
} else if (exp > 31557600) {
console.warn('tried to set expiration to more that a year');
exp = 31557600;
}
return now + exp;
}
OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) { OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) {
if (!OAUTH3.url.checkRedirect(clientParams.client_uri, clientParams)) {
return;
}
if ('token' !== clientParams.response_type) {
var message;
if ('code' === clientParams.response_type) {
message = "Authorization Code Redirect NOT IMPLEMENTED";
} else {
message = "Authorization response type '"+clientParams.response_type+"' not supported";
}
window.alert(message);
throw new Error(message);
}
scopes.new = scopes.new || []; var prom;
if (scopes.new) {
if ('token' === clientParams.response_type) { prom = OAUTH3.authz.grants(providerUri, {
// get token and redirect client-side session: session
return OAUTH3.authz.grants(providerUri, { , method: 'POST'
method: 'POST'
, client_id: clientParams.client_uri , 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 , referrer: clientParams.referrer
, session: session , scope: scopes.accepted.concat(scopes.new).join(',')
, subject: clientParams.subject
, debug: clientParams.debug
}).then(function (results) {
// TODO limit refresh token to an expirable token
// TODO inform client not to persist token
/*
if (clientParams.dnsTxt) {
Object.keys(results).forEach(function (key) {
if (/refresh/.test(key)) {
results[key] = undefined;
}
});
}
*/
OAUTH3.url.redirect(clientParams, scopes, results);
}); });
} else {
prom = OAUTH3.PromiseA.resolve();
} }
else if ('code' === clientParams.response_type) {
// get token and redirect server-side return prom.then(function () {
// (requires insecure form post as per spec) return OAUTH3.hooks.keyPairs.get(session.token.sub);
//OAUTH3.requests.authorizationDecision(); }).then(function (keyPair) {
window.alert("Authorization Code Redirect NOT IMPLEMENTED"); if (!keyPair) {
throw new Error("Authorization Code Redirect NOT IMPLEMENTED"); return OAUTH3.discover(providerUri, {
} client_id: providerUri
, debug: clientParams.debug
}).then(function (directive) {
return OAUTH3.request(OAUTH3.urls.clientToken(directive, {
method: 'POST'
, session: session
, referrer: clientParams.referrer
, response_type: clientParams.response_type
, client_id: clientParams.client_uri
, azp: clientParams.client_uri
, aud: clientParams.aud
, exp: clientParams.exp
, refresh_token: clientParams.refresh_token
, refresh_exp: clientParams.refresh_exp
, debug: clientParams.debug
})).then(function (result) {
return result.originalData || result.data;
});
});
}
return OAUTH3.hooks.grants.get(keyPair.sub, clientParams.client_uri).then(function (grant) {
var now = Math.floor(Date.now()/1000);
var payload = {
iat: now
, iss: providerUri
, aud: clientParams.aud || providerUri
, azp: clientParams.client_uri
, sub: grant.azpSub
, scope: OAUTH3.scope.stringify(grant.scope)
, };
var signProms = [];
signProms.push(OAUTH3.jwt.sign(Object.assign({
exp: calcExpiration(clientParams.exp || '1h', now)
}, payload), keyPair));
// if (clientParams.refresh_token) {
signProms.push(OAUTH3.jwt.sign(Object.assign({
exp: calcExpiration(clientParams.refresh_exp, now)
}, payload), keyPair));
// }
return OAUTH3.PromiseA.all(signProms).then(function (tokens) {
console.log('created new tokens for client');
return {
access_token: tokens[0]
, refresh_token: tokens[1]
, scope: OAUTH3.scope.stringify(grant.scope)
, token_type: 'bearer'
};
});
});
}).then(function (session) {
// TODO limit refresh token to an expirable token
// TODO inform client not to persist token
OAUTH3.url.redirect(clientParams, scopes, session);
}, function (err) {
console.error('unexpected error creating client tokens', err);
OAUTH3.url.redirect(clientParams, scopes, {error: err});
});
}; };
OAUTH3.requests = {}; OAUTH3.requests = {};
OAUTH3.requests.accounts = {}; OAUTH3.requests.accounts = {};
OAUTH3.requests.accounts.update = function (directive, session, opts) { OAUTH3.requests.accounts.update = function (directive, session, opts) {
@ -512,25 +628,178 @@ OAUTH3.requests.accounts.create = function (directive, session, account) {
, data: data , data: data
}); });
}; };
OAUTH3.hooks.grants = { OAUTH3.hooks.grants = {
// Provider Only get: function (id, clientUri) {
set: function (clientUri, newGrants) { OAUTH3.hooks._checkStorage('grants', 'get');
clientUri = OAUTH3.uri.normalize(clientUri);
console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault'); if (!id) {
console.warn(newGrants); throw new Error("id is not set");
if (!this._cache) { this._cache = {}; } }
console.log('clientUri, newGrants'); if (!clientUri) {
console.log(clientUri, newGrants); throw new Error("clientUri is not set");
this._cache[clientUri] = newGrants; }
return newGrants; return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(id, OAUTH3.uri.normalize(clientUri)));
} }
, get: function (clientUri) { , set: function (id, clientUri, grants) {
clientUri = OAUTH3.uri.normalize(clientUri); OAUTH3.hooks._checkStorage('grants', 'set');
console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault');
if (!this._cache) { this._cache = {}; } if (!id) {
console.log('clientUri, existingGrants'); throw new Error("id is not set");
console.log(clientUri, this._cache[clientUri]); }
return this._cache[clientUri]; if (!clientUri) {
throw new Error("clientUri is not set");
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.set(id, OAUTH3.uri.normalize(clientUri), grants));
}
, all: function () {
OAUTH3.hooks._checkStorage('grants', 'all');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.all());
}
, clear: function () {
OAUTH3.hooks._checkStorage('grants', 'clear');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.clear());
}
};
OAUTH3.hooks.keyPairs = {
get: function (id) {
OAUTH3.hooks._checkStorage('keyPairs', 'get');
if (!id) {
throw new Error("id is not set");
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.get(id));
}
, set: function (id, keyPair) {
OAUTH3.hooks._checkStorage('keyPairs', 'set');
if (!keyPair && id.privateKey && id.publicKey && id.sub) {
keyPair = id;
id = keyPair.sub;
}
if (!keyPair) {
return OAUTH3.PromiseA.reject(new Error("no key pair provided to save"));
}
if (!id) {
throw new Error("id is not set");
}
keyPair.sub = keyPair.sub || id;
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.set(id, keyPair));
}
, all: function () {
OAUTH3.hooks._checkStorage('keyPairs', 'all');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.all());
}
, clear: function () {
OAUTH3.hooks._checkStorage('keyPairs', 'clear');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.clear());
}
};
OAUTH3.hooks.session.get = function (providerUri, id) {
OAUTH3.hooks._checkStorage('sessions', 'get');
var sessProm = OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id));
if (providerUri !== OAUTH3.clientUri(window.location)) {
return sessProm;
}
return sessProm.then(function (session) {
if (session && OAUTH3.jwt.freshness(session.token) === 'fresh') {
return session;
}
return OAUTH3.hooks.keyPairs.all().then(function (keyPairs) {
var pair;
if (id) {
pair = keyPairs[id];
} else if (Object.keys(keyPairs).length === 1) {
id = Object.keys(keyPairs)[0];
pair = keyPairs[id];
} else if (Object.keys(keyPairs).length > 1) {
console.error("too many users, don't know which key to use");
}
if (!pair) {
// even if the access token isn't fresh, the session might have a refresh token
return session;
}
var now = Math.floor(Date.now()/1000);
var payload = {
iat: now
, iss: providerUri
, aud: providerUri
, azp: providerUri
, sub: pair.sub || id
, scope: ''
, exp: now + 3600
};
return OAUTH3.jwt.sign(payload, pair.privateKey).then(function (token) {
console.log('created new token for provider');
return OAUTH3.hooks.session.refresh(
{ provider_uri: providerUri, client_uri: providerUri || providerUri }
, { access_token: token }
);
});
});
});
};
OAUTH3._defaultStorage.grants = {
prefix: 'grants-'
, get: function (id, clientUri) {
var key = this.prefix + id+'/'+clientUri;
var result = JSON.parse(window.localStorage.getItem(key) || 'null');
return OAUTH3.PromiseA.resolve(result);
}
, set: function (id, clientUri, grants) {
var key = this.prefix + id+'/'+clientUri;
window.localStorage.setItem(key, JSON.stringify(grants));
return this.get(clientUri);
}
, all: function () {
var prefix = this.prefix;
var result = {};
OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) {
var split = key.replace(prefix, '').split('/');
if (!result[split[0]]) { result[split[0]] = {}; }
result[split[0]][split[1]] = JSON.parse(window.localStorage.getItem(key) || 'null');
});
return OAUTH3.PromiseA.resolve(result);
}
, clear: function () {
OAUTH3._defaultStorage._getStorageKeys(this.prefix, window.localStorage).forEach(function (key) {
window.localStorage.removeItem(key);
});
return OAUTH3.PromiseA.resolve();
}
};
OAUTH3._defaultStorage.keyPairs = {
prefix: 'key_pairs-'
, get: function (id) {
var result = JSON.parse(window.localStorage.getItem(this.prefix + id) || 'null');
return OAUTH3.PromiseA.resolve(result);
}
, set: function (id, keyPair) {
window.localStorage.setItem(this.prefix + id, JSON.stringify(keyPair));
return this.get(id);
}
, all: function () {
var prefix = this.prefix;
var result = {};
OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) {
result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || 'null');
});
return OAUTH3.PromiseA.resolve(result);
}
, clear: function () {
OAUTH3._defaultStorage._getStorageKeys(this.prefix, window.localStorage).forEach(function (key) {
window.localStorage.removeItem(key);
});
return OAUTH3.PromiseA.resolve();
} }
}; };

View File

@ -28,6 +28,7 @@ OAUTH3._base64.atob = function (base64) {
OAUTH3._base64.btoa = function (text) { OAUTH3._base64.btoa = function (text) {
return new Buffer(text, 'utf8').toString('base64'); return new Buffer(text, 'utf8').toString('base64');
}; };
OAUTH3._defaultStorage = require('./oauth3.node.storage');
OAUTH3._node = {}; OAUTH3._node = {};
OAUTH3._node.discover = function(providerUri/*, opts*/) { OAUTH3._node.discover = function(providerUri/*, opts*/) {

View File

@ -67,10 +67,9 @@ module.exports = {
, sessions: { , sessions: {
all: function (providerUri) { all: function (providerUri) {
var dirname = path.join(oauth3dir, 'sessions'); return fs.readdirAsync(sessionsdir).then(function (nodes) {
return fs.readdirAsync(dirname).then(function (nodes) {
return nodes.map(function (node) { return nodes.map(function (node) {
var result = require(path.join(dirname, node)); var result = require(path.join(sessionsdir, node));
if (result.link) { if (result.link) {
return null; return null;
} }
@ -91,7 +90,7 @@ module.exports = {
result = require(path.join(sessionsdir, providerUri + '.json')); result = require(path.join(sessionsdir, providerUri + '.json'));
// TODO make safer // TODO make safer
if (result.link && '/' !== result.link[0] && !/\.\./.test(result.link)) { if (result.link && '/' !== result.link[0] && !/\.\./.test(result.link)) {
result = require(path.join(oauth3dir, 'sessions', result.link)); result = require(path.join(sessionsdir, result.link));
} }
} }
} catch(e) { } catch(e) {
@ -113,10 +112,9 @@ module.exports = {
}); });
} }
, clear: function () { , clear: function () {
var dirname = path.join(oauth3dir, 'sessions'); return fs.readdirAsync(sessionsdir).then(function (nodes) {
return fs.readdirAsync(dirname).then(function (nodes) {
return PromiseA.all(nodes.map(function (node) { return PromiseA.all(nodes.map(function (node) {
return fs.unlinkAsync(path.join(dirname, node)); return fs.unlinkAsync(path.join(sessionsdir, node));
})); }));
}); });
} }

View File

@ -9,7 +9,7 @@ OAUTH3.api['tunnel.token'] = function (providerUri, opts) {
return OAUTH3.request({ return OAUTH3.request({
method: 'POST' method: 'POST'
, url: OAUTH3.url.normalize(providerUri) , url: OAUTH3.url.normalize(providerUri)
+ '/api/org.oauth3.tunnel/accounts/' + session.token.sub + '/token' + '/api/tunnel@oauth3.org/accounts/' + session.token.sub + '/token'
, session: session , session: session
, data: { , data: {
domains: opts.data.domains domains: opts.data.domains

View File

@ -1,13 +1,12 @@
{ "terms": [ "oauth3.org/tos/draft" ] { "terms": [ "oauth3.org/tos/draft" ]
, "api": "api.:hostname" , "api": "api.:hostname"
, "authorization_dialog": { "url": "#/authorization_dialog" } , "authorization_dialog": { "url": "#/authorization_dialog" }
, "access_token": { "method": "POST", "url": "api/issuer@oauth3.org/access_token" } , "access_token": { "method": "POST", "url": "api/issuer@oauth3.org/access_token" }
, "otp": { "method": "POST", "url": "api/issuer@oauth3.org/otp" } , "otp": { "method": "POST", "url": "api/issuer@oauth3.org/access_token/send_otp" }
, "credential_otp": { "method": "POST", "url": "api/issuer@oauth3.org/otp" } , "credential_otp": { "method": "POST", "url": "api/issuer@oauth3.org/access_token/send_otp" }
, "credential_meta": { "url": "api/issuer@oauth3.org/logins/meta/:type/:id" } , "grants": { "method": "GET", "url": "api/issuer@oauth3.org/grants/:sub/:azp" }
, "credential_create": { "method": "POST", "url": "api/issuer@oauth3.org/logins" } , "publish_jwk": { "method": "POST", "url": "api/issuer@oauth3.org/jwks/:sub" }
, "grants": { "method": "GET", "url": "api/issuer@oauth3.org/grants/:azp/:sub" } , "retrieve_jwk": { "method": "GET", "url": "api/issuer@oauth3.org/jwks/:sub/:kid.json" }
, "authorization_decision": { "method": "POST", "url": "api/issuer@oauth3.org/authorization_decision" } , "callback": { "method": "GET", "url": ".well-known/oauth3/callback.html#/" }
, "callback": { "method": "GET", "url": ".well-known/oauth3/callback.html#/" } , "logout": { "method": "GET", "url": "#/logout/" }
, "logout": { "method": "GET", "url": "#/logout/" }
} }