11const Solid = require ( '../../index' )
22const path = require ( 'path' )
3- const supertest = require ( 'supertest' )
4- const expect = require ( 'chai' ) . expect
5- const nock = require ( 'nock' )
63const fs = require ( 'fs-extra' )
74const { UserStore } = require ( 'oidc-auth-manager' )
85const 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 } )
0 commit comments