/** * Authentication and Authorization tests */ import { describe, it, before, after } from 'node:test'; import assert from 'node:assert'; import { startTestServer, stopTestServer, request, createTestPod, getPodToken, getBaseUrl, assertStatus, assertHeader } from './helpers.js'; describe('Authentication', () => { before(async () => { await startTestServer(); }); after(async () => { await stopTestServer(); }); describe('Token Authentication', () => { it('should return token on pod creation', async () => { const result = await createTestPod('authtest'); assert.ok(result.token, 'Should return a token'); assert.ok(result.token.includes('.'), 'Token should have signature'); }); it('should allow authenticated access to private resources', async () => { await createTestPod('privatetest'); // Should succeed with auth const res = await request('/privatetest/private/', { auth: 'privatetest' }); assertStatus(res, 200); }); it('should deny unauthenticated access to private resources', async () => { await createTestPod('denytest'); // Should fail without auth const res = await request('/denytest/private/'); assertStatus(res, 401); }); it('should return 403 for wrong user accessing private resources', async () => { await createTestPod('user1'); await createTestPod('user2'); // User2 trying to access User1's private folder const res = await request('/user1/private/', { auth: 'user2' }); assertStatus(res, 403); }); it('should accept Bearer token format', async () => { await createTestPod('bearertest'); const token = getPodToken('bearertest'); const res = await fetch(`${getBaseUrl()}/bearertest/private/`, { headers: { 'Authorization': `Bearer ${token}` } }); assertStatus(res, 200); }); it('should reject invalid tokens', async () => { await createTestPod('invalidtest'); const res = await fetch(`${getBaseUrl()}/invalidtest/private/`, { headers: { 'Authorization': 'Bearer invalid.token' } }); assertStatus(res, 401); }); }); describe('WAC Enforcement', () => { it('should allow public read on pod root', async () => { await createTestPod('publicread'); // Public folder should be readable without auth const res = await request('/publicread/public/'); assertStatus(res, 200); }); it('should allow public read on explicit public folders', async () => { await createTestPod('explicitpublic'); // Root ACL has public read default const res = await request('/explicitpublic/'); assertStatus(res, 200); }); it('should allow authenticated write to owned resources', async () => { await createTestPod('writetest'); const res = await request('/writetest/public/test.txt', { method: 'PUT', body: 'test content', auth: 'writetest' }); assertStatus(res, 201); }); it('should deny unauthenticated write', async () => { await createTestPod('nowrite'); const res = await request('/nowrite/public/test.txt', { method: 'PUT', body: 'test content' }); assertStatus(res, 401); }); it('should deny other user write to owned resources', async () => { await createTestPod('owner1'); await createTestPod('attacker'); const res = await request('/owner1/public/test.txt', { method: 'PUT', body: 'malicious content', auth: 'attacker' }); assertStatus(res, 403); }); it('should allow public append to inbox', async () => { await createTestPod('inboxtest'); // POST to inbox should work for anyone (public append) const res = await request('/inboxtest/inbox/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Slug': 'notification' }, body: JSON.stringify({ type: 'notification' }) }); assertStatus(res, 201); }); it('should allow append-only PATCH for insert-only patch', async () => { await createTestPod('appendpatch1'); await createTestPod('appendwriter1'); const baseUrl = getBaseUrl(); // Create target resource and container first. await request('/appendpatch1/public/item.json', { method: 'PUT', headers: { 'Content-Type': 'application/ld+json' }, body: JSON.stringify({ '@context': { ex: 'http://example.org/' }, '@id': '#it', 'ex:name': 'initial' }), auth: 'appendpatch1' }); // Set container ACL: owner full control + authenticated append only. const acl = { '@context': { acl: 'http://www.w3.org/ns/auth/acl#' }, '@graph': [ { '@id': '#owner', '@type': 'acl:Authorization', 'acl:agent': { '@id': `${baseUrl}/appendpatch1/profile/card.jsonld#me` }, 'acl:accessTo': { '@id': `${baseUrl}/appendpatch1/public/` }, 'acl:default': { '@id': `${baseUrl}/appendpatch1/public/` }, 'acl:mode': [ { '@id': 'acl:Read' }, { '@id': 'acl:Write' }, { '@id': 'acl:Control' } ] }, { '@id': '#authenticated-append', '@type': 'acl:Authorization', 'acl:agentClass': { '@id': 'acl:AuthenticatedAgent' }, 'acl:accessTo': { '@id': `${baseUrl}/appendpatch1/public/` }, 'acl:default': { '@id': `${baseUrl}/appendpatch1/public/` }, 'acl:mode': [ { '@id': 'acl:Read' }, { '@id': 'acl:Append' } ] } ] }; await request('/appendpatch1/public/.acl', { method: 'PUT', headers: { 'Content-Type': 'application/ld+json' }, body: JSON.stringify(acl), auth: 'appendpatch1' }); const insertOnlyPatch = ` @prefix solid: . @prefix ex: . _:patch a solid:InsertDeletePatch; solid:inserts { <#it> ex:added "yes" }. `; const res = await request('/appendpatch1/public/item.json', { method: 'PATCH', headers: { 'Content-Type': 'text/n3' }, body: insertOnlyPatch, auth: 'appendwriter1' }); assertStatus(res, 204); }); it('should deny append-only PATCH when patch includes deletes', async () => { await createTestPod('appendpatch2'); await createTestPod('appendwriter2'); const baseUrl = getBaseUrl(); await request('/appendpatch2/public/item.json', { method: 'PUT', headers: { 'Content-Type': 'application/ld+json' }, body: JSON.stringify({ '@context': { ex: 'http://example.org/' }, '@id': '#it', 'ex:name': 'initial' }), auth: 'appendpatch2' }); const acl = { '@context': { acl: 'http://www.w3.org/ns/auth/acl#' }, '@graph': [ { '@id': '#owner', '@type': 'acl:Authorization', 'acl:agent': { '@id': `${baseUrl}/appendpatch2/profile/card.jsonld#me` }, 'acl:accessTo': { '@id': `${baseUrl}/appendpatch2/public/` }, 'acl:default': { '@id': `${baseUrl}/appendpatch2/public/` }, 'acl:mode': [ { '@id': 'acl:Read' }, { '@id': 'acl:Write' }, { '@id': 'acl:Control' } ] }, { '@id': '#authenticated-append', '@type': 'acl:Authorization', 'acl:agentClass': { '@id': 'acl:AuthenticatedAgent' }, 'acl:accessTo': { '@id': `${baseUrl}/appendpatch2/public/` }, 'acl:default': { '@id': `${baseUrl}/appendpatch2/public/` }, 'acl:mode': [ { '@id': 'acl:Read' }, { '@id': 'acl:Append' } ] } ] }; await request('/appendpatch2/public/.acl', { method: 'PUT', headers: { 'Content-Type': 'application/ld+json' }, body: JSON.stringify(acl), auth: 'appendpatch2' }); const deletePatch = ` @prefix solid: . @prefix ex: . _:patch a solid:InsertDeletePatch; solid:deletes { <#it> ex:name "initial" }. `; const res = await request('/appendpatch2/public/item.json', { method: 'PATCH', headers: { 'Content-Type': 'text/n3' }, body: deletePatch, auth: 'appendwriter2' }); assertStatus(res, 403); }); it('should deny public read on inbox', async () => { await createTestPod('inboxread'); // GET inbox should fail for unauthenticated const res = await request('/inboxread/inbox/'); assertStatus(res, 401); }); it('should allow any authenticated user with acl:AuthenticatedAgent', async () => { await createTestPod('authuser1'); await createTestPod('authuser2'); // Create a test resource with acl:AuthenticatedAgent ACL const baseUrl = getBaseUrl(); // First, create a resource (this will create parent containers) await request('/authuser1/authenticated-only/test.txt', { method: 'PUT', headers: { 'Content-Type': 'text/plain' }, body: 'authenticated content', auth: 'authuser1' }); // Now create a custom ACL for the container with acl:AuthenticatedAgent // Include owner with Control so they can manage the ACL const acl = { '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' }, '@graph': [ { '@id': '#owner', '@type': 'acl:Authorization', 'acl:agent': { '@id': `${baseUrl}/authuser1/profile/card.jsonld#me` }, 'acl:accessTo': { '@id': `${baseUrl}/authuser1/authenticated-only/` }, 'acl:default': { '@id': `${baseUrl}/authuser1/authenticated-only/` }, 'acl:mode': [ { '@id': 'acl:Read' }, { '@id': 'acl:Write' }, { '@id': 'acl:Control' } ] }, { '@id': '#authenticated', '@type': 'acl:Authorization', 'acl:agentClass': { '@id': 'acl:AuthenticatedAgent' }, 'acl:accessTo': { '@id': `${baseUrl}/authuser1/authenticated-only/` }, 'acl:default': { '@id': `${baseUrl}/authuser1/authenticated-only/` }, 'acl:mode': [{ '@id': 'acl:Read' }] } ] }; await request('/authuser1/authenticated-only/.acl', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(acl), auth: 'authuser1' }); // Test 1: Anonymous access should be denied const res1 = await request('/authuser1/authenticated-only/test.txt'); assertStatus(res1, 401); // Test 2: Owner should have access const res2 = await request('/authuser1/authenticated-only/test.txt', { auth: 'authuser1' }); assertStatus(res2, 200); // Test 3: Different authenticated user should also have access (key test!) const res3 = await request('/authuser1/authenticated-only/test.txt', { auth: 'authuser2' }); assertStatus(res3, 200); }); }); describe('WAC-Allow Header', () => { it('should include user permissions for authenticated requests', async () => { await createTestPod('wacallow'); const res = await request('/wacallow/public/', { auth: 'wacallow' }); const wacAllow = res.headers.get('WAC-Allow'); assert.ok(wacAllow, 'Should have WAC-Allow header'); assert.ok(wacAllow.includes('user='), 'Should include user permissions'); }); it('should include public permissions', async () => { await createTestPod('wacpublic'); const res = await request('/wacpublic/public/'); const wacAllow = res.headers.get('WAC-Allow'); assert.ok(wacAllow, 'Should have WAC-Allow header'); assert.ok(wacAllow.includes('public='), 'Should include public permissions'); }); }); });