-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathmiddleware.js
More file actions
114 lines (98 loc) · 4.05 KB
/
middleware.js
File metadata and controls
114 lines (98 loc) · 4.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* Authorization middleware
* Combines authentication (token verification) with WAC checking
* Supports both simple Bearer tokens and Solid-OIDC DPoP tokens
*/
import { getWebIdFromRequestAsync } from './token.js';
import { checkAccess, getRequiredMode } from '../wac/checker.js';
import * as storage from '../storage/filesystem.js';
import { getEffectiveUrlPath } from '../utils/url.js';
/**
* Check if request is authorized
* @param {object} request - Fastify request
* @param {object} reply - Fastify reply
* @returns {Promise<{authorized: boolean, webId: string|null, wacAllow: string, authError: string|null}>}
*/
export async function authorize(request, reply) {
const urlPath = request.url.split('?')[0];
const method = request.method;
// Skip auth for .acl files (they need special handling)
// and for OPTIONS (CORS preflight)
if (urlPath.endsWith('.acl') || method === 'OPTIONS') {
return { authorized: true, webId: null, wacAllow: 'user="read write append control", public="read write append"', authError: null };
}
// Get WebID from token (supports both simple and Solid-OIDC tokens)
const { webId, error: authError } = await getWebIdFromRequestAsync(request);
// Log auth failures for debugging
if (authError) {
request.log.warn({ authError, method, urlPath, hasAuth: !!request.headers.authorization }, 'Auth error');
}
// Get effective storage path (includes pod name in subdomain mode)
const storagePath = getEffectiveUrlPath(request);
// Get resource info
const stats = await storage.stat(storagePath);
const resourceExists = stats !== null;
const isContainer = stats?.isDirectory || urlPath.endsWith('/');
// Build resource URL (uses actual request hostname which may be subdomain)
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
// Get required access mode for this method
const requiredMode = getRequiredMode(method);
// For write operations on non-existent resources, check parent container
let checkPath = storagePath;
let checkUrl = resourceUrl;
let checkIsContainer = isContainer;
if (!resourceExists && (method === 'PUT' || method === 'POST' || method === 'PATCH')) {
// Check write permission on parent container
const parentPath = getParentPath(storagePath);
checkPath = parentPath;
// For URL, also need to get parent
const parentUrlPath = getParentPath(urlPath);
checkUrl = `${request.protocol}://${request.hostname}${parentUrlPath}`;
checkIsContainer = true;
}
// Check WAC permissions
const { allowed, wacAllow } = await checkAccess({
resourceUrl: checkUrl,
resourcePath: checkPath,
isContainer: checkIsContainer,
agentWebId: webId,
requiredMode
});
return { authorized: allowed, webId, wacAllow, authError };
}
/**
* Get parent container path
*/
function getParentPath(path) {
const normalized = path.endsWith('/') ? path.slice(0, -1) : path;
const lastSlash = normalized.lastIndexOf('/');
if (lastSlash <= 0) return '/';
return normalized.substring(0, lastSlash + 1);
}
/**
* Handle unauthorized request
* @param {object} reply - Fastify reply
* @param {boolean} isAuthenticated - Whether user is authenticated
* @param {string} wacAllow - WAC-Allow header value
* @param {string|null} authError - Authentication error message (for DPoP failures)
* @param {string|null} issuer - IdP issuer URL for WWW-Authenticate header
*/
export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null, issuer = null) {
reply.header('WAC-Allow', wacAllow);
if (!isAuthenticated) {
// Not authenticated - return 401 with WWW-Authenticate header
// Solid-OIDC requires DPoP authentication
const realm = issuer || 'Solid';
reply.header('WWW-Authenticate', `DPoP realm="${realm}", Bearer realm="${realm}"`);
return reply.code(401).send({
error: 'Unauthorized',
message: authError || 'Authentication required'
});
} else {
// Authenticated but not authorized - return 403
return reply.code(403).send({
error: 'Forbidden',
message: 'Access denied'
});
}
}