diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7f951bf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "node_modules/terminal-forms.js"] + path = node_modules/terminal-forms.js + url = git@git.daplie.com:/OAuth3/terminal-forms.js diff --git a/bin/oauth3.js b/bin/oauth3.js new file mode 100644 index 0000000..2f74aaf --- /dev/null +++ b/bin/oauth3.js @@ -0,0 +1,102 @@ +'use strict'; + +// process.stdout.isTTY +var form = require('terminal-forms.js').create(process.stdin, process.stdout); +var OAUTH3 = require('../oauth3.node.js'); +// TODO change to ._hooks +OAUTH3.hooks.directives._get = require('../oauth3.node.storage.js').directives._get; +OAUTH3.hooks.directives._set = require('../oauth3.node.storage.js').directives._set; +OAUTH3.hooks.session._get = require('../oauth3.node.storage.js').session._get; +OAUTH3.hooks.session._set = require('../oauth3.node.storage.js').session._set; +var url = require('url'); +//console.log('stdin tty', process.stdin.isTTY); +//console.log('stdout tty', process.stdout.isTTY); + +form.ask({ label: "What's your OAuth3 Provider URL? ", type: 'url' }).then(function (urlResult) { + var urlObj = url.parse(urlResult.result || urlResult.input); + // TODO get unique client id for bootstrapping app + var oauth3 = OAUTH3.create(urlObj); + var providerPromise = oauth3.setProvider(urlObj.host + urlObj.pathname); + + function getCurrentUserEmail() { + return form.ask({ label: "What's your email (or cloud mail) address? ", type: 'email' }).then(function (emailResult) { + // TODO lookup uuid locally before performing loginMeta + // TODO lookup token locally before performing loginMeta / otp + return providerPromise.then(function () { + return OAUTH3.authn.loginMeta(oauth3._providerDirectives, { email: emailResult.input }).then(function (/*result*/) { + return emailResult.input; + }, function (/*err*/) { + // TODO require hashcash to create user account + function confirmCreateAccount() { + // TODO directives should specify private (invite-only) vs internal (request) vs public (allow) accounts + return form.ask({ + label: "We don't recognize that address. Do you want to create a new account? [Y/n] " + , type: 'text' // TODO boolean with default Y or N + }).then(function (result) { + if (!result.input) { + result.input = 'Y'; + } + + result.input = result.input.toLowerCase(); + + if ('y' !== result.input) { + return getCurrentUserEmail(); + } + + return emailResult.input; + }); + } + + return confirmCreateAccount(); + }); + }); + }); + } + + return getCurrentUserEmail().then(function (email) { + // TODO skip if token exists locally + form.println("Sending login code to '" + email + "'..."); + return OAUTH3.authn.otp(oauth3._providerDirectives, { email: email }).then(function (otpResult) { + return form.ask({ + label: "What's your login code? " + , help: "(it was sent to '" + email + "' and looks like 1234-5678-9012)" + // onkeyup + // ondebounce + // onchange + // regexp // html5 name? + , onReturnAsync: function (rs, ws, input/*, ch*/) { + var formatted = input.toLowerCase().replace(/[^\d]+/g, ''); + + if (12 !== formatted.length) { + return form.PromiseA.reject(new Error("invalid code please try again in the format xxxx-yyyy-zzzz")); + } + + formatted = formatted.match(/.{4,4}/g).join('-'); + + if (14 !== formatted.split('').length) { + return form.PromiseA.reject(new Error("invalid code '" + formatted + "', please try again xxxx-yyyy-zzzz")); + } + + var data = { + username: email + , username_type: 'email' + , client_id: OAUTH3.uri.normalize(oauth3._providerDirectives.issuer) + , client_uri: OAUTH3.uri.normalize(oauth3._providerDirectives.issuer) + , otp_code: formatted + , otp_uuid: otpResult.data.uuid + }; + + // returns session instead of input + var colors = require('colors'); + form.setStatus(colors.dim("authenticating with server...")); + return OAUTH3.authn.resourceOwnerPassword(oauth3._providerDirectives, data); + } + }).then(function (results) { + var session = results.result; + + form.println('session:'); + form.println(session); + }); + }); + }); +}); diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..ab2e3cd --- /dev/null +++ b/bower.json @@ -0,0 +1,37 @@ +{ + "name": "oauth3.js", + "authors": [ + "AJ ONeal " + ], + "description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.", + "main": "oauth3.core.js", + "keywords": [ + "oauth", + "oauth2", + "oauth3", + "oidc", + "openid", + "connect", + "openidconnect", + "authn", + "authz", + "authentication", + "authorization", + "user", + "password", + "passphrase", + "login", + "signin", + "log", + "sign" + ], + "license": "MIT", + "homepage": "https://git.daplie.com/OAuth3/oauth3.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/bump-versions.sh b/bump-versions.sh new file mode 100644 index 0000000..e7875b5 --- /dev/null +++ b/bump-versions.sh @@ -0,0 +1,14 @@ +git push --tags + +git checkout v1.0 +git push + +git checkout v1 +git merge v1.0 +git push + +git checkout master +git merge v1 +git push + +git checkout v1.0 diff --git a/dns.examples.js b/dns.examples.js new file mode 100644 index 0000000..a6be988 --- /dev/null +++ b/dns.examples.js @@ -0,0 +1,48 @@ +oauth3.api('devices.list').then(function (result) { + console.log(result); +}); + +oauth3.api( + 'devices.set' +, { data: { + name: 'tester.local' + , uid: 'test-01-uid' + , addresses: [ + { + type: 'A' + , address: '192.168.1.104' + } + ] + } } +).then(function (result) { + console.log('devices.set'); + console.log(result); +}); + +// TODO don't allow attaching if the device is not set +// TODO update API as well +oauth3.api( + 'devices.attach' +, { data: { + sub: 'test-01' + , sld: 'aj' + , tld: 'daplie.me' + , uid: 'test-01-uid' + } } +).then(function (result) { + console.log('devices.attach'); + console.log(result); +}); + +oauth3.api( + 'devices.detach' +, { data: { + sub: 'test-01' + , sld: 'aj' + , tld: 'daplie.me' + , uid: 'test-01-uid' + } } +).then(function (result) { + console.log('devices.detach'); + console.log(result); +}); diff --git a/node_modules/terminal-forms.js b/node_modules/terminal-forms.js new file mode 160000 index 0000000..66d46ea --- /dev/null +++ b/node_modules/terminal-forms.js @@ -0,0 +1 @@ +Subproject commit 66d46eab32d8014f43307f9fbe97027b8c913f7a diff --git a/oauth3.core.js b/oauth3.core.js index fd1a421..acf488f 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -4,7 +4,7 @@ var OAUTH3 = exports.OAUTH3 = { clientUri: function (location) { - return OAUTH3.uri.normalize(location.host + location.pathname); + return OAUTH3.uri.normalize(location.host + (location.pathname || '')); } , error: { parse: function (providerUri, params) { @@ -175,7 +175,7 @@ // TODO put in different file for browser vs node try { return Array.prototype.slice.call( - window.crypto.getRandomValues(new Uint8Array(16)) + OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(16)) ).map(function (ch) { return (ch).toString(16); }).join(''); } catch(e) { return OAUTH3.utils._insecureRandomState(); @@ -616,6 +616,11 @@ return OAUTH3.hooks.session.check(preq, opts).then(fetch); } , _requestHelper: function (preq, opts) { + /* + if (opts && opts.directives) { + preq.url = OAUTH3.url.resolve(opts.directives.issuer, preq.url); + } + */ return OAUTH3._browser.request(preq, opts); } , implicitGrant: function(directives, opts) { @@ -695,7 +700,8 @@ return OAUTH3.discover(providerUri, opts).then(function (directive) { var prequest = OAUTH3.urls.refreshToken(directive, opts); - return OAUTH3.request(prequest).then(function (req) { + prequest.url = OAUTH3.url.resolve(providerUri/*directives.issuer*/, prequest.url); + return OAUTH3.request(prequest/*, { directives: directive }*/).then(function (req) { var data = req.data; data.provider_uri = providerUri; if (data.error) { @@ -747,7 +753,7 @@ // Let the Code Waste begin!! // , _browser: { - window: window + window: 'undefined' !== typeof window ? window : null // TODO we don't need to include this if we're using jQuery or angular , discover: function(providerUri, opts) { opts = opts || {}; @@ -841,7 +847,8 @@ } resolve({ - request: xhr + _request: xhr + , headers: null // TODO , data: data , status: xhr.status }); @@ -1047,9 +1054,8 @@ this._clientUri = OAUTH3.clientUri(location); } if (this._providerUri) { - p = OAUTH3.discover(this._providerUri, { client_id: this._clientUri }).then(function (/*directives*/) { - $('.js-signin').removeAttr('disabled'); - }); + // returns directives + p = OAUTH3.discover(this._providerUri, { client_id: this._clientUri }); } return OAUTH3.discover(this._clientUri, { client_id: this._clientUri }).then(function (clientDirectives) { @@ -1101,6 +1107,7 @@ } // TODO maybe use a baseUrl from the directives file? preq.url = OAUTH3.url.resolve(this._providerUri, preq.url); + return OAUTH3.request(preq, opts); } , logout: function (opts) { diff --git a/oauth3.dns.js b/oauth3.dns.js new file mode 100644 index 0000000..abc139e --- /dev/null +++ b/oauth3.dns.js @@ -0,0 +1,79 @@ +;(function (exports) { +'use strict'; + +var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; + +OAUTH3.api['dns.list'] = function (providerUri, opts) { + var session = opts.session; + + return OAUTH3.request({ + method: 'GET' + , url: OAUTH3.url.normalize(providerUri) + + '/api/com.daplie.domains/accounts/' + session.token.sub + '/dns' + , session: session + }).then(function (res) { + return res.data.records || res.data; + }); +}; + +OAUTH3.api['devices.list'] = function (providerUri, opts) { + var session = opts.session; + + return OAUTH3.request({ + url: OAUTH3.url.normalize(providerUri) + + '/api/com.daplie.domains/accounts/' + session.token.sub + '/devices' + , method: 'GET' + , session: session + }, {}).then(function (res) { + return res.data.devices || res.data; + }); +}; + +OAUTH3.api['devices.attach'] = function (providerUri, opts) { + var session = opts.session; + + return OAUTH3.request({ + url: OAUTH3.url.normalize(providerUri) + + '/api/com.daplie.domains/accounts/' + session.token.sub + //+ '/devices/' + device + '/' + + '/devices/' + (opts.data.uid || '_') + '/' + opts.data.device + + '/' + opts.data.tld + '/' + opts.data.sld + '/' + (opts.data.sub || '') + , method: 'POST' + , session: session + }, {}).then(function (res) { + return res.data.devices || res.data; + }); +}; + +OAUTH3.api['devices.detach'] = function (providerUri, opts) { + var session = opts.session; + + return OAUTH3.request({ + url: OAUTH3.url.normalize(providerUri) + + '/api/com.daplie.domains/accounts/' + session.token.sub + //+ '/devices/' + device + '/' + + '/devices/' + (opts.data.uid || '_') + '/' + opts.data.device + + '/' + opts.data.tld + '/' + opts.data.sld + '/' + (opts.data.sub || '') + , method: 'DELETE' + , session: session + }, {}).then(function (res) { + return res.data.devices || res.data; + }); +}; + +OAUTH3.api['devices.detach'] = function (providerUri, opts) { + var session = opts.session; + + return OAUTH3.request({ + url: OAUTH3.url.normalize(providerUri) + + '/api/com.daplie.domains/accounts/' + session.token.sub + + '/devices/' + opts.data.device + + '/' + opts.data.tld + '/' + opts.data.sld + '/' + (opts.data.sub || '') + , method: 'DELETE' + , session: session + }, {}).then(function (res) { + return res.data.device || res.data; + }); +}; + +}('undefined' !== typeof exports ? exports : window)); diff --git a/oauth3.issuer.js b/oauth3.issuer.js index dfacf07..c783992 100644 --- a/oauth3.issuer.js +++ b/oauth3.issuer.js @@ -130,6 +130,7 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) { var clientUri = opts.client_uri; var args = directive[type]; 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 @@ -239,20 +240,23 @@ OAUTH3.authn.loginMeta = function (directive, opts) { return OAUTH3.request({ method: directive.credential_meta.method || 'GET' // TODO lint urls + // TODO client_uri , url: OAUTH3.url.resolve(directive.issuer, directive.credential_meta.url) .replace(':type', 'email') .replace(':id', opts.email) }); }; OAUTH3.authn.otp = function (directive, opts) { + // TODO client_uri var preq = { method: directive.credential_otp.method || 'POST' , url: OAUTH3.url.resolve(directive.issuer, directive.credential_otp.url) , data: { // TODO replace with signed hosted file client_agree_tos: 'oauth3.org/tos/draft' - , client_id: directive.issuer // In this case, the issuer is its own client - , client_uri: directive.issuer + // TODO unbreak the client_uri option (if broken) + , client_id: /*opts.client_id ||*/ OAUTH3.uri.normalize(directive.issuer) // In this case, the issuer is its own client + , client_uri: /*opts.client_uri ||*/ OAUTH3.uri.normalize(directive.issuer) , request_otp: true , username: opts.email } @@ -268,6 +272,7 @@ OAUTH3.authn.resourceOwnerPassword = function (directive, opts) { return OAUTH3.discover(providerUri, opts).then(function (directive) { var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts); + // TODO return not the raw request? return OAUTH3.request(prequest).then(function (req) { var data = req.data; data.provider_uri = providerUri; diff --git a/oauth3.ng.js b/oauth3.ng.js index 97f0b96..c906151 100644 --- a/oauth3.ng.js +++ b/oauth3.ng.js @@ -15,9 +15,9 @@ angular function PromiseAngularQ(fn) { var d = $q.defer(); - $timeout(function () { + //$timeout(function () { fn(d.resolve, d.reject); - }, 0); + //}, 0); //this.then = d.promise.then; //this.catch = d.promise.catch; diff --git a/oauth3.node.js b/oauth3.node.js new file mode 100644 index 0000000..95b0bd3 --- /dev/null +++ b/oauth3.node.js @@ -0,0 +1,101 @@ +'use strict'; + +//var OAUTH3 = require('./oauth3.core.js').OAUTH3; +var OAUTH3 = require('./oauth3.issuer.js').OAUTH3; +// used for OAUTH3.urls.resourcePasswordOwner +// used for OAUTH3.authn.loginMeta +// used for OAUTH3.authn.otp +// used for OAUTH3.authn.resourcePasswordOwner +var PromiseA = require('bluebird'); +var requestAsync = PromiseA.promisify(require('request')); +var crypto = require('crypto'); + +OAUTH3.PromiseA = PromiseA; +OAUTH3._discoverHelper = function(providerUri, opts) { + return OAUTH3._node.discover(providerUri, opts); +}; +OAUTH3._requestHelper = function (preq, opts) { + /* + if (opts && opts.directives) { + preq.url = OAUTH3.url.resolve(opts.directives.issuer, preq.url); + } + */ + return OAUTH3._node.request(preq, opts); +}; +OAUTH3._base64.atob = function (base64) { + return new Buffer(base64, 'base64').toString('utf8'); +}; +OAUTH3._base64.btoa = function (text) { + return new Buffer(text, 'utf8').toString('base64'); +}; + +OAUTH3._node = {}; +OAUTH3._node.discover = function(providerUri/*, opts*/) { + return OAUTH3.request({ + method: 'GET' + , url: OAUTH3.url.normalize(providerUri) + '/.well-known/oauth3/directives.json' + }).then(function (resp) { + return resp.data; + }); +}; +OAUTH3._node.request = function(preq/*, _sys*/) { + var data = { + method: preq.method + , url: preq.url || preq.uri + , headers: preq.headers + , json: preq.data || preq.body || preq.json || undefined // TODO which to use? + , formData: preq.formData || undefined + }; + + //console.log('DEBUG request'); + //console.log(preq.url || preq.uri); + //console.log(data.json); + + return requestAsync(data).then(OAUTH3._node._parseJson); +}; +OAUTH3._node._parseJson = function (resp) { + var err; + var json = resp.body; + + // TODO toCamelCase + if (!(resp.statusCode >= 200 && resp.statusCode < 400)) { + // console.log('[A3] DEBUG', resp.body); + err = new Error("bad response code: " + resp.statusCode); + err.result = resp.body; + return PromiseA.reject(err); + } + + //console.log('resp.body', typeof resp.body); + if ('string' === typeof json) { + try { + json = JSON.parse(json); + } catch(e) { + err = new Error('response not parsable:' + resp.body); + err.result = resp.body; + return PromiseA.reject(err); + } + } + + // handle both Oauth2- and node-style errors + if (json.error) { + err = new Error(json.error && json.error.message || json.error_description || json.error); + err.result = json; + return PromiseA.reject(err); + } + + return { + headers: resp.headers + , status: resp.statusCode + , data: json + }; +}; +OAUTH3._logoutHelper = function(/*directives, opts*/) { + // TODO allow prompting of which account + return OAUTH3.PromiseA.reject(new Error("logout not yet implemented for node.js")); +}; +OAUTH3._node.randomState = function () { + return crypto.randomBytes(16).toString('hex'); +}; +OAUTH3.randomState = OAUTH3._node.randomState; + +module.exports = OAUTH3; diff --git a/oauth3.node.storage.js b/oauth3.node.storage.js new file mode 100644 index 0000000..0285a30 --- /dev/null +++ b/oauth3.node.storage.js @@ -0,0 +1,36 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +module.exports = { + directives: { + _get: function (providerUri) { + // TODO make safe + try { + return require(path.join(process.cwd(), providerUri + '.directives.json')); + } catch(e) { + return null; + } + } + , _set: function (providerUri, directives) { + fs.writeFileSync(path.join(process.cwd(), providerUri + '.directives.json'), JSON.stringify(directives, null, 2)); + return directives; + } + } + +, session: { + _get: function (providerUri) { + // TODO make safe + try { + return require(path.join(process.cwd(), providerUri + '.session.json')); + } catch(e) { + return null; + } + } + , _set: function (providerUri, session) { + fs.writeFileSync(path.join(process.cwd(), providerUri + '.session.json'), JSON.stringify(session, null, 2)); + return session; + } + } +}; diff --git a/oauth3.tunnel.js b/oauth3.tunnel.js new file mode 100644 index 0000000..3ef5cbc --- /dev/null +++ b/oauth3.tunnel.js @@ -0,0 +1,23 @@ +;(function (exports) { +'use strict'; + +var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; + +OAUTH3.api['tunnel.token'] = function (providerUri, opts) { + var session = opts.session; + + return OAUTH3.request({ + method: 'POST' + , url: OAUTH3.url.normalize(providerUri) + + '/api/org.oauth3.tunnel/accounts/' + session.token.sub + '/token' + , session: session + , data: { + domains: opts.data.domains + , device: opts.data.device + } + }).then(function (res) { + return res.data.records || res.data; + }); +}; + +}('undefined' !== typeof exports ? exports : window)); diff --git a/package.json b/package.json index fa01b59..febe179 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,36 @@ { - "name": "oauth3", - "respository": { - "type": "git", - "url": "git+ssh://git@git.daplie.com:OAuth3/oauth3.js.git" - }, + "name": "oauth3.js", + "version": "1.0.0", + "description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.", + "main": "oauth3.node.js", "scripts": { - "install": "./node_modules/.bin/gulp" + "install": "./node_modules/.bin/gulp", + "test": "echo \"Error: no test specified\" && exit 1" }, + "repository": { + "type": "git", + "url": "git@git.daplie.com:OAuth3/oauth3.js.git" + }, + "keywords": [ + "oauth", + "oauth2", + "oauth3", + "oidc", + "openid", + "connect", + "openidconnect", + "authn", + "authz", + "authentication", + "authorization", + "user", + "password", + "passphrase", + "login", + "signin", + "log", + "sign" + ], "devDependencies": { "browserify-aes": "^1.0.6", "create-hash": "^1.1.2", @@ -20,5 +44,7 @@ "gulp-streamify": "^1.0.2", "gulp-uglify": "^2.1.0", "vinyl-source-stream": "^1.1.0" - } + }, + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "(MIT OR Apache-2.0)" }