Merge remote-tracking branch 'origin/signing' into v1.0

This commit is contained in:
AJ ONeal 2017-03-22 10:00:47 -06:00
commit 5ed05f03cf
9 changed files with 597 additions and 55 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

@ -6,7 +6,7 @@ The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript im
Instead of bloating your webapp and ruining the mobile experience, Instead of bloating your webapp and ruining the mobile experience,
you can use a single, small javascript file for all OAuth3 providers you can use a single, small javascript file for all OAuth3 providers
(and almost all OAuth2 providers) with a seemless experience. (and almost all OAuth2 providers) with a seamless experience.
Also, instead of complicated (or worse - insecure) CLI and Desktop login methods, Also, instead of complicated (or worse - insecure) CLI and Desktop login methods,
you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit) you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit)
@ -74,7 +74,7 @@ function onClickLogin() {
console.info('Authentication was Successful:'); console.info('Authentication was Successful:');
console.log(session); console.log(session);
// You can use the PPID (or preferrably a hash of it) as the login for your app // You can use the PPID (or preferably a hash of it) as the login for your app
// (it securely functions as both username and password which is known only by your app) // (it securely functions as both username and password which is known only by your app)
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key // If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
// //
@ -168,7 +168,7 @@ pushd /path/to/your/web/app
# clone the project as assets/org.oauth3 # clone the project as assets/org.oauth3
mkdir -p assets mkdir -p assets
git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3 git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3
pushd assests/org.oauth3 pushd assets/org.oauth3
git checkout v1 git checkout v1
popd popd
@ -232,7 +232,7 @@ function onClickLogin() {
console.info('Authentication was Successful:'); console.info('Authentication was Successful:');
console.log(session); console.log(session);
// You can use the PPID (or preferrably a hash of it) as the login for your app // You can use the PPID (or preferably a hash of it) as the login for your app
// (it securely functions as both username and password which is known only by your app) // (it securely functions as both username and password which is known only by your app)
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key // If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
// //
@ -448,7 +448,7 @@ As a general rule I don't like rules that sometimes apply and sometimes don't,
so I may need to rethink this. However, there are cases where including the protocol so I may need to rethink this. However, there are cases where including the protocol
can be very ugly and confusing and we definitely need to allow relative paths. can be very ugly and confusing and we definitely need to allow relative paths.
A potential work-around would be to assume all paths are relative (elimitate #4 instead) A potential work-around would be to assume all paths are relative (eliminate #4 instead)
and have the path always key off of the base URL - if oauth3 directives are to be found at and have the path always key off of the base URL - if oauth3 directives are to be found at
https://example.com/username/.well-known/oauth3/directives.json then /api/whatever would refer https://example.com/username/.well-known/oauth3/directives.json then /api/whatever would refer
to https://example.com/username/api/whatever. to https://example.com/username/api/whatever.

View File

@ -0,0 +1,97 @@
;(function () {
'use strict';
var createHash = require('create-hash');
var pbkdf2 = require('pbkdf2');
var aes = require('browserify-aes');
var ec = require('elliptic/lib/elliptic/ec')('p256');
function sha256(buf) {
return createHash('sha256').update(buf).digest();
}
function runPbkdf2(password, salt) {
// Derived AES key is 128 bit, and the function takes a size in bytes.
return pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256');
}
function encrypt(key, iv, data) {
var cipher = aes.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv));
return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]);
}
function decrypt(key, iv, data) {
var decipher = aes.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv));
decipher.setAuthTag(Buffer(data.slice(-16)));
return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]);
}
function bnToBuffer(bn, size) {
var buf = bn.toArrayLike(Buffer);
if (!size || buf.length === size) {
return buf;
} else if (buf.length < size) {
return Buffer.concat([Buffer(size-buf.length).fill(0), buf]);
} else if (buf.length > size) {
throw new Error('EC signature number bigger than expected');
}
throw new Error('invalid size "'+size+'" converting BigNumber to Buffer');
}
function bnToB64(bn) {
var b64 = bnToBuffer(bn).toString('base64');
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, '');
}
function genEcdsaKeyPair() {
var key = ec.genKeyPair();
var pubJwk = {
key_ops: ['verify']
, kty: 'EC'
, crv: 'P-256'
, x: bnToB64(key.getPublic().getX())
, y: bnToB64(key.getPublic().getY())
};
var privJwk = JSON.parse(JSON.stringify(pubJwk));
privJwk.key_ops = ['sign'];
privJwk.d = bnToB64(key.getPrivate());
return {privateKey: privJwk, publicKey: pubJwk};
}
function sign(jwk, msg) {
var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64'));
var sig = key.sign(sha256(msg));
return Buffer.concat([bnToBuffer(sig.r, 32), bnToBuffer(sig.s, 32)]);
}
function verify(jwk, msg, signature) {
var key = ec.keyFromPublic({x: Buffer(jwk.x, 'base64'), y: Buffer(jwk.y, 'base64')});
var sig = {
r: Buffer(signature.slice(0, signature.length/2))
, s: Buffer(signature.slice(signature.length/2))
};
return key.verify(sha256(msg), sig);
}
function promiseWrap(func) {
return function() {
var args = arguments;
// This fallback file should only be used when the browser doesn't support everything we
// need with WebCrypto. Since it is only used in the browser we should be able to assume
// that OAUTH3 has been placed in the global scope and that we can access it here.
return new OAUTH3.PromiseA(function (resolve) {
resolve(func.apply(null, args));
});
};
}
exports.sha256 = promiseWrap(sha256);
exports.pbkdf2 = promiseWrap(runPbkdf2);
exports.encrypt = promiseWrap(encrypt);
exports.decrypt = promiseWrap(decrypt);
exports.sign = promiseWrap(sign);
exports.verify = promiseWrap(verify);
exports.genEcdsaKeyPair = promiseWrap(genEcdsaKeyPair);
}());

