diff --git a/bin/oauth3.js b/bin/oauth3.js index e4c4eb9..b821ee2 100644 --- a/bin/oauth3.js +++ b/bin/oauth3.js @@ -15,138 +15,195 @@ OAUTH3._hooks.session.get = require('../oauth3.node.storage.js').sessions.get; OAUTH3._hooks.session.set = require('../oauth3.node.storage.js').sessions.set; */ -var url = require('url'); -//console.log('stdin tty', process.stdin.isTTY); -//console.log('stdout tty', process.stdout.isTTY); -var oauth3; -var opts = { - providerUri: undefined -}; +// opts = { email, providerUri } +module.exports.login = function (options) { + options = options || {}; + var url = require('url'); + //console.log('stdin tty', process.stdin.isTTY); + //console.log('stdout tty', process.stdout.isTTY); + var oauth3; + var opts = { + email: options.email + , providerUri: options.providerUri + }; + if (opts.form) { + form = opts.form; + } + var email; + var providerUrl; + var providerUri; + var sameProvider; + var username; -function getCurrentUserEmail() { - return form.ask({ label: "What's your email (or cloud mail) address? ", type: 'email' }).then(function (emailResult) { - var emailParts = (emailResult.result || emailResult.input).split('@'); - var domain = emailParts[1]; + function getSession() { var username; - var sameProvider; - var urlObj = url.parse(opts.providerUri || domain); - // TODO get unique client id for bootstrapping app - oauth3 = OAUTH3.create(urlObj); - form.println("got to setProvider"); - return oauth3.setProvider(domain).then(function () { - form.println("got to setProvider SUCCESS"); - sameProvider = true; - // ignore - }, function () { - form.println("got to setProvider ERROR"); - function askOauth3Url() { - return 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 - oauth3 = OAUTH3.create(urlObj); - return oauth3.setProvider(urlResult.result || urlResult.input).then(function () { - // ignore - }, function (err) { - form.println(err.stack || err.message || err.toString()); - return askOauth3Url(); + // TODO lookup uuid locally before performing loginMeta + // TODO lookup token locally before performing loginMeta / otp + return OAUTH3.authn.loginMeta(oauth3._providerDirectives, { email: email }).then(function (/*result*/) { + return { node: email, type: 'email' }; + }, 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'; + } + + // TODO needs backup address if email is on same domain as login + result.input = result.input.toLowerCase(); + + if ('y' !== result.input) { + return getCurrentUserEmail(); + } + + if (!sameProvider) { + return { node: email, type: 'email' }; + } + + return form.ask({ + label: "What's your recovery email (or cloud mail) address? ", type: 'email' + }).then(function (recoveryResult) { + return { + node: email + , type: 'name' + , recovery: recoveryResult.result || recoveryResult.input + }; }); }); } - return askOauth3Url(); - }).then(function () { - // TODO lookup uuid locally before performing loginMeta - // TODO lookup token locally before performing loginMeta / otp - form.println("got to loginMeta"); - return OAUTH3.authn.loginMeta(oauth3._providerDirectives, { email: emailResult.input }).then(function (/*result*/) { - return { node: emailResult.result || emailResult.input, type: 'email' }; - }, 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 confirmCreateAccount(); + }).then(function (user) { + // TODO skip if token exists locally + var email = (user.recovery || user.node); + 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 (result) { + return result; + }, function (/*err*/) { + // TODO test error + return form.PromiseA.reject(new Error("The code '" + formatted + "' is mistyped or incorrect. Double check and try again.")); + }); + } + }); + }); + }); + } + + function getCurrentUserEmail() { + return form.ask({ + label: "What's your email (or cloud mail) address? ", type: 'email', value: opts.email + }).then(function (emailResult) { + opts.email = undefined; + email = (emailResult.result || emailResult.input); + var emailParts = email.split('@'); + var domain = emailParts[1]; + username = emailParts[0]; + providerUrl = 'https://' + domain; + providerUri = domain; + + var urlObj = url.parse(providerUrl); + // TODO get unique client id for bootstrapping app + oauth3 = OAUTH3.create(urlObj); + return oauth3.setProvider(domain).then(function () { + sameProvider = true; + // ignore + }, function () { + function askOauth3Url() { 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'; - } + label: "What's your OAuth3 Provider URL? ", type: 'url', value: opts.providerUri + }).then(function (urlResult) { + opts.providerUri = undefined; + providerUrl = urlResult.result || urlResult.input; + providerUri = OAUTH3.uri.normalize(providerUrl); - // TODO needs backup address if email is on same domain as login - result.input = result.input.toLowerCase(); - - if ('y' !== result.input) { - return getCurrentUserEmail(); - } - - if (!sameProvider) { - return { node: emailResult.result || emailResult.input, type: 'email' }; - } - - return form.ask({ - label: "What's your recovery email (or cloud mail) address? ", type: 'email' - }).then(function (recoveryResult) { - username = emailParts[0]; - return { - node: emailResult.result || emailResult.input - , type: 'name' - , recovery: recoveryResult.result || recoveryResult.input - }; + var urlObj = url.parse(providerUrl); + // TODO get unique client id for bootstrapping app + oauth3 = OAUTH3.create(urlObj); + return oauth3.setProvider(providerUri).then(function () { + // ignore + }, function (err) { + form.println(err.stack || err.message || err.toString()); + return askOauth3Url(); }); }); } - return confirmCreateAccount(); + return askOauth3Url(); }); }); - }); -} + } -return getCurrentUserEmail().then(function (user) { - // TODO skip if token exists locally - var email = (user.recovery || user.node); - 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); + return getCurrentUserEmail().then(function () { + return OAUTH3._hooks.meta.get(email).then(function (id) { + if (!id) { + return null; } - }).then(function (results) { - var session = results.result; - - form.println('session:'); - form.println(session); + return OAUTH3._hooks.sessions.get(providerUri, id).then(function (session) { + if (session) { + return session; + } + return null; + }); }); + }).then(function (session) { + if (session) { + return session; + } + + return getSession().then(function (sessionResult) { + console.log('sessionResult'); + console.log(sessionResult); + var session = sessionResult.result; + var id = require('crypto').createHash('sha256').update(session.token.sub || '').digest('hex'); + + return OAUTH3._hooks.sessions.set(providerUri, session, id).then(function (session) { + return OAUTH3._hooks.meta.set(email, id).then(function () { + return session; + }); + }); + }); + }).then(function (session) { + form.println('session:'); + form.println(session); + oauth3.__session = session; + return oauth3; }); -}); +}; diff --git a/node_modules/terminal-forms.js b/node_modules/terminal-forms.js index f078d47..91efb1b 160000 --- a/node_modules/terminal-forms.js +++ b/node_modules/terminal-forms.js @@ -1 +1 @@ -Subproject commit f078d479b085e8658fb2039eb3e4de49afd9db0e +Subproject commit 91efb1bbf2ab8f4db86f3e93a5c82bf541f1cc67 diff --git a/oauth3.core.js b/oauth3.core.js index 9b327e8..bcfd474 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -1118,6 +1118,7 @@ return OAUTH3.implicitGrant(me._providerDirectives, opts).then(function (session) { me._session = true; + me.__session = session; return session; }); } @@ -1130,7 +1131,7 @@ preq.client_id = this._clientUri; preq.method = preq.method || 'GET'; if (this._session) { - preq.session = preq.session || OAUTH3.hooks.session._getCached(this._providerUri); + preq.session = preq.session || this.session(); // OAUTH3.hooks.session._getCached(this._providerUri); } // TODO maybe use a baseUrl from the directives file? preq.url = OAUTH3.url.resolve(this._providerUri, preq.url); @@ -1138,6 +1139,8 @@ return OAUTH3.request(preq, opts); } , logout: function (opts) { + this.__session = false; + this._session = false; opts = opts || {}; opts.client_uri = this._clientUri; opts.client_id = this._clientUri; @@ -1148,7 +1151,8 @@ , api: function (api, opts) { opts = opts || {}; opts.api = api; - opts.session = OAUTH3.hooks.session._getCached(this._providerUri); + opts.session = this.__session || OAUTH3.hooks.session._getCached(this._providerUri); + return OAUTH3.api(this._providerUri, opts); } }; diff --git a/oauth3.node.storage.js b/oauth3.node.storage.js index 0351508..22ee06b 100644 --- a/oauth3.node.storage.js +++ b/oauth3.node.storage.js @@ -1,46 +1,134 @@ 'use strict'; -var fs = require('fs'); +var PromiseA = require('bluebird'); +var fs = PromiseA.promisifyAll(require('fs')); var path = require('path'); +//var oauth3dir = process.cwd(); +var oauth3dir = path.join(require('os').homedir(), '.oauth3', 'v1'); +var sessionsdir = path.join(oauth3dir, 'sessions'); +var directivesdir = path.join(oauth3dir, 'directives'); +var metadir = path.join(oauth3dir, 'meta'); + +if (!fs.existsSync(oauth3dir)) { + fs.mkdirSync(oauth3dir); +} +if (!fs.existsSync(directivesdir)) { + fs.mkdirSync(directivesdir); +} +if (!fs.existsSync(sessionsdir)) { + fs.mkdirSync(sessionsdir); +} +if (!fs.existsSync(metadir)) { + fs.mkdirSync(metadir); +} module.exports = { directives: { - get: function (providerUri) { + all: function () { + return fs.readdirAsync(directivesdir).then(function (nodes) { + return nodes.map(function (node) { + try { + return require(path.join(directivesdir, node)); + } catch(e) { + return null; + } + }).filter(Boolean); + }); + } + , get: function (providerUri) { // TODO make safe try { - return require(path.join(process.cwd(), providerUri + '.directives.json')); + return require(path.join(directivesdir, providerUri + '.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; + return fs.writeFileAsync( + path.join(directivesdir, providerUri + '.json') + , JSON.stringify(directives, null, 2) + ).then(function () { + return directives; + }); + } + , clear: function () { + return fs.readdirAsync(directivesdir).then(function (nodes) { + return PromiseA.all(nodes.map(function (node) { + return fs.unlinkAsync(path.join(directivesdir, node)).then(function () { }, function () { }); + })); + }); } } , sessions: { - get: function (providerUri, id) { - // TODO make safe + all: function (providerUri) { + var dirname = path.join(oauth3dir, 'sessions'); + return fs.readdirAsync(dirname).then(function (nodes) { + return nodes.map(function (node) { + var result = require(path.join(dirname, node)); + if (result.link) { + return null; + } + }).filter(Boolean).filter(function (result) { + if (!providerUri || providerUri === result.issuer) { + return result; + } + }); + }); + } + , get: function (providerUri, id) { + var result; try { if (id) { - return require(path.join(process.cwd(), providerUri + '.' + id + '.session.json')); + return PromiseA.resolve(require(path.join(sessionsdir, providerUri + '.' + id + '.json'))); } else { - return require(path.join(process.cwd(), providerUri + '.session.json')); + 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)); + } } } catch(e) { - return null; + return PromiseA.resolve(null); } + return PromiseA.resolve(result); } , set: function (providerUri, session, id) { + var p; + if (id) { - fs.writeFileSync(path.join(process.cwd(), providerUri + '.' + id + '.session.json'), JSON.stringify(session, null, 2)); + p = fs.writeFileAsync(path.join(sessionsdir, providerUri + '.' + id + '.json'), JSON.stringify(session, null, 2)); } else { - fs.writeFileSync(path.join(process.cwd(), providerUri + '.session.json'), JSON.stringify(session, null, 2)); + p = fs.writeFileAsync(path.join(sessionsdir, providerUri + '.json'), JSON.stringify(session, null, 2)); } - return session; + return p.then(function () { + return session; + }); + } + , clear: function () { + var dirname = path.join(oauth3dir, 'sessions'); + return fs.readdirAsync(dirname).then(function (nodes) { + return PromiseA.all(nodes.map(function (node) { + return fs.unlinkAsync(path.join(dirname, node)); + })); + }); + } + } +, meta: { + get: function (key) { + // TODO make safe + try { + return PromiseA.resolve(require(path.join(metadir, key + '.json'))); + } catch(e) { + return PromiseA.resolve(null); + } + } + , set: function (key, value) { + return fs.writeFileAsync(path.join(metadir, key + '.json'), JSON.stringify(value, null, 2)).then(function () { + return value; + }); } } };