Skip to content

Commit

Permalink
Accounts API v1 (nodeSolidServer#339)
Browse files Browse the repository at this point in the history
* implemented signout

* designing the scenario for accounts with OIDC

* adding waterfall

* adding scenario workflow

* put all the APIs under /api

* Update scenarios.md

* adding scenario folders

* enabling webid

* starting writing first test

* rename to test/api-accounts

* skipping tests for now

* first implementation of signin endpoint

* implementing signin

* adding lib/api

* adding APIs and renaming folders in gitignore

* 406 to 400

* turn all errors into 400

* update tests to 400

* missing a 400

* from oidc to oidc issuer

* missed oidc.issuer

* implementing OIDC for new account creation (nodeSolidServer#349)

* implementing OIDC for new account creation

* adding create user call in new

* oidc in new

* client is under .client

* adding password

* Remove unused requires

* skipping oidc if no provider is found
  • Loading branch information
nicola authored and Andrei committed May 12, 2016
1 parent c319b07 commit fc424c6
Show file tree
Hide file tree
Showing 18 changed files with 287 additions and 36 deletions.
12 changes: 6 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 5 additions & 5 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
config.json.bck
config.json
test
accounts
settings
profile
.acl
inbox
./accounts
./settings
./profile
./.acl
./inbox
8 changes: 4 additions & 4 deletions lib/account-recovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/api/accounts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
signin: require('./signin'),
signout: require('./signout')
}
33 changes: 33 additions & 0 deletions lib/api/accounts/signin.js
Original file line number Diff line number Diff line change
@@ -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'])
})
}
}
9 changes: 9 additions & 0 deletions lib/api/accounts/signout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = signout

function signout () {
return (req, res, next) => {
req.session.userId = ''
req.session.identified = false
res.status(200).send()
}
}
3 changes: 3 additions & 0 deletions lib/api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
accounts: require('./accounts')
}
15 changes: 13 additions & 2 deletions lib/create-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion lib/handlers/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
}
Expand Down
40 changes: 31 additions & 9 deletions lib/identity-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
128 changes: 128 additions & 0 deletions test/api-accounts.js
Original file line number Diff line number Diff line change
@@ -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 '<https://oidc.amazingwebsite.tld>; 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)
})
})
})
Loading

0 comments on commit fc424c6

Please sign in to comment.