25
gulpfile.js Normal file
View File

@ -0,0 +1,25 @@
;(function () {
'use strict';
var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var streamify = require('gulp-streamify');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');
gulp.task('default', function () {
return browserify('./browserify/crypto.fallback.js', {standalone: 'OAUTH3_crypto_fallback'}).bundle()
.pipe(source('browserify/crypto.fallback.js'))
.pipe(rename('oauth3.crypto.fallback.js'))
.pipe(gulp.dest('./'))
.pipe(streamify(uglify()))
.pipe(rename('oauth3.crypto.fallback.min.js'))
.pipe(gulp.dest('./'))
;
});
gulp.task('watch', function () {
gulp.watch('browserify/*.js', [ 'default' ]);
});
}());

View File

@ -14,6 +14,27 @@
return err; return err;
} }
} }
, _binStr: {
bufferToBinStr: function (buf) {
return Array.prototype.map.call(new Uint8Array(buf), function(ch) {
return String.fromCharCode(ch);
}).join('');
}
, binStrToBuffer: function (str) {
var buf;
if ('undefined' !== typeof Uint8Array) {
buf = new Uint8Array(str.length);
} else {
buf = [];
}
Array.prototype.forEach.call(str, function (ch, ind) {
buf[ind] = ch.charCodeAt(0);
});
return buf;
}
}
, _base64: { , _base64: {
atob: function (base64) { atob: function (base64) {
// atob must be called from the global context // atob must be called from the global context
@ -43,6 +64,12 @@
b64 = b64.replace(/=+/g, ''); b64 = b64.replace(/=+/g, '');
return b64; return b64;
} }
, urlSafeToBuffer: function (str) {
return OAUTH3._binStr.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str));
}
, bufferToUrlSafe: function (buf) {
return OAUTH3._base64.encodeUrlSafe(OAUTH3._binStr.bufferToBinStr(buf));
}
} }
, uri: { , uri: {
normalize: function (uri) { normalize: function (uri) {
@ -181,15 +208,17 @@
// { header: {}, payload: {}, signature: '' } // { header: {}, payload: {}, signature: '' }
var parts = str.split(/\./g); var parts = str.split(/\./g);
var jsons = parts.slice(0, 2).map(function (urlsafe64) { var jsons = parts.slice(0, 2).map(function (urlsafe64) {
var b64 = OAUTH3._base64.decodeUrlSafe(urlsafe64); return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64));
return b64;
}); });
return { return { header: jsons[0], payload: jsons[1] };
header: JSON.parse(jsons[0]) }
, payload: JSON.parse(jsons[1]) , verify: function (jwk, token) {
, signature: parts[2] // should remain url-safe base64 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);
} }
, freshness: function (tokenMeta, staletime, _now) { , freshness: function (tokenMeta, staletime, _now) {
staletime = staletime || (15 * 60); staletime = staletime || (15 * 60);
@ -740,6 +769,7 @@
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params)); return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params));
} }
OAUTH3.hooks.session._cache = {};
return params; return params;
}); });
} }

293
oauth3.crypto.js Normal file
View File

