javascript-solid-server 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +26 -1
- package/CTH.md +222 -0
- package/README.md +32 -1
- package/bin/jss.js +5 -1
- package/cth-config/application.yaml +2 -0
- package/cth-config/jss.ttl +6 -0
- package/cth-config/test-subjects.ttl +14 -0
- package/cth.env +19 -0
- package/package.json +1 -1
- package/scripts/test-cth-compat.js +3 -2
- package/src/auth/middleware.js +6 -2
- package/src/auth/token.js +44 -1
- package/src/handlers/container.js +8 -3
- package/src/handlers/resource.js +65 -4
- package/src/idp/accounts.js +11 -2
- package/src/idp/credentials.js +38 -38
- package/src/idp/index.js +112 -21
- package/src/idp/interactions.js +123 -11
- package/src/idp/provider.js +68 -2
- package/src/rdf/turtle.js +15 -2
- package/src/wac/parser.js +43 -1
- package/test/idp.test.js +17 -14
- package/test/ldp.test.js +10 -5
- package/test-data-idp-accounts/.idp/accounts/292738d6-3363-4f40-9a6b-884bfd17830a.json +9 -0
- package/test-data-idp-accounts/.idp/accounts/_email_index.json +3 -0
- package/test-data-idp-accounts/.idp/accounts/_webid_index.json +3 -0
- package/test-data-idp-accounts/.idp/keys/jwks.json +22 -0
- package/test-dpop-flow.js +148 -0
- package/test-subjects.ttl +21 -0
- package/data/alice/.acl +0 -50
- package/data/alice/inbox/.acl +0 -50
- package/data/alice/index.html +0 -80
- package/data/alice/private/.acl +0 -32
- package/data/alice/public/test.json +0 -1
- package/data/alice/settings/.acl +0 -32
- package/data/alice/settings/prefs +0 -17
- package/data/alice/settings/privateTypeIndex +0 -7
- package/data/alice/settings/publicTypeIndex +0 -7
- package/data/bob/.acl +0 -50
- package/data/bob/inbox/.acl +0 -50
- package/data/bob/index.html +0 -80
- package/data/bob/private/.acl +0 -32
- package/data/bob/settings/.acl +0 -32
- package/data/bob/settings/prefs +0 -17
- package/data/bob/settings/privateTypeIndex +0 -7
- package/data/bob/settings/publicTypeIndex +0 -7
|
@@ -26,7 +26,32 @@
|
|
|
26
26
|
"Bash(npm view:*)",
|
|
27
27
|
"Bash(npm ls:*)",
|
|
28
28
|
"Bash(timeout 10 node:*)",
|
|
29
|
-
"Bash(npm run test:cth:*)"
|
|
29
|
+
"Bash(npm run test:cth:*)",
|
|
30
|
+
"Bash(__NEW_LINE__ curl -s -X POST http://localhost:4000/.pods )",
|
|
31
|
+
"Bash(lsof:*)",
|
|
32
|
+
"Bash(xargs kill -9)",
|
|
33
|
+
"Bash(docker:*)",
|
|
34
|
+
"Bash(solidproject/conformance-test-harness )",
|
|
35
|
+
"Bash(timeout 30 node:*)",
|
|
36
|
+
"Bash(timeout 20 node:*)",
|
|
37
|
+
"Bash(timeout 25 node:*)",
|
|
38
|
+
"Bash(JSS_PORT=4000 JSS_CONNEG=true timeout 5 node:*)",
|
|
39
|
+
"Bash(pgrep:*)",
|
|
40
|
+
"Bash(python3:*)",
|
|
41
|
+
"Bash(ls:*)",
|
|
42
|
+
"Bash(timeout 15 node:*)",
|
|
43
|
+
"Bash(echo 'No .idp folder' echo find /home/melvin/remote/github.com/JavaScriptSolidServer/JavaScriptSolidServer/data/.idp/ -name *.json)",
|
|
44
|
+
"Bash(echo '=== Interactions ===' ls -la /home/melvin/remote/github.com/JavaScriptSolidServer/JavaScriptSolidServer/data/.idp/interaction/ echo echo '=== Latest interaction ===' cat /home/melvin/remote/github.com/JavaScriptSolidServer/JavaScriptSolidServer/data/.idp/interaction/*.json)",
|
|
45
|
+
"Bash(1 echo \"\" echo \"=== Server errors ===\" grep -E \"(error|Error)\" /tmp/jss.log)",
|
|
46
|
+
"Bash(echo 'Server not ready' curl -s -X POST http://localhost:4000/.pods -H 'Content-Type: application/json' -d {\"\"name\"\":\"\"alice\"\",\"\"email\"\":\"\"alice@example.com\"\",\"\"password\"\":\"\"alicepassword123\"\"})",
|
|
47
|
+
"Bash(head -1 curl -s -X POST http://localhost:4000/.pods -H \"Content-Type: application/json\" -d '{\"\"\"\"name\"\"\"\":\"\"\"\"bob\"\"\"\",\"\"\"\"email\"\"\"\":\"\"\"\"bob@example.com\"\"\"\",\"\"\"\"password\"\"\"\":\"\"\"\"bobpassword123\"\"\"\"}')",
|
|
48
|
+
"Bash(xargs:*)",
|
|
49
|
+
"Bash(fuser:*)",
|
|
50
|
+
"Bash(kill:*)",
|
|
51
|
+
"Bash(ACCESS_TOKEN=\"eyJhbGciOiJFUzI1NiIsImtpZCI6IjQwY2U0YzIzLWY2OWQtNDU4NS05ODg2LTE4MDQzZWIyZjU2ZCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAvIiwic3ViIjoiYjhlZjY5YWUtODc0ZS00MDg5LThiMDktOGQwY2QyM2VlZWY3IiwiYXVkIjoic29saWQiLCJ3ZWJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q0MDAwL2FsaWNlLyNtZSIsImlhdCI6MTc2NjgyOTQ5MiwiZXhwIjoxNzY2ODMzMDkyLCJqdGkiOiIwMWY4ODVlZS05ZjY2LTQ3M2MtYmZkNC05MWM4ZGU3NGJhZjYiLCJjbGllbnRfaWQiOiJjcmVkZW50aWFsc19jbGllbnQiLCJzY29wZSI6Im9wZW5pZCB3ZWJpZCJ9.DYTlSRkORyDN28XtXk-zbR7xNLViD97KkPqUKb6chV860BaIgwa1suif4TxHQDnK_ejvbvmZ46_n5WwwRnf_Zw\" curl -sI -X PUT http://localhost:4000/alice/cth-test/ -H \"Content-Type: text/turtle\" -H \"Authorization: Bearer $ACCESS_TOKEN\")",
|
|
52
|
+
"Bash(timeout 60 docker run:*)",
|
|
53
|
+
"Bash(rm:*)",
|
|
54
|
+
"Bash(mkdir:*)"
|
|
30
55
|
]
|
|
31
56
|
}
|
|
32
57
|
}
|
package/CTH.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Running Solid Conformance Test Harness (CTH)
|
|
2
|
+
|
|
3
|
+
Step-by-step instructions for running the Solid Conformance Test Harness against this server.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js 18+
|
|
8
|
+
- Docker
|
|
9
|
+
- Port 4000 available
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. Kill any existing server on port 4000
|
|
15
|
+
fuser -k 4000/tcp 2>/dev/null || true
|
|
16
|
+
|
|
17
|
+
# 2. Clean data directory
|
|
18
|
+
rm -rf data && mkdir data
|
|
19
|
+
|
|
20
|
+
# 3. Start server with IdP and content negotiation
|
|
21
|
+
JSS_PORT=4000 JSS_CONNEG=true JSS_IDP=true node bin/jss.js start &
|
|
22
|
+
|
|
23
|
+
# 4. Wait for server to be ready
|
|
24
|
+
sleep 3
|
|
25
|
+
curl -s http://localhost:4000/ > /dev/null && echo "Server ready"
|
|
26
|
+
|
|
27
|
+
# 5. Create test users
|
|
28
|
+
curl -s -X POST http://localhost:4000/.pods \
|
|
29
|
+
-H "Content-Type: application/json" \
|
|
30
|
+
-d '{"name": "alice", "email": "alice@example.com", "password": "alicepassword123"}'
|
|
31
|
+
|
|
32
|
+
curl -s -X POST http://localhost:4000/.pods \
|
|
33
|
+
-H "Content-Type: application/json" \
|
|
34
|
+
-d '{"name": "bob", "email": "bob@example.com", "password": "bobpassword123"}'
|
|
35
|
+
|
|
36
|
+
# 6. Create test container (required by CTH)
|
|
37
|
+
mkdir -p data/alice/cth-test
|
|
38
|
+
|
|
39
|
+
# 7. Run authentication tests (assumes test-subjects.ttl and cth.env exist - see Configuration Files below)
|
|
40
|
+
docker run --rm --network=host \
|
|
41
|
+
-v $(pwd)/test-subjects.ttl:/app/test-subjects.ttl \
|
|
42
|
+
--env-file cth.env \
|
|
43
|
+
-e SUBJECTS=/app/test-subjects.ttl \
|
|
44
|
+
solidproject/conformance-test-harness:latest \
|
|
45
|
+
--target="https://github.com/solid/conformance-test-harness/jss" \
|
|
46
|
+
--filter="authentication"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration Files
|
|
50
|
+
|
|
51
|
+
### Test Subjects File (test-subjects.ttl)
|
|
52
|
+
|
|
53
|
+
Create `test-subjects.ttl`:
|
|
54
|
+
|
|
55
|
+
```turtle
|
|
56
|
+
@base <https://github.com/solid/conformance-test-harness/> .
|
|
57
|
+
@prefix solid-test: <https://github.com/solid/conformance-test-harness/vocab#> .
|
|
58
|
+
@prefix doap: <http://usefulinc.com/ns/doap#> .
|
|
59
|
+
@prefix earl: <http://www.w3.org/ns/earl#> .
|
|
60
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
61
|
+
|
|
62
|
+
<jss>
|
|
63
|
+
a earl:Software, earl:TestSubject ;
|
|
64
|
+
doap:name "JavaScript Solid Server"@en ;
|
|
65
|
+
doap:release <jss#test-subject-release> ;
|
|
66
|
+
doap:developer <https://github.com/JavaScriptSolidServer> ;
|
|
67
|
+
doap:homepage <https://github.com/JavaScriptSolidServer/JavaScriptSolidServer> ;
|
|
68
|
+
doap:description "A minimal, fast, JSON-LD native Solid server."@en ;
|
|
69
|
+
doap:programming-language "JavaScript"@en ;
|
|
70
|
+
solid-test:skip "acp", "wac", "wac-allow-public" .
|
|
71
|
+
|
|
72
|
+
<jss#test-subject-release>
|
|
73
|
+
doap:revision "0.0.14"@en ;
|
|
74
|
+
doap:created "2025-12-27"^^xsd:date .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Environment File (cth.env)
|
|
78
|
+
|
|
79
|
+
Create `cth.env`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
SERVER_ROOT=http://localhost:4000
|
|
83
|
+
TEST_CONTAINER=/alice/cth-test/
|
|
84
|
+
RESOURCE_SERVER_ROOT=http://localhost:4000
|
|
85
|
+
LOGIN_ENDPOINT=http://localhost:4000/idp/credentials
|
|
86
|
+
SOLID_IDENTITY_PROVIDER=http://localhost:4000/
|
|
87
|
+
USERS_ALICE_IDP=http://localhost:4000/
|
|
88
|
+
USERS_BOB_IDP=http://localhost:4000/
|
|
89
|
+
USERS_ALICE_WEBID=http://localhost:4000/alice/#me
|
|
90
|
+
USERS_BOB_WEBID=http://localhost:4000/bob/#me
|
|
91
|
+
USERS_ALICE_USERNAME=alice@example.com
|
|
92
|
+
USERS_ALICE_PASSWORD=alicepassword123
|
|
93
|
+
USERS_BOB_USERNAME=bob@example.com
|
|
94
|
+
USERS_BOB_PASSWORD=bobpassword123
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Environment Variables Reference
|
|
98
|
+
|
|
99
|
+
| Variable | Description | Example |
|
|
100
|
+
|----------|-------------|---------|
|
|
101
|
+
| `SERVER_ROOT` | Server base URL | `http://localhost:4000` |
|
|
102
|
+
| `TEST_CONTAINER` | Path to test container | `/alice/cth-test/` |
|
|
103
|
+
| `SOLID_IDENTITY_PROVIDER` | IdP issuer URL (with trailing slash) | `http://localhost:4000/` |
|
|
104
|
+
| `USERS_ALICE_IDP` | Alice's IdP | `http://localhost:4000/` |
|
|
105
|
+
| `USERS_ALICE_WEBID` | Alice's WebID | `http://localhost:4000/alice/#me` |
|
|
106
|
+
| `USERS_ALICE_USERNAME` | Alice's email | `alice@example.com` |
|
|
107
|
+
| `USERS_ALICE_PASSWORD` | Alice's password | `alicepassword123` |
|
|
108
|
+
| `USERS_BOB_IDP` | Bob's IdP | `http://localhost:4000/` |
|
|
109
|
+
| `USERS_BOB_WEBID` | Bob's WebID | `http://localhost:4000/bob/#me` |
|
|
110
|
+
| `USERS_BOB_USERNAME` | Bob's email | `bob@example.com` |
|
|
111
|
+
| `USERS_BOB_PASSWORD` | Bob's password | `bobpassword123` |
|
|
112
|
+
| `SUBJECTS` | Path to test-subjects.ttl inside container | `/app/test-subjects.ttl` |
|
|
113
|
+
|
|
114
|
+
## Running Specific Test Suites
|
|
115
|
+
|
|
116
|
+
### Authentication Tests (6 scenarios)
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
docker run --rm --network=host \
|
|
120
|
+
--env-file cth.env \
|
|
121
|
+
-v $(pwd)/test-subjects.ttl:/app/test-subjects.ttl \
|
|
122
|
+
-e SUBJECTS=/app/test-subjects.ttl \
|
|
123
|
+
solidproject/conformance-test-harness:latest \
|
|
124
|
+
--target="https://github.com/solid/conformance-test-harness/jss" \
|
|
125
|
+
--filter="authentication"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Expected result:** 6/6 scenarios passing
|
|
129
|
+
|
|
130
|
+
### All Protocol Tests
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
docker run --rm --network=host \
|
|
134
|
+
--env-file cth.env \
|
|
135
|
+
-v $(pwd)/test-subjects.ttl:/app/test-subjects.ttl \
|
|
136
|
+
-e SUBJECTS=/app/test-subjects.ttl \
|
|
137
|
+
solidproject/conformance-test-harness:latest \
|
|
138
|
+
--target="https://github.com/solid/conformance-test-harness/jss"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Interpreting Results
|
|
142
|
+
|
|
143
|
+
### Success Output
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
scenarios: 6 | passed: 6 | failed: 0 | time: 0.6349
|
|
147
|
+
MustFeatures passed: 1, failed: 0
|
|
148
|
+
MustScenarios passed: 6, failed: 0
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Failure Output
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
scenarios: 6 | passed: 4 | failed: 2 | time: 0.7952
|
|
155
|
+
Then status 401
|
|
156
|
+
status code was: 200, expected: 401, response time in milliseconds: 15
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Troubleshooting
|
|
160
|
+
|
|
161
|
+
### "Cannot get ACL url for root test container"
|
|
162
|
+
|
|
163
|
+
The test container doesn't exist. Create it:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
mkdir -p data/alice/cth-test
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### "Failed to read WebID Document" (401)
|
|
170
|
+
|
|
171
|
+
The WebID profile is not publicly readable. Check that the pod's ACL allows public read on the container itself (but not necessarily on children).
|
|
172
|
+
|
|
173
|
+
### "NullPointerException" during authentication
|
|
174
|
+
|
|
175
|
+
Usually means the IdP isn't returning proper responses. Check:
|
|
176
|
+
1. Server is running with `--idp` flag
|
|
177
|
+
2. Issuer URL has trailing slash
|
|
178
|
+
3. Users were created with email and password
|
|
179
|
+
|
|
180
|
+
### "DPoP htu mismatch"
|
|
181
|
+
|
|
182
|
+
URL mismatch in DPoP proof validation. Check that issuer URL doesn't have double slashes.
|
|
183
|
+
|
|
184
|
+
### Token format errors
|
|
185
|
+
|
|
186
|
+
Ensure the server returns JWT access tokens with:
|
|
187
|
+
- `aud: "solid"` claim
|
|
188
|
+
- 3-part JWT format (header.payload.signature)
|
|
189
|
+
- `webid` claim
|
|
190
|
+
|
|
191
|
+
## Server Requirements for CTH
|
|
192
|
+
|
|
193
|
+
The server must support:
|
|
194
|
+
|
|
195
|
+
1. **Solid-OIDC Identity Provider**
|
|
196
|
+
- OIDC discovery at `/.well-known/openid-configuration`
|
|
197
|
+
- JWKS at `/.well-known/jwks.json`
|
|
198
|
+
- Dynamic client registration at `/idp/reg`
|
|
199
|
+
- Credentials endpoint at `/idp/credentials` (for programmatic login)
|
|
200
|
+
|
|
201
|
+
2. **DPoP Token Binding**
|
|
202
|
+
- RS256 and ES256 algorithms
|
|
203
|
+
- Proper `cnf.jkt` claim in tokens
|
|
204
|
+
|
|
205
|
+
3. **WWW-Authenticate Header**
|
|
206
|
+
- 401 responses must include `WWW-Authenticate: DPoP realm="..."`
|
|
207
|
+
|
|
208
|
+
4. **Container Creation via PUT**
|
|
209
|
+
- PUT to path ending with `/` creates container
|
|
210
|
+
|
|
211
|
+
5. **ACL Inheritance**
|
|
212
|
+
- Children should NOT inherit public read by default
|
|
213
|
+
- Only owner permissions should have `acl:default`
|
|
214
|
+
|
|
215
|
+
## Current CTH Status (v0.0.14)
|
|
216
|
+
|
|
217
|
+
| Test Suite | Status |
|
|
218
|
+
|------------|--------|
|
|
219
|
+
| Authentication | 6/6 passing |
|
|
220
|
+
| Protocol (other) | Not yet tested |
|
|
221
|
+
| WAC | Skipped |
|
|
222
|
+
| ACP | Skipped |
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ npm run benchmark
|
|
|
54
54
|
|
|
55
55
|
## Features
|
|
56
56
|
|
|
57
|
-
### Implemented (v0.0.
|
|
57
|
+
### Implemented (v0.0.15)
|
|
58
58
|
|
|
59
59
|
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
|
|
60
60
|
- **N3 Patch** - Solid's native patch format for RDF updates
|
|
@@ -379,6 +379,37 @@ npm test
|
|
|
379
379
|
|
|
380
380
|
Currently passing: **182 tests** (including 27 conformance tests)
|
|
381
381
|
|
|
382
|
+
### Conformance Test Harness (CTH)
|
|
383
|
+
|
|
384
|
+
This server passes the Solid Conformance Test Harness authentication tests:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
# Start server with IdP and content negotiation
|
|
388
|
+
JSS_PORT=4000 JSS_CONNEG=true JSS_IDP=true jss start
|
|
389
|
+
|
|
390
|
+
# Create test users
|
|
391
|
+
curl -X POST http://localhost:4000/.pods \
|
|
392
|
+
-H "Content-Type: application/json" \
|
|
393
|
+
-d '{"name": "alice", "email": "alice@example.com", "password": "alicepassword123"}'
|
|
394
|
+
|
|
395
|
+
curl -X POST http://localhost:4000/.pods \
|
|
396
|
+
-H "Content-Type: application/json" \
|
|
397
|
+
-d '{"name": "bob", "email": "bob@example.com", "password": "bobpassword123"}'
|
|
398
|
+
|
|
399
|
+
# Run CTH authentication tests
|
|
400
|
+
docker run --rm --network=host \
|
|
401
|
+
-e SOLID_IDENTITY_PROVIDER="http://localhost:4000/" \
|
|
402
|
+
-e USERS_ALICE_WEBID="http://localhost:4000/alice/#me" \
|
|
403
|
+
-e USERS_ALICE_PASSWORD="alicepassword123" \
|
|
404
|
+
-e USERS_BOB_WEBID="http://localhost:4000/bob/#me" \
|
|
405
|
+
-e USERS_BOB_PASSWORD="bobpassword123" \
|
|
406
|
+
solidproject/conformance-test-harness:latest \
|
|
407
|
+
--filter="authentication"
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**CTH Status (v0.0.15):**
|
|
411
|
+
- Authentication tests: 6/6 passing
|
|
412
|
+
|
|
382
413
|
## Project Structure
|
|
383
414
|
|
|
384
415
|
```
|
package/bin/jss.js
CHANGED
|
@@ -62,7 +62,11 @@ program
|
|
|
62
62
|
const protocol = config.ssl ? 'https' : 'http';
|
|
63
63
|
const serverHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
|
|
64
64
|
const baseUrl = `${protocol}://${serverHost}:${config.port}`;
|
|
65
|
-
|
|
65
|
+
// Ensure issuer has trailing slash for CTH compatibility
|
|
66
|
+
let idpIssuer = config.idpIssuer || baseUrl;
|
|
67
|
+
if (idpIssuer && !idpIssuer.endsWith('/')) {
|
|
68
|
+
idpIssuer = idpIssuer + '/';
|
|
69
|
+
}
|
|
66
70
|
|
|
67
71
|
// Create and start server
|
|
68
72
|
const server = createServer({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@prefix doap: <http://usefulinc.com/ns/doap#> .
|
|
2
|
+
@prefix earl: <http://www.w3.org/ns/earl#> .
|
|
3
|
+
@prefix solid-test: <https://github.com/solid-contrib/specification-tests/blob/main/vocab.ttl#> .
|
|
4
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
5
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
6
|
+
|
|
7
|
+
<jss>
|
|
8
|
+
a earl:Software, earl:TestSubject ;
|
|
9
|
+
doap:name "JavaScript Solid Server" ;
|
|
10
|
+
doap:description "A minimal, fast, JSON-LD native Solid server" ;
|
|
11
|
+
doap:programming-language "JavaScript" ;
|
|
12
|
+
solid-test:serverRoot <http://localhost:4000/> ;
|
|
13
|
+
solid-test:skip "acp" ;
|
|
14
|
+
rdfs:comment "Uses WAC for access control" .
|
package/cth.env
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# CTH Environment for JSS
|
|
2
|
+
# Generated by test-cth-compat.js
|
|
3
|
+
|
|
4
|
+
SOLID_IDENTITY_PROVIDER=http://localhost:3456
|
|
5
|
+
RESOURCE_SERVER_ROOT=http://localhost:3456
|
|
6
|
+
TEST_CONTAINER=alice/public/
|
|
7
|
+
|
|
8
|
+
USERS_ALICE_WEBID=http://localhost:3456/alice/#me
|
|
9
|
+
USERS_ALICE_USERNAME=alice@test.local
|
|
10
|
+
USERS_ALICE_PASSWORD=alicepassword123
|
|
11
|
+
|
|
12
|
+
USERS_BOB_WEBID=http://localhost:3456/bob/#me
|
|
13
|
+
USERS_BOB_USERNAME=bob@test.local
|
|
14
|
+
USERS_BOB_PASSWORD=bobpassword123
|
|
15
|
+
|
|
16
|
+
LOGIN_ENDPOINT=http://localhost:3456/idp/credentials
|
|
17
|
+
|
|
18
|
+
# For self-signed certs
|
|
19
|
+
ALLOW_SELF_SIGNED_CERTS=true
|
package/package.json
CHANGED
|
@@ -88,11 +88,12 @@ async function main() {
|
|
|
88
88
|
const res = await fetch(`${BASE_URL}/.well-known/openid-configuration`);
|
|
89
89
|
if (res.status === 200) {
|
|
90
90
|
const config = await res.json();
|
|
91
|
-
|
|
91
|
+
// Issuer has trailing slash for CTH compatibility
|
|
92
|
+
if (config.issuer === BASE_URL + '/') {
|
|
92
93
|
pass('/.well-known/openid-configuration returns valid config');
|
|
93
94
|
passed++;
|
|
94
95
|
} else {
|
|
95
|
-
fail(`Issuer mismatch: expected ${BASE_URL}
|
|
96
|
+
fail(`Issuer mismatch: expected ${BASE_URL}/, got ${config.issuer}`);
|
|
96
97
|
failed++;
|
|
97
98
|
}
|
|
98
99
|
} else {
|
package/src/auth/middleware.js
CHANGED
|
@@ -79,12 +79,16 @@ function getParentPath(path) {
|
|
|
79
79
|
* @param {boolean} isAuthenticated - Whether user is authenticated
|
|
80
80
|
* @param {string} wacAllow - WAC-Allow header value
|
|
81
81
|
* @param {string|null} authError - Authentication error message (for DPoP failures)
|
|
82
|
+
* @param {string|null} issuer - IdP issuer URL for WWW-Authenticate header
|
|
82
83
|
*/
|
|
83
|
-
export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null) {
|
|
84
|
+
export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null, issuer = null) {
|
|
84
85
|
reply.header('WAC-Allow', wacAllow);
|
|
85
86
|
|
|
86
87
|
if (!isAuthenticated) {
|
|
87
|
-
// Not authenticated - return 401
|
|
88
|
+
// Not authenticated - return 401 with WWW-Authenticate header
|
|
89
|
+
// Solid-OIDC requires DPoP authentication
|
|
90
|
+
const realm = issuer || 'Solid';
|
|
91
|
+
reply.header('WWW-Authenticate', `DPoP realm="${realm}", Bearer realm="${realm}"`);
|
|
88
92
|
return reply.code(401).send({
|
|
89
93
|
error: 'Unauthorized',
|
|
90
94
|
message: authError || 'Authentication required'
|
package/src/auth/token.js
CHANGED
|
@@ -35,7 +35,7 @@ export function createToken(webId, expiresIn = 3600) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Verify and decode a token
|
|
38
|
+
* Verify and decode a token (simple 2-part or JWT 3-part)
|
|
39
39
|
* @param {string} token - The token to verify
|
|
40
40
|
* @returns {{webId: string, iat: number, exp: number} | null} Decoded payload or null
|
|
41
41
|
*/
|
|
@@ -45,6 +45,12 @@ export function verifyToken(token) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const parts = token.split('.');
|
|
48
|
+
|
|
49
|
+
// Handle JWT tokens (3 parts) from credentials endpoint
|
|
50
|
+
if (parts.length === 3) {
|
|
51
|
+
return verifyJwtToken(token);
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
if (parts.length !== 2) {
|
|
49
55
|
return null;
|
|
50
56
|
}
|
|
@@ -76,6 +82,43 @@ export function verifyToken(token) {
|
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Verify a JWT token from credentials endpoint
|
|
87
|
+
* JWT tokens are self-contained and signed with the IdP's private key
|
|
88
|
+
* @param {string} token - JWT token
|
|
89
|
+
* @returns {{webId: string, iat: number, exp: number} | null} Decoded payload or null
|
|
90
|
+
*/
|
|
91
|
+
function verifyJwtToken(token) {
|
|
92
|
+
try {
|
|
93
|
+
const parts = token.split('.');
|
|
94
|
+
if (parts.length !== 3) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Decode the payload (middle part)
|
|
99
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
100
|
+
|
|
101
|
+
// Check expiration
|
|
102
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// JWT from credentials endpoint uses 'webid' claim (lowercase)
|
|
107
|
+
if (payload.webid) {
|
|
108
|
+
return { webId: payload.webid, iat: payload.iat, exp: payload.exp };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Also check uppercase WebId for compatibility
|
|
112
|
+
if (payload.webId) {
|
|
113
|
+
return payload;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
79
122
|
/**
|
|
80
123
|
* Extract token from Authorization header
|
|
81
124
|
* @param {string} authHeader - Authorization header value
|
|
@@ -2,7 +2,7 @@ import * as storage from '../storage/filesystem.js';
|
|
|
2
2
|
import { getAllHeaders } from '../ldp/headers.js';
|
|
3
3
|
import { isContainer } from '../utils/url.js';
|
|
4
4
|
import { generateProfile, generatePreferences, generateTypeIndex, serialize } from '../webid/profile.js';
|
|
5
|
-
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, serializeAcl } from '../wac/parser.js';
|
|
5
|
+
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, generatePublicFolderAcl, serializeAcl } from '../wac/parser.js';
|
|
6
6
|
import { createToken } from '../auth/token.js';
|
|
7
7
|
import { canAcceptInput, toJsonLd, getVaryHeader, RDF_TYPES } from '../rdf/conneg.js';
|
|
8
8
|
import { emitChange } from '../notifications/events.js';
|
|
@@ -157,7 +157,8 @@ export async function handleCreatePod(request, reply) {
|
|
|
157
157
|
const baseUri = `${request.protocol}://${request.hostname}`;
|
|
158
158
|
const podUri = `${baseUri}${podPath}`;
|
|
159
159
|
const webId = `${podUri}#me`;
|
|
160
|
-
|
|
160
|
+
// Issuer needs trailing slash for CTH compatibility
|
|
161
|
+
const issuer = baseUri + '/';
|
|
161
162
|
|
|
162
163
|
try {
|
|
163
164
|
// Create pod directory structure
|
|
@@ -199,6 +200,10 @@ export async function handleCreatePod(request, reply) {
|
|
|
199
200
|
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
|
|
200
201
|
await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
|
|
201
202
|
|
|
203
|
+
// Public folder: owner full, public read (with inheritance)
|
|
204
|
+
const publicAcl = generatePublicFolderAcl(`${podUri}public/`, webId);
|
|
205
|
+
await storage.write(`${podPath}public/.acl`, serializeAcl(publicAcl));
|
|
206
|
+
|
|
202
207
|
} catch (err) {
|
|
203
208
|
console.error('Pod creation error:', err);
|
|
204
209
|
// Cleanup on failure
|
|
@@ -223,7 +228,7 @@ export async function handleCreatePod(request, reply) {
|
|
|
223
228
|
webId,
|
|
224
229
|
podUri,
|
|
225
230
|
idpIssuer: issuer,
|
|
226
|
-
loginUrl: `${
|
|
231
|
+
loginUrl: `${baseUri}/idp/auth`,
|
|
227
232
|
});
|
|
228
233
|
} catch (err) {
|
|
229
234
|
console.error('Account creation error:', err);
|
package/src/handlers/resource.js
CHANGED
|
@@ -51,6 +51,46 @@ export async function handleGet(request, reply) {
|
|
|
51
51
|
const content = await storage.read(indexPath);
|
|
52
52
|
const indexStats = await storage.stat(indexPath);
|
|
53
53
|
|
|
54
|
+
// Check if RDF format requested via content negotiation
|
|
55
|
+
const acceptHeader = request.headers.accept || '';
|
|
56
|
+
const wantsTurtle = connegEnabled && (
|
|
57
|
+
acceptHeader.includes('text/turtle') ||
|
|
58
|
+
acceptHeader.includes('text/n3') ||
|
|
59
|
+
acceptHeader.includes('application/n-triples')
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (wantsTurtle) {
|
|
63
|
+
// Extract JSON-LD from HTML and convert to Turtle
|
|
64
|
+
try {
|
|
65
|
+
const htmlStr = content.toString();
|
|
66
|
+
const jsonLdMatch = htmlStr.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
|
|
67
|
+
if (jsonLdMatch) {
|
|
68
|
+
const jsonLd = JSON.parse(jsonLdMatch[1]);
|
|
69
|
+
const { content: turtleContent } = await fromJsonLd(
|
|
70
|
+
jsonLd,
|
|
71
|
+
'text/turtle',
|
|
72
|
+
resourceUrl,
|
|
73
|
+
true
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const headers = getAllHeaders({
|
|
77
|
+
isContainer: true,
|
|
78
|
+
etag: indexStats?.etag || stats.etag,
|
|
79
|
+
contentType: 'text/turtle',
|
|
80
|
+
origin,
|
|
81
|
+
resourceUrl,
|
|
82
|
+
connegEnabled
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
86
|
+
return reply.send(turtleContent);
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Fall through to serve HTML if conversion fails
|
|
90
|
+
console.error('Failed to convert profile to Turtle:', err.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
54
94
|
const headers = getAllHeaders({
|
|
55
95
|
isContainer: true,
|
|
56
96
|
etag: indexStats?.etag || stats.etag,
|
|
@@ -176,15 +216,36 @@ export async function handleHead(request, reply) {
|
|
|
176
216
|
*/
|
|
177
217
|
export async function handlePut(request, reply) {
|
|
178
218
|
const urlPath = request.url.split('?')[0];
|
|
219
|
+
const connegEnabled = request.connegEnabled || false;
|
|
220
|
+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
179
221
|
|
|
180
|
-
//
|
|
222
|
+
// Handle container creation via PUT
|
|
181
223
|
if (isContainer(urlPath)) {
|
|
182
|
-
|
|
224
|
+
const stats = await storage.stat(urlPath);
|
|
225
|
+
if (stats?.isDirectory) {
|
|
226
|
+
// Container already exists - don't allow PUT to modify
|
|
227
|
+
return reply.code(409).send({ error: 'Cannot PUT to existing container' });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create the container (and any intermediate containers)
|
|
231
|
+
const success = await storage.createContainer(urlPath);
|
|
232
|
+
if (!success) {
|
|
233
|
+
return reply.code(500).send({ error: 'Failed to create container' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const origin = request.headers.origin;
|
|
237
|
+
const headers = getAllHeaders({
|
|
238
|
+
isContainer: true,
|
|
239
|
+
origin,
|
|
240
|
+
connegEnabled
|
|
241
|
+
});
|
|
242
|
+
headers['Location'] = resourceUrl;
|
|
243
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
244
|
+
emitChange(request.protocol + '://' + request.hostname, urlPath, 'created');
|
|
245
|
+
return reply.code(201).send();
|
|
183
246
|
}
|
|
184
247
|
|
|
185
|
-
const connegEnabled = request.connegEnabled || false;
|
|
186
248
|
const contentType = request.headers['content-type'] || '';
|
|
187
|
-
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
188
249
|
|
|
189
250
|
// Check if we can accept this input type
|
|
190
251
|
if (!canAcceptInput(contentType, connegEnabled)) {
|
package/src/idp/accounts.js
CHANGED
|
@@ -241,13 +241,22 @@ export async function getAccountForProvider(id) {
|
|
|
241
241
|
// Always include webid for Solid-OIDC
|
|
242
242
|
result.webid = account.webId;
|
|
243
243
|
|
|
244
|
+
// Handle scope being a string, array, Set, or object with keys
|
|
245
|
+
const hasScope = (s) => {
|
|
246
|
+
if (typeof scope === 'string') return scope.includes(s);
|
|
247
|
+
if (Array.isArray(scope)) return scope.includes(s);
|
|
248
|
+
if (scope instanceof Set) return scope.has(s);
|
|
249
|
+
if (scope && typeof scope === 'object') return s in scope || Object.keys(scope).includes(s);
|
|
250
|
+
return false;
|
|
251
|
+
};
|
|
252
|
+
|
|
244
253
|
// Profile scope
|
|
245
|
-
if (
|
|
254
|
+
if (hasScope('profile')) {
|
|
246
255
|
result.name = account.podName;
|
|
247
256
|
}
|
|
248
257
|
|
|
249
258
|
// Email scope
|
|
250
|
-
if (
|
|
259
|
+
if (hasScope('email')) {
|
|
251
260
|
result.email = account.email;
|
|
252
261
|
result.email_verified = false; // We don't have email verification yet
|
|
253
262
|
}
|