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/
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/`
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)
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/`
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`
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"
integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA="
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>
</body>
</html>
@ -81,7 +81,7 @@ function onClickLogin() {
console.info('Secure PPID (aka subject):', session.token.sub);
return oauth3.request({
url: 'https://oauth3.org/api/issuer@oauth3.org/inspect_token'
url: 'https://oauth3.org/api/issuer@oauth3.org/inspect'
, session: session
}).then(function (resp) {

View File

@ -131,8 +131,8 @@ parseArgs(process.argv, {
// authn / authz
, [ '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: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: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/tunnel@oauth3.org/checkip)'.replace(/\b:provider\b/, defaults.provider) ]
, [ 'devices:attach', "attach a device to 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)' ]

View File

@ -78,7 +78,7 @@
, uri: {
normalize: function (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
// example.com
@ -94,7 +94,7 @@
, url: {
normalize: function (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
// example.com
@ -168,9 +168,12 @@
}
}
, scope: {
stringify: function (scope) {
parse: function (scope) {
return (scope||'').split(/[+, ]+/g);
}
, stringify: function (scope) {
if (Array.isArray(scope)) {
scope = scope.join(' ');
scope = scope.join(',');
}
return scope;
}
@ -204,40 +207,90 @@
}
, jwt: {
// decode only (no verification)
decode: function (str) {
decode: function (token, opts) {
// 'abc.qrs.xyz'
// [ 'abc', 'qrs', 'xyz' ]
// [ {}, {}, 'foo' ]
// { header: {}, payload: {}, signature: '' }
var parts = str.split(/\./g);
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64));
});
// {}
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;
}
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 data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.'));
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) {
staletime = staletime || (15 * 60);
var now = _now || Date.now();
var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000));
, sign: function (payload, jwk) {
if (!OAUTH3.crypto) {
return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable"));
}
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';
}
if (fresh <= 0) {
return 'expired';
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';
}
return 'stale';
}
}
, urls: {
@ -275,7 +328,7 @@
// Example Implicit Grant Request
// (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
// &scope=`encodeURIComponent('profile.login profile.email')`
// &state=`cryptoutil.random().toString('hex')`
@ -341,29 +394,36 @@
// , "username": "<<username>>", "password": "password" }
//
opts = opts || {};
var type = 'access_token';
var grantType = 'refresh_token';
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 clientSecret = opts.client_secret;
var args = directive[type];
var args = directive.access_token;
var params = {
"grant_type": grantType
, "refresh_token": opts.refresh_token || (opts.session && opts.session.refresh_token)
"grant_type": 'refresh_token'
, "refresh_token": refresh_token
, "response_type": 'token'
, "client_id": opts.client_id || opts.client_uri
, "client_uri": opts.client_uri
//, "scope": undefined
//, "client_secret": undefined
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
if (clientSecret) {
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 = clientSecret;
params.client_secret = opts.client_secret;
}
if (scope) {
@ -430,42 +490,44 @@
}
}
, hooks: {
directives: {
get: function (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
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;
});
_checkStorage: function (grpName, funcName) {
if (!OAUTH3._hooks) {
OAUTH3._hooks = {};
}
, _getCached: function (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
return OAUTH3.hooks.directives._cache[providerUri];
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) {
providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
OAUTH3.hooks.directives._cache[providerUri] = directives;
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._set(providerUri, directives));
}
, _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) || '{}');
OAUTH3.hooks._checkStorage('directives', 'set');
if (!providerUri) {
throw new Error("providerUri is not set");
}
return OAUTH3._hooks.directives.get(providerUri);
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.set(OAUTH3.uri.normalize(providerUri), directives));
}
, _set: function (providerUri, directives) {
if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.set) {
console.warn('[Warn] Please implement OAUTH3._hooks.directives.set = function (providerUri, directives) { return PromiseA<directives>; }');
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
return directives;
}
return OAUTH3._hooks.directives.set(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());
}
}
, session: {
@ -485,7 +547,7 @@
oldSession.client_uri = clientUri; // azp
// 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.acx||{}).id
@ -496,7 +558,7 @@
oldSession.token.provider_uri = providerUri;
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.acx||{}).id
|| ((oldSession.refresh.axs||[])[0]||{}).appScopedId
@ -506,7 +568,7 @@
}
// 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) {
opts = opts || {};
@ -559,62 +621,40 @@
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) {
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);
if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; }
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));
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.set(providerUri, newSession, id));
}
, get: function (providerUri, id) {
providerUri = OAUTH3.uri.normalize(providerUri);
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');
return OAUTH3.PromiseA.resolve(
OAUTH3.hooks.session._getCached(providerUri, id) || OAUTH3.hooks.session._get(providerUri, id)
).then(function (session) {
var s = session || { token: {} };
OAUTH3.hooks.session._cache[providerUri + (id || s.id || s.token.id || '')] = session;
if (!id) {
OAUTH3.hooks.session._cache[providerUri] = session;
}
return session;
});
if (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.all(providerUri));
}
, _get: function (providerUri, id) {
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.all) {
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.all = function ([providerUri]) { return PromiseA<sessions>; }');
, clear: function (providerUri) {
OAUTH3.hooks._checkStorage('sessions', 'clear');
if (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
}
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.get) {
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);
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.clear(providerUri));
}
}
}
@ -803,7 +843,7 @@
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params));
}
OAUTH3.hooks.session._cache = {};
OAUTH3.hooks.session.clear();
return params;
});
}
@ -1123,6 +1163,82 @@
};
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
@ -1154,7 +1270,7 @@
, _resourceProviderUri: null
, _identityProviderDirectives: 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) {
var me = this;
if (!opts) {
@ -1223,7 +1339,7 @@
var me = this;
return me._initClient().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);
});
});

View File

@ -1,5 +1,5 @@
;(function (exports) {
'use strict';
'use strict';
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
@ -82,6 +82,7 @@
};
function checkWebCrypto() {
/* global OAUTH3_crypto_fallback */
var loadFallback = function() {
var prom;
loadFallback = function () { return prom; };
@ -96,17 +97,16 @@
resolve();
}
};
script.src = '/assets/org.oauth3/oauth3.crypto.fallback.js';
script.src = '/assets/oauth3.org/oauth3.crypto.fallback.js';
body.appendChild(script);
});
return prom;
};
function checkException(name, func) {
new OAUTH3.PromiseA(function (resolve) { resolve(func()); })
OAUTH3.PromiseA.resolve().then(func)
.then(function () {
OAUTH3.crypto.core[name] = webCrypto[name];
})
.catch(function (err) {
}, function (err) {
console.warn('error with WebCrypto', name, '- using fallback', err);
loadFallback().then(function () {
OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
@ -195,101 +195,61 @@
.then(OAUTH3._base64.bufferToUrlSafe);
};
OAUTH3.crypto._createKey = function (ppid) {
var saltProm = OAUTH3.crypto.core.randomBytes(16);
var kekProm = saltProm.then(function (salt) {
return OAUTH3.crypto.core.pbkdf2(ppid, salt);
});
var ecdsaProm = OAUTH3.crypto.core.genEcdsaKeyPair()
.then(function (keyPair) {
OAUTH3.crypto.createKeyPair = function () {
// TODO: maybe support other types of key pairs, not just ECDSA P-256
return OAUTH3.crypto.core.genEcdsaKeyPair().then(function (keyPair) {
return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) {
keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256';
keyPair.privateKey.kid = keyPair.publicKey.kid = kid;
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([
kekProm
, ecdsaProm
, saltProm
, OAUTH3.crypto.core.randomBytes(16)
, OAUTH3.crypto.core.randomBytes(12)
, OAUTH3.crypto.core.randomBytes(12)
]).then(function (results) {
, ]).then(function (results) {
var kek = results[0];
var keyPair = results[1];
var salt = results[2];
var userSecret = results[3];
var ecdsaIv = results[4];
var secretIv = results[5];
var salt = results[1];
var ecdsaIv = results[2];
return OAUTH3.PromiseA.all([
OAUTH3.crypto.core.encrypt(kek, ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey)))
, OAUTH3.crypto.core.encrypt(kek, secretIv, userSecret)
])
.then(function (encrypted) {
var privKeyBuf = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey));
return OAUTH3.crypto.core.encrypt(kek, ecdsaIv, privKeyBuf).then(function (encrypted) {
return {
publicKey: keyPair.publicKey
, privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0])
, userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1])
, privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted)
, salt: OAUTH3._base64.bufferToUrlSafe(salt)
, 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 encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey);
var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv);
return OAUTH3.crypto.core.pbkdf2(ppid, salt)
return OAUTH3.crypto.core.pbkdf2(password, salt)
.then(function (key) {
return OAUTH3.crypto.core.decrypt(key, iv, encJwk);
})
.then(OAUTH3._binStr.bufferToBinStr)
.then(JSON.parse);
};
OAUTH3.crypto._getKey = function (ppid) {
return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(ppid))
.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);
.then(JSON.parse)
.then(function (privateKey) {
return {
privateKey: privateKey
, publicKey: storedObj.publicKey
, };
});
});
};
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));

View File

@ -3,39 +3,6 @@
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) {
// TODO browser
// Node should replace this
@ -58,8 +25,16 @@ OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) {
};
OAUTH3.url.checkRedirect = function (client, query) {
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);
// General rule:
@ -72,6 +47,18 @@ OAUTH3.url.checkRedirect = function (client, query) {
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;
};
OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) {
@ -110,13 +97,11 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
// Example Resource Owner Password Request
// (generally for 1st party and direct-partner mobile apps, and webapps)
//
// POST https://example.com/api/org.oauth3.provider/access_token
// POST https://example.com/api/issuer@oauth3.org/access_token
// { "grant_type": "password", "client_id": "<<id>>", "scope": "<<scope>>"
// , "username": "<<username>>", "password": "password" }
//
opts = opts || {};
var type = 'access_token';
var grantType = 'password';
if (!opts.password) {
if (opts.otp) {
@ -125,16 +110,13 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
}
}
var scope = opts.scope || directive.authn_scope;
var clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos;
var clientUri = opts.client_uri;
var args = directive[type];
var args = directive.access_token;
var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined;
// TODO require user agent
var params = {
client_id: opts.client_id || opts.client_uri
, client_uri: opts.client_uri
, grant_type: grantType
, grant_type: 'password'
, username: opts.username
, password: opts.password || otpCode || 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
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
if (opts.totp) {
params.totp = opts.totp;
}
if (clientUri) {
params.clientAgreeTos = clientAgreeTos;
if (!clientAgreeTos) {
if (opts.client_uri) {
params.clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos;
if (!params.clientAgreeTos) {
throw new Error('Developer Error: missing clientAgreeTos uri');
}
}
var scope = opts.scope || directive.authn_scope;
if (scope) {
params.scope = OAUTH3.scope.stringify(scope);
}
var uri = args.url;
var body;
if ('GET' === args.method.toUpperCase()) {
uri += '?' + OAUTH3.query.stringify(params);
} else {
@ -181,6 +161,10 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
OAUTH3.urls.grants = function (directive, opts) {
// directive = { issuer, authorization_decision }
// opts = { response_type, scopes{ granted, requested, pending, accepted } }
var grantsDir = directive.grants;
if (!grantsDir) {
throw new Error("provider doesn't support grants");
}
if (!opts) {
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");
}
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 ('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)) {
throw new Error("You must supply options.response_type as 'token' or 'code'");
if ('string' !== typeof opts.sub) {
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(/(: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_uri: opts.client_uri
, referrer: opts.referrer
, response_type: opts.response_type
, scope: opts.scope
, tenant_id: opts.tenant_id
, sub: opts.sub
};
var body;
var body;
if ('GET' === opts.method) {
url += '?' + OAUTH3.query.stringify(data);
}
else {
} else {
body = data;
}
@ -234,6 +217,76 @@ OAUTH3.urls.grants = function (directive, opts) {
, 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.loginMeta = function (directive, opts) {
@ -267,112 +320,95 @@ OAUTH3.authn.otp = function (directive, opts) {
OAUTH3.authn.resourceOwnerPassword = function (directive, opts) {
var providerUri = directive.issuer;
//var scope = opts.scope;
//var appId = opts.appId;
return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts);
return OAUTH3.request(OAUTH3.urls.resourceOwnerPassword(directive, opts)).then(function (resp) {
var data = resp.data;
data.provider_uri = providerUri;
if (data.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data));
}
// TODO return not the raw request?
return OAUTH3.request(prequest).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
);
}).then(function (session) {
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(
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
, data
);
return OAUTH3.crypto.createKeyPair().then(function (keyPair) {
return OAUTH3.request(OAUTH3.urls.publishKey(directive, {
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.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 scope = clientParams.scope || '';
var clientObj = clientParams;
if (!scope) {
scope = 'oauth3_authn';
var scope = clientParams.scope || 'oauth3_authn';
if ('oauth3_authn' === scope) {
// implicit ppid grant is automatic
console.warn('[security] fix scope checking on backend so that we can do automatic grants');
// TODO check user preference if implicit ppid grant is allowed
//return generateToken(session, clientObj);
}
return OAUTH3.authz.grants(providerUri, {
method: 'GET'
, client_id: clientUri
, client_uri: clientUri
, session: session
}).then(function (grantResults) {
var grants;
var grantedScopes;
var grantedScopesMap;
var pendingScopes;
var acceptedScopes;
var scopes = scope.split(/[+, ]/g);
var callbackUrl;
// it doesn't matter who the referrer is as long as the destination
// is an authorized destination for the client in question
// (though it may not hurt to pass the referrer's info on to the client)
if (!OAUTH3.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.");
return OAUTH3.hooks.grants.get(session.token.sub, clientUri).then(function (granted) {
if (granted) {
if (typeof granted.scope === 'string') {
return OAUTH3.scope.parse(granted.scope);
} else if (Array.isArray(granted.scope)) {
return granted.scope;
}
location.href = callbackUrl;
return;
}
if ('oauth3_authn' === scope) {
// implicit ppid grant is automatic
console.warn('[security] fix scope checking on backend so that we can do automatic grants');
// TODO check user preference if implicit ppid grant is allowed
//return generateToken(session, clientObj);
}
grants = (grantResults).grants.filter(function (grant) {
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
return true;
return OAUTH3.authz.grants(providerUri, {
method: 'GET'
, client_id: clientUri
, client_uri: clientUri
, session: session
}).then(function (results) {
return results.grants;
}, function (err) {
if (!/no .*grants .*found/i.test(err.message)) {
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 {
pending: pendingScopes // not yet accepted
, granted: grantedScopes // all granted, ever
, requested: scopes // all requested, now
, accepted: acceptedScopes // granted (ever) and requested (now)
requested: requested // all requested, now
, granted: granted // all granted, ever
, accepted: accepted // intersection of granted (ever) and requested (now)
, pending: pending // not yet accepted
};
});
};
@ -381,73 +417,153 @@ OAUTH3.authz.grants = function (providerUri, opts) {
client_id: providerUri
, debug: opts.debug
}).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) {
if ('POST' === opts.method) {
// TODO this is clientToken
return grantsResult.originalData || grantsResult.data;
}
var grants = grantsResult.originalData || grantsResult.data;
// TODO
if (grants.error) {
return OAUTH3.PromiseA.reject(OAUTH3.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) || []
};
});
OAUTH3.hooks.grants.set(grants.sub, grants.azp, grants);
return {
client: grants.azp
, clientSub: grants.azpSub
, grants: OAUTH3.scope.parse(grants.scope)
};
});
};
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) {
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 || [];
if ('token' === clientParams.response_type) {
// get token and redirect client-side
return OAUTH3.authz.grants(providerUri, {
method: 'POST'
var prom;
if (scopes.new) {
prom = OAUTH3.authz.grants(providerUri, {
session: session
, method: 'POST'
, client_id: clientParams.client_uri
, client_uri: clientParams.client_uri
, scope: scopes.granted.concat(scopes.new).join(',')
, response_type: clientParams.response_type
, referrer: clientParams.referrer
, session: session
, 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);
, scope: scopes.accepted.concat(scopes.new).join(',')
});
} else {
prom = OAUTH3.PromiseA.resolve();
}
else if ('code' === clientParams.response_type) {
// get token and redirect server-side
// (requires insecure form post as per spec)
//OAUTH3.requests.authorizationDecision();
window.alert("Authorization Code Redirect NOT IMPLEMENTED");
throw new Error("Authorization Code Redirect NOT IMPLEMENTED");
}
return prom.then(function () {
return OAUTH3.hooks.keyPairs.get(session.token.sub);
}).then(function (keyPair) {
if (!keyPair) {
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.accounts = {};
OAUTH3.requests.accounts.update = function (directive, session, opts) {
@ -512,25 +628,178 @@ OAUTH3.requests.accounts.create = function (directive, session, account) {
, data: data
});
};
OAUTH3.hooks.grants = {
// Provider Only
set: function (clientUri, newGrants) {
clientUri = OAUTH3.uri.normalize(clientUri);
console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault');
console.warn(newGrants);
if (!this._cache) { this._cache = {}; }
console.log('clientUri, newGrants');
console.log(clientUri, newGrants);
this._cache[clientUri] = newGrants;
return newGrants;
get: function (id, clientUri) {
OAUTH3.hooks._checkStorage('grants', 'get');
if (!id) {
throw new Error("id is not set");
}
if (!clientUri) {
throw new Error("clientUri is not set");
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(id, OAUTH3.uri.normalize(clientUri)));
}
, get: function (clientUri) {
clientUri = OAUTH3.uri.normalize(clientUri);
console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault');
if (!this._cache) { this._cache = {}; }
console.log('clientUri, existingGrants');
console.log(clientUri, this._cache[clientUri]);
return this._cache[clientUri];
, set: function (id, clientUri, grants) {
OAUTH3.hooks._checkStorage('grants', 'set');
if (!id) {
throw new Error("id is not set");
}
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) {
return new Buffer(text, 'utf8').toString('base64');
};
OAUTH3._defaultStorage = require('./oauth3.node.storage');
OAUTH3._node = {};
OAUTH3._node.discover = function(providerUri/*, opts*/) {

View File

@ -67,10 +67,9 @@ module.exports = {
, sessions: {
all: function (providerUri) {
var dirname = path.join(oauth3dir, 'sessions');
return fs.readdirAsync(dirname).then(function (nodes) {
return fs.readdirAsync(sessionsdir).then(function (nodes) {
return nodes.map(function (node) {
var result = require(path.join(dirname, node));
var result = require(path.join(sessionsdir, node));
if (result.link) {
return null;
}
@ -91,7 +90,7 @@ module.exports = {
result = require(path.join(sessionsdir, providerUri + '.json'));
// TODO make safer
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) {
@ -113,10 +112,9 @@ module.exports = {
});
}
, clear: function () {
var dirname = path.join(oauth3dir, 'sessions');
return fs.readdirAsync(dirname).then(function (nodes) {
return fs.readdirAsync(sessionsdir).then(function (nodes) {
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({
method: 'POST'
, 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
, data: {
domains: opts.data.domains

View File

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