diff --git a/.gitignore b/.gitignore index afde3c5..b4d6104 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +prefactor +.well-known node_modules/ DS_Store -.vscode \ No newline at end of file +.vscode diff --git a/README.md b/README.md index 46d5e7e..ba07ea3 100644 --- a/README.md +++ b/README.md @@ -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 `` to your `index.html` +9. Add `` to your `index.html` 9. Add `` 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 - + @@ -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) { diff --git a/bin/cli.js b/bin/cli.js index da3be23..5319555 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -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)' ] diff --git a/oauth3.core.js b/oauth3.core.js index a31940e..8c445d9 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -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": "<>", "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; }'); - 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; }'); - 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; }'); + , 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; }'); - 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; }'); - 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); }); }); diff --git a/oauth3.crypto.js b/oauth3.crypto.js index 67c27ff..d87a158 100644 --- a/oauth3.crypto.js +++ b/oauth3.crypto.js @@ -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)); diff --git a/oauth3.issuer.js b/oauth3.issuer.js index 2a7a9e4..f70b0ae 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -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": "<>", "scope": "<>" // , "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(); } }; diff --git a/oauth3.node.js b/oauth3.node.js index 95b0bd3..97fecd1 100644 --- a/oauth3.node.js +++ b/oauth3.node.js @@ -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*/) { diff --git a/oauth3.node.storage.js b/oauth3.node.storage.js index 370c678..c18046f 100644 --- a/oauth3.node.storage.js +++ b/oauth3.node.storage.js @@ -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)); })); }); } diff --git a/oauth3.tunnel.js b/oauth3.tunnel.js index 3ef5cbc..38bf827 100644 --- a/oauth3.tunnel.js +++ b/oauth3.tunnel.js @@ -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 diff --git a/well-known/oauth3/directives.json b/well-known/oauth3/directives.json index b710741..d546348 100644 --- a/well-known/oauth3/directives.json +++ b/well-known/oauth3/directives.json @@ -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/" } }