Skip to content

Commit e273032

Browse files
Add a 'two pods plus external web app' integration test
1 parent ad09398 commit e273032

File tree

11 files changed

+348
-19
lines changed

11 files changed

+348
-19
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"mime-types": "^2.1.11",
5656
"moment": "^2.18.1",
5757
"negotiator": "^0.6.0",
58-
"node-fetch": "^1.6.3",
58+
"node-fetch": "^1.7.1",
5959
"node-forge": "^0.6.38",
6060
"nodemailer": "^3.1.4",
6161
"nomnom": "^1.8.1",
@@ -84,13 +84,15 @@
8484
"chai-as-promised": "^6.0.0",
8585
"dirty-chai": "^1.2.2",
8686
"hippie": "^0.5.0",
87+
"localstorage-memory": "^1.0.2",
8788
"mocha": "^3.2.0",
8889
"nock": "^9.0.14",
8990
"node-mocks-http": "^1.5.6",
9091
"nyc": "^10.1.2",
9192
"proxyquire": "^1.7.10",
9293
"sinon": "^2.1.0",
9394
"sinon-chai": "^2.8.0",
95+
"solid-auth-oidc": "^0.1.3",
9496
"standard": "^8.6.0",
9597
"supertest": "^3.0.0"
9698
},

test/integration/acl-oidc-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const fs = require('fs-extra')
33
const request = require('request')
44
const path = require('path')
55
const rm = require('../utils').rm
6+
const nock = require('nock')
67

78
const ldnode = require('../../index')
89

test/integration/authentication-oidc-test.js

Lines changed: 185 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
const Solid = require('../../index')
22
const path = require('path')
3-
const supertest = require('supertest')
4-
const expect = require('chai').expect
5-
const nock = require('nock')
63
const fs = require('fs-extra')
74
const { UserStore } = require('oidc-auth-manager')
85
const UserAccount = require('../../lib/models/user-account')
6+
const SolidAuthOIDC = require('solid-auth-oidc')
7+
8+
const fetch = require('node-fetch')
9+
const localStorage = require('localstorage-memory')
10+
const url = require('url')
11+
const { URL } = url
12+
global.URL = URL
13+
14+
const supertest = require('supertest')
15+
const nock = require('nock')
16+
const chai = require('chai')
17+
const expect = chai.expect
18+
chai.use(require('dirty-chai'))
919

1020
// In this test we always assume that we are Alice
1121

@@ -270,11 +280,11 @@ describe('Authentication API (OIDC)', () => {
270280
})
271281
})
272282