@ -0,0 +1,293 @@
;(function (exports) {
'use strict';
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
OAUTH3.crypto = {};
try {
OAUTH3.crypto.core = require('./oauth3.node.crypto');
} catch (error) {
OAUTH3.crypto.core = {};
// We don't currently have a fallback method for this function, so we assign
// it directly to the core object instead of the webCrypto object.
OAUTH3.crypto.core.randomBytes = function (size) {
var buf = OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(size));
return OAUTH3.PromiseA.resolve(buf);
};
var webCrypto = {};
webCrypto.sha256 = function (buf) {
return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf);
};
webCrypto.pbkdf2 = function (password, salt) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey'])
.then(function (key) {
var opts = {name: 'PBKDF2', salt: salt, iterations: 8192, hash: {name: 'SHA-256'}};
return OAUTH3._browser.window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']);
})
.then(function (key) {
return OAUTH3._browser.window.crypto.subtle.exportKey('raw', key);
});
};
webCrypto.encrypt = function (rawKey, iv, data) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt'])
.then(function (key) {
return OAUTH3._browser.window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data);
});
};
webCrypto.decrypt = function (rawKey, iv, data) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt'])
.then(function (key) {
return OAUTH3._browser.window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data);
});
};
webCrypto.genEcdsaKeyPair = function () {
return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
.then(function (keyPair) {
return OAUTH3.PromiseA.all([
OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.privateKey)
, OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.publicKey)
]);
}).then(function (jwkPair) {
return { privateKey: jwkPair[0], publicKey: jwkPair[1] };
});
};
webCrypto.sign = function (jwk, msg) {
return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign'])
.then(function (key) {
return OAUTH3._browser.window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, msg);
})
.then(function (sig) {
return new Uint8Array(sig);
});
};
webCrypto.verify = function (jwk, msg, signature) {
// If the JWK has properties that should only exist on the private key or is missing
// "verify" in the key_ops, importing in as a public key won't work.
if (jwk.hasOwnProperty('d') || jwk.hasOwnProperty('key_ops')) {
jwk = JSON.parse(JSON.stringify(jwk));
delete jwk.d;
delete jwk.key_ops;
}
return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['verify'])
.then(function (key) {
return OAUTH3._browser.window.crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, msg);
});
};
function checkWebCrypto() {
var loadFallback = function() {
var prom;
loadFallback = function () { return prom; };
prom = new OAUTH3.PromiseA(function (resolve) {
var body = document.getElementsByTagName('body')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.onload = resolve;
script.onreadystatechange = function () {
if (this.readyState === 'complete' || this.readyState === 'loaded') {
resolve();
}
};
script.src = '/assets/org.oauth3/oauth3.crypto.fallback.js';
body.appendChild(script);
});
return prom;
};
function checkException(name, func) {
new OAUTH3.PromiseA(function (resolve) { resolve(func()); })
.then(function () {
OAUTH3.crypto.core[name] = webCrypto[name];
})
.catch(function (err) {
console.warn('error with WebCrypto', name, '- using fallback', err);
loadFallback().then(function () {
OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
});
});
}
function checkResult(name, expected, func) {
checkException(name, function () {
return func()
.then(function (result) {
if (typeof expected === typeof result) {
return result;
}
return OAUTH3._base64.bufferToUrlSafe(result);
})
.then(function (result) {
if (result !== expected) {
throw new Error("result ("+result+") doesn't match expectation ("+expected+")");
}
});
});
}
var zeroBuf = new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
var dataBuf = OAUTH3._base64.urlSafeToBuffer('1234567890abcdefghijklmn');
var keyBuf = OAUTH3._base64.urlSafeToBuffer('l_Aeoqk6ePjwjCYrlHrgrg');
var encBuf = OAUTH3._base64.urlSafeToBuffer('Ji_gEtcNElUONSR4Mf9S75davXjh_6-oQN9AgO5UF8rERw');
checkResult('sha256', 'BwMveUm2V1axuERvUoxM4dScgNl9yKhER9a6p80GXj4', function () {
return webCrypto.sha256(dataBuf);
});
checkResult('pbkdf2', OAUTH3._base64.bufferToUrlSafe(keyBuf), function () {
return webCrypto.pbkdf2('password', zeroBuf);
});
checkResult('encrypt', OAUTH3._base64.bufferToUrlSafe(encBuf), function () {
return webCrypto.encrypt(keyBuf, zeroBuf.slice(0, 12), dataBuf);
});
checkResult('decrypt', OAUTH3._base64.bufferToUrlSafe(dataBuf), function () {
return webCrypto.decrypt(keyBuf, zeroBuf.slice(0, 12), encBuf);
});
var jwk = {
kty: "EC"
, crv: "P-256"
, d: "ChXx7ea5YtEltCufA8CVb0lQv3glcCfcSpEgdedgIP0"
, x: "Akt5ZDbytcKS5UQMURvGb_UIMS4qFctDwrX8bX22ato"
, y: "cV7nhpWNT1FeRIbdold4jLtgsEpZBFcNy3p2E5mqvto"
};
var sig = OAUTH3._base64.urlSafeToBuffer('nc3F8qeP8OXpfqPD9tTcFQg0Wfp37RTAppLPIKE1ZupR_8Aba64hNExwd1dOk802OFQxaECPDZCkKe7WA9RXAg');
checkResult('verify', true, function() {
return webCrypto.verify(jwk, dataBuf, sig);
});
// The results of these functions are less predictable, so we can't check their return value.
checkException('genEcdsaKeyPair', function () {
return webCrypto.genEcdsaKeyPair();
});
checkException('sign', function () {
return webCrypto.sign(jwk, dataBuf);
});
}
checkWebCrypto();
}
OAUTH3.crypto.thumbprintJwk = function (jwk) {
var keys;
if (jwk.kty === 'EC') {
keys = ['crv', 'x', 'y'];
} else if (jwk.kty === 'RSA') {
keys = ['e', 'n'];
} else if (jwk.kty === 'oct') {
keys = ['k'];
} else {
return OAUTH3.PromiseA.reject(new Error('invalid JWK key type ' + jwk.kty));
}
keys.push('kty');
keys.sort();
var missing = keys.filter(function (name) { return !jwk.hasOwnProperty(name); });
if (missing.length > 0) {
return OAUTH3.PromiseA.reject(new Error('JWK of type '+jwk.kty+' missing fields ' + missing));
}
var jwkStr = '{' + keys.map(function (name) { return name+':'+jwk[name]; }).join(',') + '}';
return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(jwkStr))
.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) {
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;
});
});
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) {
var kek = results[0];
var keyPair = results[1];
var salt = results[2];
var userSecret = results[3];
var ecdsaIv = results[4];
var secretIv = results[5];
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) {
return {
publicKey: keyPair.publicKey
, privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0])
, userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1])
, salt: OAUTH3._base64.bufferToUrlSafe(salt)
, ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv)
, secretIv: OAUTH3._base64.bufferToUrlSafe(secretIv)
};
});
});
};
OAUTH3.crypto._decryptKey = function (ppid, storedObj) {
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)
.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);
});
});
};
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,30 +3,6 @@
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
OAUTH3._base64.btoa = function (b64) {
// http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome
return (exports.btoa || require('btoa'))(b64);
};
OAUTH3._base64.encodeUrlSafe = function (b64) {
// Base64 to URL-safe Base64
b64 = b64.replace(/\+/g, '-').replace(/\//g, '_');
b64 = b64.replace(/=+/g, '');
return OAUTH3._base64.btoa(b64);
};
OAUTH3.jwt.encode = function (parts) {
parts.header = parts.header || { alg: 'none', typ: 'jwt' };
parts.signature = parts.signature || '';
var result = [
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('.');
return result;
};
OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
var providerUri = directive.issuer; var providerUri = directive.issuer;
@ -50,12 +26,12 @@
}; };
OAUTH3.authz.scopes = function () { OAUTH3.authz.scopes = function () {
return { return OAUTH3.PromiseA.resolve({
pending: ['oauth3_authn'] // not yet accepted pending: ['oauth3_authn'] // not yet accepted
, granted: [] // all granted, ever , granted: [] // all granted, ever
, requested: ['oauth3_authn'] // all requested, now , requested: ['oauth3_authn'] // all requested, now
, accepted: [] // granted (ever) and requested (now) , accepted: [] // granted (ever) and requested (now)
}; });
}; };
OAUTH3.authz.grants = function (providerUri, opts) { OAUTH3.authz.grants = function (providerUri, opts) {
if ('POST' === opts.method) { if ('POST' === opts.method) {
@ -82,12 +58,8 @@
}; };
OAUTH3._mockToken = function (providerUri, opts) { OAUTH3._mockToken = function (providerUri, opts) {
var accessToken = OAUTH3.jwt.encode({ var payload = { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope };
header: { alg: 'none' } return OAUTH3.crypto._signPayload(payload).then(function (accessToken) {
, payload: { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope }
, signature: "fakeSig"
});
return OAUTH3.hooks.session.refresh( return OAUTH3.hooks.session.refresh(
opts.session || { opts.session || {
provider_uri: providerUri provider_uri: providerUri
@ -100,6 +72,7 @@
, scope: opts.scope , scope: opts.scope
} }
); );
});
}; };
}('undefined' !== typeof exports ? exports : window)); }('undefined' !== typeof exports ? exports : window));

