/**
* 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 { 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 { 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');
});
});
});