From c2b6a91716c2e9a33e6cb0414bf849aa2f4fc20f Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 21 Feb 2017 11:49:41 -0700 Subject: [PATCH] clean up, clean up, everybody everywhere --- oauth3.implicit.js | 241 +++++++++++++++++++++--------------------- oauth3.issuer.js | 81 +++++++------- oauth3.issuer.mock.js | 17 +-- 3 files changed, 168 insertions(+), 171 deletions(-) diff --git a/oauth3.implicit.js b/oauth3.implicit.js index 87d0c4f..4c27c4d 100644 --- a/oauth3.implicit.js +++ b/oauth3.implicit.js @@ -3,22 +3,22 @@ 'use strict'; var OAUTH3 = exports.OAUTH3 = { - utils: { - clientUri: function (location) { - return OAUTH3.utils.uri.normalize(location.host + location.pathname); + clientUri: function (location) { + return OAUTH3.utils.uri.normalize(location.host + location.pathname); + } + , error: { + parse: function (providerUri, params) { + var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'"); + err.uri = params.error_uri || params.error.uri; + err.code = params.error.code || params.error; + return err; } - , _error: { - parse: function (providerUri, params) { - var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'"); - err.uri = params.error_uri || params.error.uri; - err.code = params.error.code || params.error; - return err; - } - } - , atob: function (base64) { + } + , _base64: { + atob: function (base64) { return (exports.atob || require('atob'))(base64); } - , _urlSafeBase64ToBase64: function (b64) { + , decodeUrlSafe: function (b64) { // URL-safe Base64 to Base64 // https://en.wikipedia.org/wiki/Base64 // https://gist.github.com/catwell/3046205 @@ -26,105 +26,105 @@ if (2 === mod) { b64 += '=='; } if (3 === mod) { b64 += '='; } b64 = b64.replace(/-/g, '+').replace(/_/g, '/'); - return b64; + return OAUTH3._base64.atob(b64); } - , uri: { - normalize: function (uri) { - if ('string' !== typeof uri) { - console.error((new Error('stack')).stack); - } - // tested with - // example.com - // example.com/ - // http://example.com - // https://example.com/ - return uri - .replace(/^(https?:\/\/)?/i, '') - .replace(/\/?$/, '') - ; + } + , uri: { + normalize: function (uri) { + if ('string' !== typeof uri) { + console.error((new Error('stack')).stack); } + // tested with + // example.com + // example.com/ + // http://example.com + // https://example.com/ + return uri + .replace(/^(https?:\/\/)?/i, '') + .replace(/\/?$/, '') + ; } - , url: { - normalize: function (url) { - if ('string' !== typeof url) { - console.error((new Error('stack')).stack); - } - // tested with - // example.com - // example.com/ - // http://example.com - // https://example.com/ - return url - .replace(/^(https?:\/\/)?/i, 'https://') - .replace(/\/?$/, '') - ; - } - , resolve: function (base, next) { - if (/^https:\/\//i.test(next)) { - return next; - } - return this.normalize(base) + '/' + this._normalizePath(next); - } - , _normalizePath: function (path) { - return path.replace(/^\//, '').replace(/\/$/, ''); + } + , url: { + normalize: function (url) { + if ('string' !== typeof url) { + console.error((new Error('stack')).stack); } + // tested with + // example.com + // example.com/ + // http://example.com + // https://example.com/ + return url + .replace(/^(https?:\/\/)?/i, 'https://') + .replace(/\/?$/, '') + ; } - , query: { - stringify: function (params) { - var qs = []; + , resolve: function (base, next) { + if (/^https:\/\//i.test(next)) { + return next; + } + return this.normalize(base) + '/' + this._normalizePath(next); + } + , _normalizePath: function (path) { + return path.replace(/^\//, '').replace(/\/$/, ''); + } + } + , query: { + stringify: function (params) { + var qs = []; - Object.keys(params).forEach(function (key) { - // TODO nullify instead? - if ('undefined' === typeof params[key]) { - return; - } - - if ('scope' === key) { - params[key] = OAUTH3.utils.scope.stringify(params[key]); - } - - qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); - }); - - return qs.join('&'); - } - } - , scope: { - stringify: function (scope) { - if (Array.isArray(scope)) { - scope = scope.join(' '); + Object.keys(params).forEach(function (key) { + // TODO nullify instead? + if ('undefined' === typeof params[key]) { + return; } - return scope; - } + + if ('scope' === key) { + params[key] = OAUTH3.utils.scope.stringify(params[key]); + } + + qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); + }); + + return qs.join('&'); } - , randomState: function () { - // TODO put in different file for browser vs node - try { - return Array.prototype.slice.call( - window.crypto.getRandomValues(new Uint8Array(16)) - ).map(function (ch) { return (ch).toString(16); }).join(''); - } catch(e) { - return OAUTH3.utils._insecureRandomState(); + } + , scope: { + stringify: function (scope) { + if (Array.isArray(scope)) { + scope = scope.join(' '); } + return scope; } - , _insecureRandomState: function () { - var i; - var ch; - var str; - // TODO use fisher-yates on 0..255 and select [0] 16 times - // [security] https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.5qx0bf95a - // https://github.com/v8/v8/blob/b0e4dce6091a8777bda80d962df76525dc6c5ea9/src/js/math.js#L135-L144 - // Note: newer versions of v8 do not have this bug, but other engines may still - console.warn('[security] crypto.getRandomValues() failed, falling back to Math.random()'); - str = ''; - for (i = 0; i < 32; i += 1) { - ch = Math.round(Math.random() * 255).toString(16); - if (ch.length < 2) { ch = '0' + ch; } - str += ch; - } - return str; + } + , randomState: function () { + // TODO put in different file for browser vs node + try { + return Array.prototype.slice.call( + window.crypto.getRandomValues(new Uint8Array(16)) + ).map(function (ch) { return (ch).toString(16); }).join(''); + } catch(e) { + return OAUTH3.utils._insecureRandomState(); } } + , _insecureRandomState: function () { + var i; + var ch; + var str; + // TODO use fisher-yates on 0..255 and select [0] 16 times + // [security] https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.5qx0bf95a + // https://github.com/v8/v8/blob/b0e4dce6091a8777bda80d962df76525dc6c5ea9/src/js/math.js#L135-L144 + // Note: newer versions of v8 do not have this bug, but other engines may still + console.warn('[security] crypto.getRandomValues() failed, falling back to Math.random()'); + str = ''; + for (i = 0; i < 32; i += 1) { + ch = Math.round(Math.random() * 255).toString(16); + if (ch.length < 2) { ch = '0' + ch; } + str += ch; + } + return str; + } , jwt: { // decode only (no verification) decode: function (str) { @@ -135,9 +135,8 @@ // { header: {}, payload: {}, signature: '' } var parts = str.split(/\./g); var jsons = parts.slice(0, 2).map(function (urlsafe64) { - var atob = exports.atob || require('atob'); - var b64 = OAUTH3.utils._urlSafeBase64ToBase64(urlsafe64); - return atob(b64); + var b64 = OAUTH3._base64.decodeUrlSafe(urlsafe64); + return b64; }); return { @@ -161,22 +160,6 @@ return 'stale'; } - /* - // encode-only (no signature) - , encode: function (parts) { - parts.header = parts.header || { alg: 'none', typ: 'jwt' }; - parts.signature = parts.signature || ''; - - var btoa = exports.btoa || require('btoa'); - var result = [ - core.utils.base64ToUrlSafeBase64(btoa(JSON.stringify(parts.header, null))) - , core.utils.base64ToUrlSafeBase64(btoa(JSON.stringify(parts.payload, null))) - , parts.signature // should already be url-safe base64 - ].join('.'); - - return result; - } - */ } , urls: { discover: function (providerUri, opts) { @@ -727,7 +710,7 @@ } // TODO params should have response_type indicating json, binary, etc - var directives = JSON.parse(OAUTH3.utils.atob(OAUTH3.utils._urlSafeBase64ToBase64(params.result || params.directives))); + var directives = JSON.parse(OAUTH3._base64.decodeUrlSafe(params.result || params.directives)); // caller will call OAUTH3.hooks.directives._set(providerUri, directives); return directives; }); @@ -917,10 +900,24 @@ } } }; - OAUTH3.utils._formatError = OAUTH3.utils._error.parse; + OAUTH3.login = OAUTH3.implicitGrant; + + // TODO get rid of these + OAUTH3.utils = { + clientUri: OAUTH3.clientUri + , query: OAUTH3.query + , scope: OAUTH3.scope + , uri: OAUTH3.uri + , url: OAUTH3.url + , _error: OAUTH3.error + , _formatError: OAUTH3.error + , _urlSafeBase64ToBase64: OAUTH3._urlSafeBase64ToBase64 + , randomState: OAUTH3.randomState + , _insecureRandomState: OAUTH3._insecureRandomState + }; if ('undefined' !== typeof Promise) { OAUTH3.PromiseA = Promise; } -}('undefined' !== typeof exports ? exports : window)); \ No newline at end of file +}('undefined' !== typeof exports ? exports : window)); diff --git a/oauth3.issuer.js b/oauth3.issuer.js index 1482fc6..55c83c9 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -1,8 +1,9 @@ -/* global Promise */ ;(function (exports) { 'use strict'; -OAUTH3.utils.query.parse = function (search) { +var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.implicit.js').OAUTH3; + +OAUTH3.query.parse = function (search) { // parse a query or a hash if (-1 !== ['#', '?'].indexOf(search[0])) { search = search.substring(1); @@ -32,19 +33,19 @@ OAUTH3.utils.query.parse = function (search) { } return argsParsed; }; -OAUTH3.utils.scope.parse = function (scope) { +OAUTH3.scope.parse = function (scope) { return (scope||'').split(/[, ]/g); }; -OAUTH3.utils.url.parse = function (url) { +OAUTH3.url.parse = function (url) { // TODO browser // Node should replace this var parser = document.createElement('a'); parser.href = url; return parser; }; -OAUTH3.utils.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) { - var src = OAUTH3.utils.url.parse(referrerUrl); - var dst = OAUTH3.utils.url.parse(redirectUrl); +OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) { + var src = OAUTH3.url.parse(referrerUrl); + var dst = OAUTH3.url.parse(redirectUrl); // TODO how should we handle subdomains? // It should be safe for api.example.com to redirect to example.com @@ -55,11 +56,11 @@ OAUTH3.utils.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) { // api.example.com.evil.com SHOULD NOT match example.com return dst.hostname === src.hostname; }; -OAUTH3.utils.url.checkRedirect = function (client, query) { +OAUTH3.url.checkRedirect = function (client, query) { console.warn("[security] URL path checking not yet implemented"); - var clientUrl = OAUTH3.utils.url.normalize(client.url); - var redirectUrl = OAUTH3.utils.url.normalize(query.redirect_uri); + var clientUrl = OAUTH3.url.normalize(client.url); + var redirectUrl = OAUTH3.url.normalize(query.redirect_uri); // General rule: // I can callback to a shorter domain (fewer subs) or a shorter path (on the same domain) @@ -67,13 +68,13 @@ OAUTH3.utils.url.checkRedirect = function (client, query) { // We can callback to an explicitly listed domain (TODO and path) - if (OAUTH3.utils.url._isRedirectHostSafe(clientUrl, redirectUrl)) { + if (OAUTH3.url._isRedirectHostSafe(clientUrl, redirectUrl)) { return true; } return false; }; -OAUTH3.utils.url.redirect = function (clientParams, grants, tokenOrError) { +OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) { // TODO OAUTH3.redirect(clientParams, grants, tokenOrError) // TODO check redirect safeness right here with grants.client.urls // TODO check for '#' and '?'. If none, issue warning and use '?' (for backwards compat) @@ -93,13 +94,13 @@ OAUTH3.utils.url.redirect = function (clientParams, grants, tokenOrError) { authz.error_description = tokenOrError.error.message || tokenOrError.error_description; authz.error_uri = tokenOrError.error.uri || tokenOrError.error_uri; } - var redirect = clientParams.redirect_uri + '#' + window.OAUTH3.utils.query.stringify(authz); + var redirect = clientParams.redirect_uri + '#' + window.OAUTH3.query.stringify(authz); if (clientParams.debug) { console.info('final redirect_uri:', redirect); window.alert("You're in debug mode so we've taken a pause. Hit OK to continue"); } - + window.location = redirect; }; @@ -164,11 +165,11 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { } if (scope) { - params.scope = OAUTH3.utils.scope.stringify(scope); + params.scope = OAUTH3.scope.stringify(scope); } if ('GET' === args.method.toUpperCase()) { - uri += '?' + OAUTH3.utils.query.stringify(params); + uri += '?' + OAUTH3.query.stringify(params); } else { body = params; } @@ -207,8 +208,8 @@ OAUTH3.urls.grants = function (directive, opts) { } } - var url = OAUTH3.utils.url.resolve(directive.issuer, directive.grants.url) - .replace(/(:azp|:client_id)/g, OAUTH3.utils.uri.normalize(opts.client_id || opts.client_uri)) + var url = OAUTH3.url.resolve(directive.issuer, directive.grants.url) + .replace(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri)) .replace(/(:sub|:account_id)/g, opts.session.token.sub) ; var data = { @@ -222,7 +223,7 @@ OAUTH3.urls.grants = function (directive, opts) { var body; if ('GET' === opts.method) { - url += '?' + OAUTH3.utils.query.stringify(data); + url += '?' + OAUTH3.query.stringify(data); } else { body = data; @@ -237,9 +238,7 @@ OAUTH3.urls.grants = function (directive, opts) { }; OAUTH3.authn = {}; - -OAUTH3.authz = {}; -OAUTH3.authz.loginMeta = function (directive, opts) { +OAUTH3.authn.loginMeta = OAUTH3.authz.loginMeta = function (directive, opts) { if (opts.mock) { if (opts.mockError) { return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}}); @@ -250,23 +249,22 @@ OAUTH3.authz.loginMeta = function (directive, opts) { return OAUTH3.request({ method: directive.credential_meta.method || 'GET' // TODO lint urls - , url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_meta.url) + , url: OAUTH3.url.resolve(directive.issuer, directive.credential_meta.url) .replace(':type', 'email') .replace(':id', opts.email) }); }; - -OAUTH3.authz.otp = function (directive, opts) { +OAUTH3.authn.otp = OAUTH3.authz.otp = function (directive, opts) { if (opts.mock) { if (opts.mockError) { return OAUTH3.PromiseA.resolve({data: {error: {message: "Yikes!", code: 'E'}}}); } return OAUTH3.PromiseA.resolve({data: {uuid: "uuidblah"}}); } - + return OAUTH3.request({ method: directive.credential_otp.url.method || 'POST' - , url: OAUTH3.utils.url.resolve(directive.issuer, directive.credential_otp.url) + , url: OAUTH3.url.resolve(directive.issuer, directive.credential_otp.url) , data: { // TODO replace with signed hosted file client_agree_tos: 'oauth3.org/tos/draft' @@ -277,8 +275,7 @@ OAUTH3.authz.otp = function (directive, opts) { } }); }; - -OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { +OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { console.log('ginger bread man'); var providerUri = directive.issuer; @@ -291,7 +288,7 @@ OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { var data = req.data; data.provider_uri = providerUri; if (data.error) { - return oauth3.PromiseA.reject(OAUTH3.utils._error.parse(providerUri, data.error)); + return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data.error)); } return OAUTH3.hooks.refreshSession( opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri } @@ -300,6 +297,8 @@ OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { }); }); }; + +OAUTH3.authz = {}; OAUTH3.authz.scopes = function (providerUri, session, clientParams) { if (clientParams.mock) { return { @@ -312,7 +311,7 @@ OAUTH3.authz.scopes = function (providerUri, session, clientParams) { // OAuth3.requests.grants(providerUri, {}); // return list of grants // OAuth3.checkGrants(providerUri, {}); // - var clientUri = OAUTH3.utils.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer); + var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer); var scope = clientParams.scope || ''; var clientObj = clientParams; @@ -348,14 +347,14 @@ OAUTH3.authz.scopes = function (providerUri, session, clientParams) { // 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.utils.url.checkRedirect(grantResults.client, clientObj)) { + 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.utils.uri.normalize(window.document.referrer) + + '&referrer_uri=' + OAUTH3.uri.normalize(window.document.referrer) ; - if (opts.debug) { + if (clientParams.debug) { console.log('grantResults Redirect Attack'); console.log(grantResults); console.log(clientObj); @@ -424,21 +423,21 @@ OAUTH3.authz.grants = function (providerUri, opts) { var grants = grantsResult.originalData || grantsResult.data; // TODO if (grants.error) { - return oauth3.PromiseA.reject(oauth3.utils._formatError(grants.error)); + return OAUTH3.PromiseA.reject(OAUTH3.error.parse(grants.error)); } console.warn('requests.grants', grants); - oauth3.hooks.setGrants(opts.client_id + '-client', grants.client); + 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.setGrants(clientId, [ grant ]); + OAUTH3.hooks.grants.set(clientId, [ grant ]); }); return { - client: oauth3.hooks.getGrants(opts.client_id + '-client') - , grants: oauth3.hooks.getGrants(opts.client_id) || [] + client: OAUTH3.hooks.grants.get(opts.client_id + '-client') + , grants: OAUTH3.hooks.grants.get(opts.client_id) || [] }; }); }); @@ -464,7 +463,7 @@ OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, s console.info('generate token results'); console.info(results); - OAUTH3.utils.url.redirect(clientParams, scopes, results); + OAUTH3.url.redirect(clientParams, scopes, results); }); } else if ('code' === clientParams.response_type) { @@ -548,4 +547,4 @@ OAUTH3._browser.isIframe = function isIframe () { } }; -}('undefined' !== typeof exports ? exports : window)); \ No newline at end of file +}('undefined' !== typeof exports ? exports : window)); diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index cb7da28..24693de 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -1,22 +1,23 @@ -/* global Promise */ ;(function (exports) { 'use strict'; - OAUTH3.utils._base64ToUrlSafeBase64 = function (b64) { + var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.implicit.js').OAUTH3; + + OAUTH3._base64.btoa = exports.btoa || require('btoa'); + OAUTH3._base64.encodeUrlSafe = function (b64) { // Base64 to URL-safe Base64 b64 = b64.replace(/\+/g, '-').replace(/\//g, '_'); b64 = b64.replace(/=+/g, ''); - return b64; + return OAUTH3._base64.btoa(b64); }; OAUTH3.jwt.encode = function (parts) { parts.header = parts.header || { alg: 'none', typ: 'jwt' }; parts.signature = parts.signature || ''; - var btoa = exports.btoa || require('btoa'); var result = [ - OAUTH3.utils._base64ToUrlSafeBase64(btoa(JSON.stringify(parts.header, null))) - , OAUTH3.utils._base64ToUrlSafeBase64(btoa(JSON.stringify(parts.payload, null))) + OAUTH3._base64.encodeUrlSafe(JSON.stringify(parts.header, null)) + , OAUTH3._base64.encodeUrlSafe(JSON.stringify(parts.payload, null)) , parts.signature // should already be url-safe base64 ].join('.'); @@ -63,7 +64,7 @@ , payload: { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope } , signature: "fakeSig" }); - + return OAUTH3.hooks.session.refresh( opts.session || { provider_uri: providerUri @@ -78,4 +79,4 @@ ); }; -}('undefined' !== typeof exports ? exports : window)); \ No newline at end of file +}('undefined' !== typeof exports ? exports : window));