106
oauth3.node.crypto.js Normal file
View File

@ -0,0 +1,106 @@
;(function () {
'use strict';
var crypto = require('crypto');
var OAUTH3 = require('./oauth3.core.js').OAUTH3;
var ec = require('elliptic').ec('p256');
function randomBytes(size) {
return new OAUTH3.PromiseA(function (resolve, reject) {
crypto.randomBytes(size, function (err, buf) {
if (err) {
reject(err);
} else {
resolve(buf);
}
});
});
}
function sha256(buf) {
return crypto.createHash('sha256').update(buf).digest();
}
function pbkdf2(password, salt) {
// Derived AES key is 128 bit, and the function takes a size in bytes.
return crypto.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256');
}
function encrypt(key, iv, data) {
var cipher = crypto.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv));
return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]);
}
function decrypt(key, iv, data) {
var decipher = crypto.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv));
decipher.setAuthTag(Buffer(data.slice(-16)));
return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]);
}
function bnToBuffer(bn, size) {
var buf = bn.toArrayLike(Buffer);
if (!size || buf.length === size) {
return buf;
} else if (buf.length < size) {
return Buffer.concat([Buffer(size-buf.length).fill(0), buf]);
} else if (buf.length > size) {
throw new Error('EC signature number bigger than expected');
}
throw new Error('invalid size "'+size+'" converting BigNumber to Buffer');
}
function bnToB64(bn) {
var b64 = bnToBuffer(bn).toString('base64');
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, '');
}
function genEcdsaKeyPair() {
var key = ec.genKeyPair();
var pubJwk = {
key_ops: ['verify']
, kty: 'EC'
, crv: 'P-256'
, x: bnToB64(key.getPublic().getX())
, y: bnToB64(key.getPublic().getY())
};
var privJwk = JSON.parse(JSON.stringify(pubJwk));
privJwk.key_ops = ['sign'];
privJwk.d = bnToB64(key.getPrivate());
return {privateKey: privJwk, publicKey: pubJwk};
}
function sign(jwk, msg) {
var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64'));
var sig = key.sign(sha256(msg));
return Buffer.concat([bnToBuffer(sig.r, 32), bnToBuffer(sig.s, 32)]);
}
function verify(jwk, msg, signature) {
var key = ec.keyFromPublic({x: Buffer(jwk.x, 'base64'), y: Buffer(jwk.y, 'base64')});
var sig = {
r: Buffer(signature.slice(0, signature.length/2))
, s: Buffer(signature.slice(signature.length/2))
};
return key.verify(sha256(msg), sig);
}
function promiseWrap(func) {
return function() {
var args = arguments;
return new OAUTH3.PromiseA(function (resolve) {
resolve(func.apply(null, args));
});
};
}
exports.sha256 = promiseWrap(sha256);
exports.pbkdf2 = promiseWrap(pbkdf2);
exports.encrypt = promiseWrap(encrypt);
exports.decrypt = promiseWrap(decrypt);
exports.sign = promiseWrap(sign);
exports.verify = promiseWrap(verify);
exports.genEcdsaKeyPair = promiseWrap(genEcdsaKeyPair);
exports.randomBytes = randomBytes;
}());

View File

@ -4,6 +4,7 @@
"description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.", "description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.",
"main": "oauth3.node.js", "main": "oauth3.node.js",
"scripts": { "scripts": {
"install": "./node_modules/.bin/gulp",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
@ -30,6 +31,22 @@
"log", "log",
"sign" "sign"
], ],
"dependencies": {
"elliptic": "^6.4.0"
},
"devDependencies": {
"browserify-aes": "^1.0.6",
"create-hash": "^1.1.2",
"pbkdf2": "^3.0.9",
"browserify": "^14.1.0",
"gulp": "^3.9.1",
"gulp-cli": "^1.2.2",
"gulp-rename": "^1.2.2",
"gulp-streamify": "^1.0.2",
"gulp-uglify": "^2.1.0",
"vinyl-source-stream": "^1.1.0"
},
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)" "license": "(MIT OR Apache-2.0)"
} }