273-
describe('Login workflow', () => {
274-
// Step 1: Alice tries to access bob.com/foo, and
283+
describe('Two Pods + Browser Login workflow', () => {
284+
// Step 1: Alice tries to access bob.com/shared-with-alice.txt, and
275285
// gets redirected to bob.com's Provider Discovery endpoint
276286
it('401 Unauthorized -> redirect to provider discovery', (done) => {
277-
bob.get('/foo')
287+
bob.get('/shared-with-alice.txt')
278288
.expect(401)
279289
.end((err, res) => {
280290
if (err) return done(err)
@@ -285,15 +295,178 @@ describe('Authentication API (OIDC)', () => {
285295
})
286296
})
287297

288-
// Step 2: Alice enters her WebID URI to the Provider Discovery endpoint
289-
it('Enter webId -> redirect to provider login', (done) => {
290-
bob.post('/api/auth/select-provider')
298+
// Step 2: Alice enters her pod's URI to Bob's Provider Discovery endpoint
299+
it('Enter webId -> redirect to provider login', () => {
300+
return bob.post('/api/auth/select-provider')
291301
.send('webid=' + aliceServerUri)
292302
.expect(302)
293-
.end((err, res) => {
303+
.then(res => {
304+
// Submitting select-provider form redirects to Alice's pod's /authorize
305+
let authorizeUri = res.header.location
306+
expect(authorizeUri.startsWith(aliceServerUri + '/authorize'))
307+
308+
// Follow the redirect to /authorize
309+
let authorizePath = url.parse(authorizeUri).path
310+
return alice.get(authorizePath)
311+
})
312+
.then(res => {
313+
// Since alice not logged in to her pod, /authorize redirects to /login
294314
let loginUri = res.header.location
295-
expect(loginUri.startsWith(aliceServerUri + '/authorize'))
296-
done(err)
315+
expect(loginUri.startsWith('/login'))
316+
})
317+
})
318+
})
319+
320+
describe('Two Pods + Web App Login Workflow', () => {
321+
let aliceAccount = UserAccount.from({ webId: aliceWebId })
322+
let alicePassword = '12345'
323+
324+
let auth
325+
let authorizationUri, loginUri, authParams, callbackUri
326+
let loginFormFields = ''
327+
328+
before(() => {
329+
auth = new SolidAuthOIDC({ store: localStorage, window: { location: {} } })
330+
let appOptions = {
331+
redirectUri: 'https://app.example.com/callback'
332+
}
333+
334+
aliceUserStore.initCollections()
335+
336+
return aliceUserStore.createUser(aliceAccount, alicePassword)
337+
.then(() => {
338+
return auth.registerClient(aliceServerUri, appOptions)
339+
})
340+
.then(registeredClient => {
341+
auth.currentClient = registeredClient
342+
})
343+
})
344+
345+
after(() => {
346+
fs.removeSync(path.join(aliceDbPath, 'users/users'))
347+
fs.removeSync(path.join(aliceDbPath, 'oidc/op/tokens'))
348+
349+
let clientId = auth.currentClient.registration['client_id']
350+
let registration = `_key_${clientId}.json`
351+
fs.removeSync(path.join(aliceDbPath, 'oidc/op/clients', registration))
352+
})
353+
354+
// Step 1: An app makes a GET request and receives a 401
355+
it('should get a 401 error on a REST request to a protected resource', () => {
356+
return fetch(bobServerUri + '/shared-with-alice.txt')
357+
.then(res => {
358+
expect(res.status).to.equal(401)
359+
360+
expect(res.headers.get('www-authenticate'))
361+
.to.equal(`Bearer realm="${bobServerUri}", scope="openid"`)
362+
})
363+
})
364+
365+
// Step 2: App presents the Select Provider UI to user, determine the
366+
// preferred provider uri (here, aliceServerUri), and constructs
367+
// an authorization uri for that provider
368+
it('should determine the authorization uri for a preferred provider', () => {
369+
return auth.currentClient.createRequest({}, auth.store)
370+
.then(authUri => {
371+
authorizationUri = authUri
372+
373+
expect(authUri.startsWith(aliceServerUri + '/authorize')).to.be.true()
374+
})
375+
})
376+
377+
// Step 3: App redirects user to the authorization uri for login
378+
it('should redirect user to /authorize and /login', () => {
379+
return fetch(authorizationUri, { redirect: 'manual' })
380+
.then(res => {
381+
// Since user is not logged in, /authorize redirects to /login
382+
expect(res.status).to.equal(302)
383+
384+
loginUri = new URL(res.headers.get('location'))
385+
expect(loginUri.toString().startsWith(aliceServerUri + '/login'))
386+
.to.be.true()
387+
388+
authParams = loginUri.searchParams
389+
})
390+
})
391+
392+
// Step 4: Pod returns a /login page with appropriate hidden form fields
393+
it('should display the /login form', () => {
394+
return fetch(loginUri.toString())
395+
.then(loginPage => {
396+
return loginPage.text()
397+
})
398+
.then(pageText => {
399+
// Login page should contain the relevant auth params as hidden fields
400+
401+
authParams.forEach((value, key) => {
402+
let hiddenField = `<input type="hidden" name="${key}" id="${key}" value="${value}" />`
403+
404+
expect(pageText).to.match(new RegExp(hiddenField))
405+
406+
loginFormFields += `${key}=` + encodeURIComponent(value) + '&'
407+
})
408+
})
409+
})
410+
411+
// Step 5: User submits their username & password via the /login form
412+
it('should login via the /login form', () => {
413+
loginFormFields += `username=${'alice'}&password=${alicePassword}`
414+
415+
return fetch(aliceServerUri + '/login/password', {
416+
method: 'POST',
417+
body: loginFormFields,
418+
redirect: 'manual',
419+
headers: {
420+
'content-type': 'application/x-www-form-urlencoded'
421+
},
422+
credentials: 'include'
423+
})
424+
.then(res => {
425+
expect(res.status).to.equal(302)
426+
let postLoginUri = res.headers.get('location')
427+
let cookie = res.headers.get('set-cookie')
428+
429+
// Successful login gets redirected back to /authorize and then
430+
// back to app
431+
expect(postLoginUri.startsWith(aliceServerUri + '/authorize'))
432+
.to.be.true()
433+
434+
return fetch(postLoginUri, { redirect: 'manual', headers: { cookie } })
435+
})
436+
.then(res => {
437+
// User gets redirected back to original app
438+
expect(res.status).to.equal(302)
439+
callbackUri = res.headers.get('location')
440+
expect(callbackUri.startsWith('https://app.example.com#'))
441+
442+
expect(res.headers.get('user')).to.equal(aliceWebId)
443+
})
444+
})
445+
446+
// Step 6: Web App extracts tokens from the uri hash fragment, uses
447+
// them to access protected resource
448+
it('should use id token from the callback uri to access shared resource', () => {
449+
auth.window.location.href = callbackUri
450+
451+
return auth.initUserFromResponse(auth.currentClient)
452+
.then(webId => {
453+
expect(webId).to.equal(aliceWebId)
454+
455+
let idToken = auth.idToken
456+
457+
return fetch(bobServerUri + '/shared-with-alice.txt', {
458+
headers: {
459+
'Authorization': 'Bearer ' + idToken
460+
}
461+
})
462+
})
463+
.then(res => {
464+
expect(res.status).to.equal(200)
465+
466+
return res.text()
467+
})
468+
.then(contents => {
469+
expect(contents).to.equal('protected contents\n')
297470
})
298471
})
299472
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
protected resource
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<#0>
2+
a <http://www.w3.org/ns/auth/acl#Authorization>;
3+
<http://www.w3.org/ns/auth/acl#accessTo> <./protected.txt> ;
4+
5+
<http://www.w3.org/ns/auth/acl#agent> <https://alice.example.com/profile/card#me> ;
6+
7+
<http://www.w3.org/ns/auth/acl#mode>
8+
<http://www.w3.org/ns/auth/acl#Write>, <http://www.w3.org/ns/auth/acl#Read>.

test/resources/accounts-scenario/bob/foo

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/resources/accounts-scenario/bob/foo.acl

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
protected contents
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<#Alice>
2+
a <http://www.w3.org/ns/auth/acl#Authorization> ;
3+
4+
<http://www.w3.org/ns/auth/acl#accessTo> <./shared-with-alice.txt>;
5+
6+
# Alice web id
7+
<http://www.w3.org/ns/auth/acl#agent> <https://localhost:7000/profile/card#me>;
8+
9+
# Bob web id
10+
<http://www.w3.org/ns/auth/acl#agent> <https://localhost:7001/profile/card#me>;
11+
12+
<http://www.w3.org/ns/auth/acl#mode>
13+
<http://www.w3.org/ns/auth/acl#Read>,
14+
<http://www.w3.org/ns/auth/acl#Write>,
15+
<http://www.w3.org/ns/auth/acl#Control> .
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"keys": [
3+
{
4+
"kid": "2koDA6QjhXU",
5+
"kty": "RSA",
6+
"alg": "RS256",
7+
"n": "wcO-8ub-aAf1LoH3TjX1HtlYhc_AHkIxgSwFJKjF8eY3sUpkzfS_lsBYoerG-1gJVP-j5vrGNfre7lFjUd-TukKMBnONZBnER8RSbbIC2MuoUpEj6cWoL5gD1WIkznFw_tO5w6bf2kqL2GR1_GbWAYmfOJFd_lJwg6eciNzYqvDwx-hZniNqTAD63y4od1mcKJBxFXY83VdFcCCWitg37Uxeyw8qTAQgOkR258a5juU9n8y3GDWYeWKkpr9dLWJaWomI6x-dL_tROwSMcuISMpGftGf7pYN83DQBDSwXPkaQnd1g7ExSb3slSdf_Z1kTH5eRoGJdXuA7lmRpUHDrUw",
8+
"e": "AQAB",
9+
"key_ops": [
10+
"verify"
11+
],
12+
"ext": true
13+
},
14+
{
15+
"kid": "9CdpWhTmRwQ",
16+
"kty": "RSA",
17+
"alg": "RS384",
18+
"n": "riNXxT2bQG17gV8Gp28f1fZvBoA-iO85lfZncMflXJNbkTR69rbqsYOPbJ02BIvdbBk9cdCSHDDO_yH2FAnY1N0WONRcQdVkyKcfCS8gLpFDRnP7sa5_WOwnrnY00VEHpPUhcUWzTK2pNSZemGY14fPgNDW7vH8dipMfVMr9bgmvPzefgEIeANOMKA6PHx_0WcT9k8NYjDdpuo6loFmVTj5ulWNO4rVTLFCMtyTB1cwNeIeN0Kwmqcuta5Y_FiEMpP_Hw8MBdoIZfH2P7qa6lkbw_jExY1suyP5BxU6cUndzjcIeiBiVbJEPCvR1zBuxNUADFqOCEF-8fIsY8AL6fw",
19+
"e": "AQAB",
20+
"key_ops": [
21+
"verify"
22+
],
23+
"ext": true
24+
},
25+
{
26+
"kid": "lVZq8jBYVa4",
27+
"kty": "RSA",
28+
"alg": "RS512",
29+
"n": "tQz5GDyEZQdJlPq4aiKHlu9_gEft8ozIbi-tbmx_0JPUHOvZwPaWkPERu27MHNp7Z4XvftkyUXH243Prtetjq6cUd4FiYyOz6MpbktxOXfl8oSfbe89Dava4PqgolJaTBfp21WsMM9OvweOgto1Rv9do7oUsoRQIuHl17T2RmMXx4AE2CDyPA7JQCDtw6zk6erLdIZtUrF0J1UrGFHRHjsFexRIPc07X9IIMqzQlESlJXCbiVEvCteMCZbYuhbvvNqiTlNLUh4_jO_7NP6zkhwnRm5bJ4pXGrEMUi9FypRiVABkRZigvK19RDrsUA3AXt7VMkVRtXSsmv_YtmzVJgw",
30+
"e": "AQAB",
31+
"key_ops": [
32+
"verify"
33+
],
34+
"ext": true
35+
},
36+
{
37+
"kid": "Zr1pAM5uXRI",
38+
"kty": "RSA",
39+
"alg": "RS256",
40+
"n": "4nLfuOakIfP4YRrVGopRNszYlcnw4GhYwedQvt-CgAc5-1fLqi7n4Dr2vyeNB79h9cIVk_i4ehB5M8EcZN0OHHpDcTrYJOS0gyOILDwQhQezc6VRcor6g0jq-VuMFWNCXcnlowAWJ09d8c_CgNFoJsIvFji4cIeBIFYh3bpJMKQpxccYh8D12jRYQckTvmhZQhifFPYSu_YFk7R2eOFu4nuf-xZqzBKp2zFE28VPgBX_i4BV0vR5Mdl1UmnZ_LZbyseH9sIzZmVHGTCQ-5DYqFlXXBNPGs4q5qku9hN6qSmgT0yypYwwZYRG-XAx79ZSZMIFUiG_hWrc3SWPlgV9SQ",
41+
"e": "AQAB",
42+
"key_ops": [
43+
"verify"
44+
],
45+
"ext": true
46+
},
47+
{
48+
"kid": "C_ZedndjY2M",
49+
"kty": "RSA",
50+
"alg": "RS384",
51+
"n": "vxCys4nxNi56-VtzYdiH7xYhT95CC2oaLDlFIY216O5VoQYrMQvwdZvRPGpKepxyY6xKILki7jB3BjbF3honf5M7MK-i6oQRXS6HCdruBGp6XZSAw7yemn1sP4f8MRrhJ2B130YIvhKLuQekdCLR--_n6-WfZZuUF7AXKcs5XdPW3vSy_XLAtnso8axmYASfhNK6DwOwXTA-uJTHW1HVfALUfgtzxAt7Z92ySnuI_CzXnr6lt-vDd52leaCS8yso8Anpa7xXJC3czkRFFrN20k5m6olhUpssnSVJDLyWup7PInfRQCNzuQgpomWFh9r3hwyrQMKSrliTIOiSRq7l6Q",
52+
"e": "AQAB",
53+
"key_ops": [
54+
"verify"
55+
],
56+
"ext": true
57+
},
58+
{
59+
"kid": "rB8CEEEM6qI",
60+
"kty": "RSA",
61+
"alg": "RS512",
62+
"n": "s760vqQPvEf82T4LHPby9hxFFIKUda0G79xuDmseWJgeMWlU3yv6uDPPJ2Nx-4prXC_fiFlNsEEx8p6GjKpAi32Mb1vehqTNJEmluxLrBeYYY5-mA5d8-2BE_5jLSkDEQ8pFwWICP-LbI95U7aytMqnuTqPr8cC4N2Sac2P4r8lGrzT6F276DB2Meshf00lJ9o_7ta77rLTVBCo9Ws5D9V7JiT86mx8hia_6JbvcmfgH_6NAitGMD6tvXNWH-i7o8ZLywBJO4U35wU_woduTFmj_ZFDrBgMRNMVBrvnwk_5XDfbrVkRLaunnQMadKgCMkryj4RXNqms08wF8N59uLQ",
63+
"e": "AQAB",
64+
"key_ops": [
65+
"verify"
66+
],
67+
"ext": true
68+
},
69+
{
70+
"kid": "Muh91OOgi5Q",
71+
"kty": "RSA",
72+
"alg": "RS256",
73+
"n": "wbiBktBKqfKFnXHuBrxeN09D006kX6C9VUykaQayPZJCmSv8X8zpCclOXCYHboGtkJ9y5E9iCCK0V7kFvSXOWl562tWHlNzZfZM6xZU1hS-1jFc3Hjk66yIkeocFpAdb3pUCzFmSNrQsDWoJSJa-ly6AkmPahPe2A7UzFeEjUiWKossssOhgvo3TFCB6D7kkU7DujShm74FqzjXEPmcgc3ZDzpALu7N_HqxIbuQv0TJ7yIY8cqzyTmDahy0cKGn1Z4ViVwCCZsgVniDLbcLcsXkhPWKAtM2FMLbSIJvEZrLlPFTWWsc82oky5u2aeO0hEodihkwVl-w_Xaiv3RZVmQ",
74+
"e": "AQAB",
75+
"key_ops": [
76+
"verify"
77+
],
78+
"ext": true
79+
}
80+
]
81+
}

0 commit comments

Comments
 (0)