javascript-solid-server 0.0.13 → 0.0.16
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 +27 -1
- package/CTH.md +222 -0
- package/README.md +92 -3
- package/bin/jss.js +11 -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 +17 -7
- package/src/auth/token.js +44 -1
- package/src/config.js +7 -0
- package/src/handlers/container.js +49 -16
- package/src/handlers/resource.js +99 -32
- 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/server.js +24 -0
- package/src/utils/url.js +52 -0
- 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/3c1cd503-1d7f-4ba0-a3af-ebedf519594d.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,33 @@
|
|
|
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:*)",
|
|
55
|
+
"WebFetch(domain:communitysolidserver.github.io)"
|
|
30
56
|
]
|
|
31
57
|
}
|
|
32
58
|
}
|
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.16)
|
|
58
58
|
|
|
59
59
|
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
|
|
60
60
|
- **N3 Patch** - Solid's native patch format for RDF updates
|
|
@@ -64,7 +64,8 @@ npm run benchmark
|
|
|
64
64
|
- **SSL/TLS** - HTTPS support with certificate configuration
|
|
65
65
|
- **WebSocket Notifications** - Real-time updates via solid-0.1 protocol (SolidOS compatible)
|
|
66
66
|
- **Container Management** - Create, list, and manage containers
|
|
67
|
-
- **Multi-user Pods** -
|
|
67
|
+
- **Multi-user Pods** - Path-based (`/alice/`) or subdomain-based (`alice.example.com`)
|
|
68
|
+
- **Subdomain Mode** - XSS protection via origin isolation
|
|
68
69
|
- **WebID Profiles** - JSON-LD structured data in HTML at pod root
|
|
69
70
|
- **Web Access Control (WAC)** - `.acl` file-based authorization
|
|
70
71
|
- **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, dynamic registration
|
|
@@ -135,6 +136,8 @@ jss --help # Show help
|
|
|
135
136
|
| `--notifications` | Enable WebSocket | false |
|
|
136
137
|
| `--idp` | Enable built-in IdP | false |
|
|
137
138
|
| `--idp-issuer <url>` | IdP issuer URL | (auto) |
|
|
139
|
+
| `--subdomains` | Enable subdomain-based pods | false |
|
|
140
|
+
| `--base-domain <domain>` | Base domain for subdomains | - |
|
|
138
141
|
| `-q, --quiet` | Suppress logs | false |
|
|
139
142
|
|
|
140
143
|
### Environment Variables
|
|
@@ -146,6 +149,8 @@ export JSS_PORT=8443
|
|
|
146
149
|
export JSS_SSL_KEY=/path/to/key.pem
|
|
147
150
|
export JSS_SSL_CERT=/path/to/cert.pem
|
|
148
151
|
export JSS_CONNEG=true
|
|
152
|
+
export JSS_SUBDOMAINS=true
|
|
153
|
+
export JSS_BASE_DOMAIN=example.com
|
|
149
154
|
jss start
|
|
150
155
|
```
|
|
151
156
|
|
|
@@ -338,13 +343,66 @@ curl -H "Authorization: DPoP ACCESS_TOKEN" \
|
|
|
338
343
|
http://localhost:3000/alice/private/
|
|
339
344
|
```
|
|
340
345
|
|
|
346
|
+
## Subdomain Mode (XSS Protection)
|
|
347
|
+
|
|
348
|
+
By default, JSS uses **path-based pods** (`/alice/`, `/bob/`). This is simple but has a security limitation: all pods share the same origin, making cross-site scripting (XSS) attacks possible between pods.
|
|
349
|
+
|
|
350
|
+
**Subdomain mode** provides **origin isolation** - each pod gets its own subdomain (`alice.example.com`, `bob.example.com`), preventing XSS attacks between pods.
|
|
351
|
+
|
|
352
|
+
### Why Subdomain Mode?
|
|
353
|
+
|
|
354
|
+
| Mode | URL | Origin | XSS Risk |
|
|
355
|
+
|------|-----|--------|----------|
|
|
356
|
+
| Path-based | `example.com/alice/` | `example.com` | Shared origin - pods can XSS each other |
|
|
357
|
+
| Subdomain | `alice.example.com/` | `alice.example.com` | Isolated - browser's Same-Origin Policy protects |
|
|
358
|
+
|
|
359
|
+
### Enabling Subdomain Mode
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
jss start --subdomains --base-domain example.com
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Or via environment variables:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
export JSS_SUBDOMAINS=true
|
|
369
|
+
export JSS_BASE_DOMAIN=example.com
|
|
370
|
+
jss start
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### DNS Configuration
|
|
374
|
+
|
|
375
|
+
You need a **wildcard DNS record** pointing to your server:
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
*.example.com A <your-server-ip>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Pod URLs in Subdomain Mode
|
|
382
|
+
|
|
383
|
+
| Path Mode | Subdomain Mode |
|
|
384
|
+
|-----------|----------------|
|
|
385
|
+
| `example.com/alice/` | `alice.example.com/` |
|
|
386
|
+
| `example.com/alice/public/file.txt` | `alice.example.com/public/file.txt` |
|
|
387
|
+
| `example.com/alice/#me` | `alice.example.com/#me` |
|
|
388
|
+
|
|
389
|
+
Pod creation still uses the main domain:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
curl -X POST https://example.com/.pods \
|
|
393
|
+
-H "Content-Type: application/json" \
|
|
394
|
+
-d '{"name": "alice"}'
|
|
395
|
+
```
|
|
396
|
+
|
|
341
397
|
## Configuration
|
|
342
398
|
|
|
343
399
|
```javascript
|
|
344
400
|
createServer({
|
|
345
401
|
logger: true, // Enable Fastify logging (default: true)
|
|
346
402
|
conneg: false, // Enable content negotiation (default: false)
|
|
347
|
-
notifications: false // Enable WebSocket notifications (default: false)
|
|
403
|
+
notifications: false, // Enable WebSocket notifications (default: false)
|
|
404
|
+
subdomains: false, // Enable subdomain-based pods (default: false)
|
|
405
|
+
baseDomain: null, // Base domain for subdomains (e.g., "example.com")
|
|
348
406
|
});
|
|
349
407
|
```
|
|
350
408
|
|
|
@@ -379,6 +437,37 @@ npm test
|
|
|
379
437
|
|
|
380
438
|
Currently passing: **182 tests** (including 27 conformance tests)
|
|
381
439
|
|
|
440
|
+
### Conformance Test Harness (CTH)
|
|
441
|
+
|
|
442
|
+
This server passes the Solid Conformance Test Harness authentication tests:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
# Start server with IdP and content negotiation
|
|
446
|
+
JSS_PORT=4000 JSS_CONNEG=true JSS_IDP=true jss start
|
|
447
|
+
|
|
448
|
+
# Create test users
|
|
449
|
+
curl -X POST http://localhost:4000/.pods \
|
|
450
|
+
-H "Content-Type: application/json" \
|
|
451
|
+
-d '{"name": "alice", "email": "alice@example.com", "password": "alicepassword123"}'
|
|
452
|
+
|
|
453
|
+
curl -X POST http://localhost:4000/.pods \
|
|
454
|
+
-H "Content-Type: application/json" \
|
|
455
|
+
-d '{"name": "bob", "email": "bob@example.com", "password": "bobpassword123"}'
|
|
456
|
+
|
|
457
|
+
# Run CTH authentication tests
|
|
458
|
+
docker run --rm --network=host \
|
|
459
|
+
-e SOLID_IDENTITY_PROVIDER="http://localhost:4000/" \
|
|
460
|
+
-e USERS_ALICE_WEBID="http://localhost:4000/alice/#me" \
|
|
461
|
+
-e USERS_ALICE_PASSWORD="alicepassword123" \
|
|
462
|
+
-e USERS_BOB_WEBID="http://localhost:4000/bob/#me" \
|
|
463
|
+
-e USERS_BOB_PASSWORD="bobpassword123" \
|
|
464
|
+
solidproject/conformance-test-harness:latest \
|
|
465
|
+
--filter="authentication"
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**CTH Status (v0.0.15):**
|
|
469
|
+
- Authentication tests: 6/6 passing
|
|
470
|
+
|
|
382
471
|
## Project Structure
|
|
383
472
|
|
|
384
473
|
```
|
package/bin/jss.js
CHANGED
|
@@ -47,6 +47,9 @@ program
|
|
|
47
47
|
.option('--idp', 'Enable built-in Identity Provider')
|
|
48
48
|
.option('--no-idp', 'Disable built-in Identity Provider')
|
|
49
49
|
.option('--idp-issuer <url>', 'IdP issuer URL (defaults to server URL)')
|
|
50
|
+
.option('--subdomains', 'Enable subdomain-based pods (XSS protection)')
|
|
51
|
+
.option('--no-subdomains', 'Disable subdomain-based pods')
|
|
52
|
+
.option('--base-domain <domain>', 'Base domain for subdomain pods (e.g., "example.com")')
|
|
50
53
|
.option('-q, --quiet', 'Suppress log output')
|
|
51
54
|
.option('--print-config', 'Print configuration and exit')
|
|
52
55
|
.action(async (options) => {
|
|
@@ -62,7 +65,11 @@ program
|
|
|
62
65
|
const protocol = config.ssl ? 'https' : 'http';
|
|
63
66
|
const serverHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
|
|
64
67
|
const baseUrl = `${protocol}://${serverHost}:${config.port}`;
|
|
65
|
-
|
|
68
|
+
// Ensure issuer has trailing slash for CTH compatibility
|
|
69
|
+
let idpIssuer = config.idpIssuer || baseUrl;
|
|
70
|
+
if (idpIssuer && !idpIssuer.endsWith('/')) {
|
|
71
|
+
idpIssuer = idpIssuer + '/';
|
|
72
|
+
}
|
|
66
73
|
|
|
67
74
|
// Create and start server
|
|
68
75
|
const server = createServer({
|
|
@@ -76,6 +83,8 @@ program
|
|
|
76
83
|
cert: await fs.readFile(config.sslCert),
|
|
77
84
|
} : null,
|
|
78
85
|
root: config.root,
|
|
86
|
+
subdomains: config.subdomains,
|
|
87
|
+
baseDomain: config.baseDomain,
|
|
79
88
|
});
|
|
80
89
|
|
|
81
90
|
await server.listen({ port: config.port, host: config.host });
|
|
@@ -88,6 +97,7 @@ program
|
|
|
88
97
|
if (config.conneg) console.log(' Conneg: enabled');
|
|
89
98
|
if (config.notifications) console.log(' WebSocket: enabled');
|
|
90
99
|
if (config.idp) console.log(` IdP: ${idpIssuer}`);
|
|
100
|
+
if (config.subdomains) console.log(` Subdomains: ${config.baseDomain} (XSS protection enabled)`);
|
|
91
101
|
console.log('\n Press Ctrl+C to stop\n');
|
|
92
102
|
}
|
|
93
103
|
|
|
@@ -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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { getWebIdFromRequestAsync } from './token.js';
|
|
8
8
|
import { checkAccess, getRequiredMode } from '../wac/checker.js';
|
|
9
9
|
import * as storage from '../storage/filesystem.js';
|
|
10
|
+
import { getEffectiveUrlPath } from '../utils/url.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Check if request is authorized
|
|
@@ -27,27 +28,32 @@ export async function authorize(request, reply) {
|
|
|
27
28
|
// Get WebID from token (supports both simple and Solid-OIDC tokens)
|
|
28
29
|
const { webId, error: authError } = await getWebIdFromRequestAsync(request);
|
|
29
30
|
|
|
31
|
+
// Get effective storage path (includes pod name in subdomain mode)
|
|
32
|
+
const storagePath = getEffectiveUrlPath(request);
|
|
33
|
+
|
|
30
34
|
// Get resource info
|
|
31
|
-
const stats = await storage.stat(
|
|
35
|
+
const stats = await storage.stat(storagePath);
|
|
32
36
|
const resourceExists = stats !== null;
|
|
33
37
|
const isContainer = stats?.isDirectory || urlPath.endsWith('/');
|
|
34
38
|
|
|
35
|
-
// Build resource URL
|
|
39
|
+
// Build resource URL (uses actual request hostname which may be subdomain)
|
|
36
40
|
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
37
41
|
|
|
38
42
|
// Get required access mode for this method
|
|
39
43
|
const requiredMode = getRequiredMode(method);
|
|
40
44
|
|
|
41
45
|
// For write operations on non-existent resources, check parent container
|
|
42
|
-
let checkPath =
|
|
46
|
+
let checkPath = storagePath;
|
|
43
47
|
let checkUrl = resourceUrl;
|
|
44
48
|
let checkIsContainer = isContainer;
|
|
45
49
|
|
|
46
50
|
if (!resourceExists && (method === 'PUT' || method === 'POST' || method === 'PATCH')) {
|
|
47
51
|
// Check write permission on parent container
|
|
48
|
-
const parentPath = getParentPath(
|
|
52
|
+
const parentPath = getParentPath(storagePath);
|
|
49
53
|
checkPath = parentPath;
|
|
50
|
-
|
|
54
|
+
// For URL, also need to get parent
|
|
55
|
+
const parentUrlPath = getParentPath(urlPath);
|
|
56
|
+
checkUrl = `${request.protocol}://${request.hostname}${parentUrlPath}`;
|
|
51
57
|
checkIsContainer = true;
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -79,12 +85,16 @@ function getParentPath(path) {
|
|
|
79
85
|
* @param {boolean} isAuthenticated - Whether user is authenticated
|
|
80
86
|
* @param {string} wacAllow - WAC-Allow header value
|
|
81
87
|
* @param {string|null} authError - Authentication error message (for DPoP failures)
|
|
88
|
+
* @param {string|null} issuer - IdP issuer URL for WWW-Authenticate header
|
|
82
89
|
*/
|
|
83
|
-
export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null) {
|
|
90
|
+
export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError = null, issuer = null) {
|
|
84
91
|
reply.header('WAC-Allow', wacAllow);
|
|
85
92
|
|
|
86
93
|
if (!isAuthenticated) {
|
|
87
|
-
// Not authenticated - return 401
|
|
94
|
+
// Not authenticated - return 401 with WWW-Authenticate header
|
|
95
|
+
// Solid-OIDC requires DPoP authentication
|
|
96
|
+
const realm = issuer || 'Solid';
|
|
97
|
+
reply.header('WWW-Authenticate', `DPoP realm="${realm}", Bearer realm="${realm}"`);
|
|
88
98
|
return reply.code(401).send({
|
|
89
99
|
error: 'Unauthorized',
|
|
90
100
|
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
|
package/src/config.js
CHANGED
|
@@ -33,6 +33,10 @@ export const defaults = {
|
|
|
33
33
|
idp: false,
|
|
34
34
|
idpIssuer: null,
|
|
35
35
|
|
|
36
|
+
// Subdomain mode (XSS protection)
|
|
37
|
+
subdomains: false,
|
|
38
|
+
baseDomain: null,
|
|
39
|
+
|
|
36
40
|
// Logging
|
|
37
41
|
logger: true,
|
|
38
42
|
quiet: false,
|
|
@@ -57,6 +61,8 @@ const envMap = {
|
|
|
57
61
|
JSS_CONFIG_PATH: 'configPath',
|
|
58
62
|
JSS_IDP: 'idp',
|
|
59
63
|
JSS_IDP_ISSUER: 'idpIssuer',
|
|
64
|
+
JSS_SUBDOMAINS: 'subdomains',
|
|
65
|
+
JSS_BASE_DOMAIN: 'baseDomain',
|
|
60
66
|
};
|
|
61
67
|
|
|
62
68
|
/**
|
|
@@ -188,5 +194,6 @@ export function printConfig(config) {
|
|
|
188
194
|
console.log(` Conneg: ${config.conneg}`);
|
|
189
195
|
console.log(` Notifications: ${config.notifications}`);
|
|
190
196
|
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
|
|
197
|
+
console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
|
|
191
198
|
console.log('─'.repeat(40));
|
|
192
199
|
}
|