diff --git a/.gitignore b/.gitignore index dd275c6a7..dacde2b1a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ node_modules/ *.swp .tern-port npm-debug.log -accounts -profile -inbox -.acl -config.json -settings +./accounts +./profile +./inbox +./.acl +./config.json +./settings diff --git a/.npmignore b/.npmignore index 6d88a7953..a348f69e9 100644 --- a/.npmignore +++ b/.npmignore @@ -1,8 +1,8 @@ config.json.bck config.json test -accounts -settings -profile -.acl -inbox +./accounts +./settings +./profile +./.acl +./inbox diff --git a/lib/account-recovery.js b/lib/account-recovery.js index 6e4c307d5..c5b00b665 100644 --- a/lib/account-recovery.js +++ b/lib/account-recovery.js @@ -20,7 +20,7 @@ function AccountRecovery (corsSettings, options = {}) { text: 'Hello,\n' + 'You asked to retrieve your account: ' + account + '\n' + 'Copy this address in your browser addressbar:\n\n' + - 'https://' + path.join(host, '/recovery/confirm?token=' + token) // TODO find a way to get the full url + 'https://' + path.join(host, '/api/accounts/validateToken?token=' + token) // TODO find a way to get the full url // html: '' } } @@ -29,12 +29,12 @@ function AccountRecovery (corsSettings, options = {}) { router.use(corsSettings) } - router.get('/request', function (req, res, next) { + router.get('/recover', function (req, res, next) { res.set('Content-Type', 'text/html') res.sendFile(path.join(__dirname, '../static/account-recovery.html')) }) - router.post('/request', bodyParser.urlencoded({ extended: false }), function (req, res, next) { + router.post('/recover', bodyParser.urlencoded({ extended: false }), function (req, res, next) { debug('getting request for account recovery', req.body.webid) const ldp = req.app.locals.ldp const emailService = req.app.locals.email @@ -85,7 +85,7 @@ function AccountRecovery (corsSettings, options = {}) { }) }) - router.get('/confirm', function (req, res, next) { + router.get('/validateToken', function (req, res, next) { if (!req.query.token) { res.status(406).send('Token is required') return diff --git a/lib/api/accounts/index.js b/lib/api/accounts/index.js new file mode 100644 index 000000000..c100c78d6 --- /dev/null +++ b/lib/api/accounts/index.js @@ -0,0 +1,4 @@ +module.exports = { + signin: require('./signin'), + signout: require('./signout') +} diff --git a/lib/api/accounts/signin.js b/lib/api/accounts/signin.js new file mode 100644 index 000000000..01e88709f --- /dev/null +++ b/lib/api/accounts/signin.js @@ -0,0 +1,33 @@ +module.exports = signin + +const validUrl = require('valid-url') +const request = require('request') +const li = require('li') + +function signin () { + return (req, res, next) => { + if (!validUrl.isUri(req.body.webid)) { + return res.status(400).send('This is not a valid URI') + } + + request({ method: 'OPTIONS', uri: req.body.webid }, function (err, req) { + if (err) { + res.status(400).send('Did not find a valid endpoint') + return + } + if (!req.headers.link) { + res.status(400).send('The URI requested is not a valid endpoint') + return + } + + const linkHeaders = li.parse(req.headers.link) + console.log(linkHeaders) + if (!linkHeaders['oidc.issuer']) { + res.status(400).send('The URI requested is not a valid endpoint') + return + } + + res.redirect(linkHeaders['oidc.issuer']) + }) + } +} diff --git a/lib/api/accounts/signout.js b/lib/api/accounts/signout.js new file mode 100644 index 000000000..16ab7372c --- /dev/null +++ b/lib/api/accounts/signout.js @@ -0,0 +1,9 @@ +module.exports = signout + +function signout () { + return (req, res, next) => { + req.session.userId = '' + req.session.identified = false + res.status(200).send() + } +} diff --git a/lib/api/index.js b/lib/api/index.js new file mode 100644 index 000000000..1d7959e1e --- /dev/null +++ b/lib/api/index.js @@ -0,0 +1,3 @@ +module.exports = { + accounts: require('./accounts') +} diff --git a/lib/create-app.js b/lib/create-app.js index a0e3f8558..6b739d5df 100644 --- a/lib/create-app.js +++ b/lib/create-app.js @@ -13,6 +13,8 @@ var path = require('path') var EmailService = require('./email-service') const AccountRecovery = require('./account-recovery') const capabilityDiscovery = require('./capability-discovery') +const bodyParser = require('body-parser') +const API = require('./api') var corsSettings = cors({ methods: [ @@ -89,7 +91,10 @@ function createApp (argv = {}) { if (ldp.webid) { var accountRecovery = AccountRecovery(corsSettings, { redirect: '/' }) - app.use('/recovery', accountRecovery) + // adds GET /api/accounts/recover + // adds POST /api/accounts/recover + // adds GET /api/accounts/validateToken + app.use('/api/accounts/', accountRecovery) } // Adding Multi-user support @@ -113,8 +118,14 @@ function createApp (argv = {}) { } }) } - app.use('/accounts', needsOverwrite) + + // adds POST /api/accounts/new + // adds POST /api/accounts/newCert + app.use('/api/accounts', needsOverwrite) app.use('/', corsSettings, idp.get.bind(idp)) + + app.post('/api/accounts/signin', corsSettings, bodyParser.urlencoded({ extended: false }), API.accounts.signin()) + app.post('/api/accounts/signout', corsSettings, API.accounts.signout()) } if (ldp.idp) { diff --git a/lib/handlers/authentication.js b/lib/handlers/authentication.js index df9f003c6..fef1df026 100644 --- a/lib/handlers/authentication.js +++ b/lib/handlers/authentication.js @@ -51,7 +51,8 @@ function handler (req, res, next) { return next() }) } else if (ldp.auth === 'oidc') { - return next(error(500, 'OIDC not implemented yet')) + setEmptySession(req) + return next() } else { return next(error(500, 'Authentication method not supported')) } diff --git a/lib/identity-provider.js b/lib/identity-provider.js index 66bca2b00..517bc41cc 100644 --- a/lib/identity-provider.js +++ b/lib/identity-provider.js @@ -515,16 +515,38 @@ IdentityProvider.prototype.post = function (req, res, next) { debug('Create account with settings ', options) waterfall([ - function (callback) { - if (options.spkac && options.spkac.length > 0) { - spkac = new Buffer(stripLineEndings(options.spkac), 'utf-8') - webid('tls').generate({ - spkac: spkac, - agent: agent // TODO generate agent - }, callback) - } else { + (callback) => { + if (this.auth !== 'oidc') { + return callback() + } + + const oidc = req.app.locals.oidc + + if (!oidc) { + debug('there is no OidcService') + return callback() + } + + return oidc.client.users + .create({ + email: options.email, + profile: agent, + name: options.name, + password: options.password + }) + .then(() => callback()) + .catch(callback) + }, + (callback) => { + if (!(this.auth === 'tls' && options.spkac && options.spkac.length > 0)) { return callback(null, false) } + + spkac = new Buffer(stripLineEndings(options.spkac), 'utf-8') + webid('tls').generate({ + spkac: spkac, + agent: agent // TODO generate agent + }, callback) }, function (newCert, callback) { cert = newCert @@ -587,7 +609,7 @@ IdentityProvider.prototype.middleware = function (corsSettings, firstUser) { router.all('/*', function (req, res) { var host = uriAbs(req) // TODO replace the hardcoded link with an arg - res.redirect('https://solid.github.io/solid-signup/?acc=accounts/new&crt=accounts/cert&domain=' + host) + res.redirect('https://solid.github.io/solid-signup/?acc=api/accounts/new&crt=api/accounts/cert&domain=' + host) }) router.use(errorHandler) diff --git a/package.json b/package.json index 61f815ba2..1ed997e49 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "mocha": "^2.2.5", "nock": "^7.0.2", "rsvp": "^3.1.0", + "run-waterfall": "^1.1.3", "sinon": "^1.17.4", "standard": "^7.0.1", "supertest": "^1.0.1" diff --git a/test/api-accounts.js b/test/api-accounts.js new file mode 100644 index 000000000..79ee200fd --- /dev/null +++ b/test/api-accounts.js @@ -0,0 +1,128 @@ +const Solid = require('../') +const parallel = require('run-parallel') +const waterfall = require('run-waterfall') +const path = require('path') +const supertest = require('supertest') +const expect = require('chai').expect +const nock = require('nock') +// In this test we always assume that we are Alice + +describe('API', () => { + let aliceServer + let bobServer + let alice + let bob + + const alicePod = Solid.createServer({ + root: path.join(__dirname, '/resources/accounts-scenario/alice'), + sslKey: path.join(__dirname, '/keys/key.pem'), + sslCert: path.join(__dirname, '/keys/cert.pem'), + auth: 'oidc', + dataBrowser: false, + fileBrowser: false, + webid: true + }) + const bobPod = Solid.createServer({ + root: path.join(__dirname, '/resources/accounts-scenario/bob'), + sslKey: path.join(__dirname, '/keys/key.pem'), + sslCert: path.join(__dirname, '/keys/cert.pem'), + auth: 'oidc', + dataBrowser: false, + fileBrowser: false, + webid: true + }) + + function getBobFoo (alice, bob, done) { + bob.get('/foo') + .expect(401) + .end((err, res) => { + if (err) return done(err) + expect(res).to.match(/META http-equiv="refresh"/) + done() + }) + } + + function postBobDiscoverSignIn (alice, bob, done) { + done() + } + + function entersPasswordAndConsent (alice, bob, done) { + done() + } + + before(function (done) { + parallel([ + (cb) => { + aliceServer = alicePod.listen(5000, cb) + alice = supertest('https://localhost:5000') + }, + (cb) => { + bobServer = bobPod.listen(5001, cb) + bob = supertest('https://localhost:5001') + } + ], done) + }) + + after(function () { + if (aliceServer) aliceServer.close() + if (bobServer) bobServer.close() + }) + + describe('APIs', () => { + describe('/api/accounts/signin', () => { + it('should complain if a URL is missing', (done) => { + alice.post('/api/accounts/signin') + .expect(400) + .end(done) + }) + it('should complain if a URL is invalid', (done) => { + alice.post('/api/accounts/signin') + .send('webid=HELLO') + .expect(400) + .end(done) + }) + it('should return a 400 if endpoint doesn\'t have Link Headers', (done) => { + nock('https://amazingwebsite.tld').intercept('/', 'OPTIONS').reply(200) + alice.post('/api/accounts/signin') + .send('webid=https://amazingwebsite.tld/') + .expect(400) + .end(done) + }) + it('should return a 400 if endpoint doesn\'t have oidc in the headers', (done) => { + nock('https://amazingwebsite.tld').intercept('/', 'OPTIONS').reply(200, '', { + 'Link': function (req, res, body) { + return '; rel="oidc.issuer"' + }}) + alice.post('/api/accounts/signin') + .send('webid=https://amazingwebsite.tld/') + .expect(302) + .end((err, res) => { + expect(res.header.location).to.eql('https://oidc.amazingwebsite.tld') + done(err) + }) + }) + }) + }) + + describe('Auth workflow', () => { + it.skip('step1: User tries to get /foo and gets 401 and meta redirect', (done) => { + getBobFoo(alice, bob, done) + }) + + it.skip('step2: User enters webId to signin', (done) => { + postBobDiscoverSignIn(alice, bob, done) + }) + + it.skip('step3: User enters password', (done) => { + entersPasswordAndConsent(alice, bob, done) + }) + + it.skip('entire flow', (done) => { + waterfall([ + (cb) => getBobFoo(alice, bob, cb), + (cb) => postBobDiscoverSignIn(alice, bob, cb), + (cb) => entersPasswordAndConsent(alice, bob, cb) + ], done) + }) + }) +}) diff --git a/test/identity-provider.js b/test/identity-provider.js index e7b674658..d3141f347 100644 --- a/test/identity-provider.js +++ b/test/identity-provider.js @@ -33,7 +33,7 @@ describe('Identity Provider', function () { var server = supertest(address) it('should redirect to signup on GET /accounts', function (done) { - server.get('/accounts') + server.get('/api/accounts') .expect(302, done) }) @@ -56,13 +56,13 @@ describe('Identity Provider', function () { it('should generate a certificate if spkac is valid', function (done) { var spkac = read('example_spkac.cnf') var subdomain = supertest.agent('https://nicola.' + host) - subdomain.post('/accounts/new') + subdomain.post('/api/accounts/new') .send('username=nicola') .expect(200) .end(function (err, req) { if (err) return done(err) - subdomain.post('/accounts/cert') + subdomain.post('/api/accounts/cert') .send('spkac=' + spkac + '&webid=https%3A%2F%2Fnicola.localhost%3A3457%2Fprofile%2Fcard%23me') .expect('Content-Type', /application\/x-x509-user-cert/) .expect(200) @@ -72,14 +72,14 @@ describe('Identity Provider', function () { it('should not generate a certificate if spkac is not valid', function (done) { var subdomain = supertest('https://nicola.' + host) - subdomain.post('/accounts/new') + subdomain.post('/api/accounts/new') .send('username=nicola') .expect(200) .end(function (err) { if (err) return done(err) var spkac = '' - subdomain.post('/accounts/cert') + subdomain.post('/api/accounts/cert') .send('webid=https://nicola.' + host + '/profile/card#me&spkac=' + spkac) .expect(500, done) }) @@ -97,7 +97,7 @@ describe('Identity Provider', function () { it('should return create WebID if only username is given', function (done) { var subdomain = supertest('https://nicola.' + host) - subdomain.post('/accounts/new') + subdomain.post('/api/accounts/new') .send('username=nicola') .expect(200) .end(function (err) { @@ -107,14 +107,14 @@ describe('Identity Provider', function () { it('should not create a WebID if it already exists', function (done) { var subdomain = supertest('https://nicola.' + host) - subdomain.post('/accounts/new') + subdomain.post('/api/accounts/new') .send('username=nicola') .expect(200) .end(function (err) { if (err) { return done(err) } - subdomain.post('/accounts/new') + subdomain.post('/api/accounts/new') .send('username=nicola') .expect(406) .end(function (err) { @@ -125,7 +125,7 @@ describe('Identity Provider', function () { it('should create the default folders', function (done) { var subdomain = supertest('https://nicola.' + host) - subdomain.post('/accounts/new') + subdomain.post('/api/accounts/new') .send('username=nicola') .expect(200) .end(function (err) { diff --git a/test/resources/accounts-scenario/alice/.acl b/test/resources/accounts-scenario/alice/.acl new file mode 100644 index 000000000..9362b71cf --- /dev/null +++ b/test/resources/accounts-scenario/alice/.acl @@ -0,0 +1,5 @@ +<#Owner> + a ; + <./>; + ; + , , . \ No newline at end of file diff --git a/test/resources/accounts-scenario/bob/.acl b/test/resources/accounts-scenario/bob/.acl new file mode 100644 index 000000000..49a249208 --- /dev/null +++ b/test/resources/accounts-scenario/bob/.acl @@ -0,0 +1,5 @@ +<#Owner> + a ; + <./>; + ; + , , . \ No newline at end of file diff --git a/test/resources/accounts-scenario/bob/foo b/test/resources/accounts-scenario/bob/foo new file mode 100644 index 000000000..191028156 --- /dev/null +++ b/test/resources/accounts-scenario/bob/foo @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/test/resources/accounts-scenario/bob/foo.acl b/test/resources/accounts-scenario/bob/foo.acl new file mode 100644 index 000000000..4cf18c1c8 --- /dev/null +++ b/test/resources/accounts-scenario/bob/foo.acl @@ -0,0 +1,5 @@ +<#Alice> + a ; + <./foo>; + ; + , , . \ No newline at end of file diff --git a/test/scenarios.md b/test/scenarios.md new file mode 100644 index 000000000..5436c0861 --- /dev/null +++ b/test/scenarios.md @@ -0,0 +1,23 @@ +- Full tests (Solid) + - with registered user, user is logged out + - (1) User tries to get a resource + - GET BOB/foo + - sends 401 with redirect in HTML header + - redirect GET BOB/api/accounts/signin + - (2) User enters the webId so that the authorization endpoint is discovered + - POST BOB/signin with WebID + - response is a 302 to oidc.ALICE/authorize?callback=BOB/api/oidc/rp + - (3) User is prompted password? and consent + - (user enters password)? + - user presses conset + - form submit to oidc.ALICE/authorize?callback=BOB/api/oidc/rp + - response is a 302 to BOB/api/oidc/rp + - BOB/api/oidc/rp redirects to BOB/foo + + + - needing registration + - (0) User registers an account + - POST ALICE/api/accounts/new + - gives User + - set the cookie + - send an email (for verfication)