javascript-solid-server 0.0.11 → 0.0.13
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 +7 -1
- package/README.md +68 -4
- package/bin/jss.js +22 -4
- package/data/alice/.acl +50 -0
- package/data/alice/inbox/.acl +50 -0
- package/data/alice/index.html +80 -0
- package/data/alice/private/.acl +32 -0
- package/data/alice/public/test.json +1 -0
- package/data/alice/settings/.acl +32 -0
- package/data/alice/settings/prefs +17 -0
- package/data/alice/settings/privateTypeIndex +7 -0
- package/data/alice/settings/publicTypeIndex +7 -0
- package/data/bob/.acl +50 -0
- package/data/bob/inbox/.acl +50 -0
- package/data/bob/index.html +80 -0
- package/data/bob/private/.acl +32 -0
- package/data/bob/settings/.acl +32 -0
- package/data/bob/settings/prefs +17 -0
- package/data/bob/settings/privateTypeIndex +7 -0
- package/data/bob/settings/publicTypeIndex +7 -0
- package/package.json +6 -2
- package/scripts/test-cth-compat.js +369 -0
- package/src/config.js +7 -0
- package/src/handlers/container.js +35 -2
- package/src/idp/accounts.js +258 -0
- package/src/idp/adapter.js +204 -0
- package/src/idp/credentials.js +225 -0
- package/src/idp/index.js +135 -0
- package/src/idp/interactions.js +180 -0
- package/src/idp/keys.js +157 -0
- package/src/idp/provider.js +246 -0
- package/src/idp/views.js +295 -0
- package/src/server.js +18 -2
- package/test/idp.test.js +427 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oidc-provider configuration for Solid-OIDC
|
|
3
|
+
* Configures the OpenID Connect provider with DPoP support and webid claim
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Provider from 'oidc-provider';
|
|
7
|
+
import { createAdapter } from './adapter.js';
|
|
8
|
+
import { getJwks, getCookieKeys } from './keys.js';
|
|
9
|
+
import { getAccountForProvider } from './accounts.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create and configure the OIDC provider
|
|
13
|
+
* @param {string} issuer - The issuer URL (e.g., 'https://example.com')
|
|
14
|
+
* @returns {Promise<Provider>} - Configured oidc-provider instance
|
|
15
|
+
*/
|
|
16
|
+
export async function createProvider(issuer) {
|
|
17
|
+
const jwks = await getJwks();
|
|
18
|
+
const cookieKeys = await getCookieKeys();
|
|
19
|
+
|
|
20
|
+
const configuration = {
|
|
21
|
+
// Use our filesystem adapter
|
|
22
|
+
adapter: createAdapter,
|
|
23
|
+
|
|
24
|
+
// Signing keys
|
|
25
|
+
jwks,
|
|
26
|
+
|
|
27
|
+
// Cookie configuration
|
|
28
|
+
cookies: {
|
|
29
|
+
keys: cookieKeys,
|
|
30
|
+
long: {
|
|
31
|
+
signed: true,
|
|
32
|
+
maxAge: 14 * 24 * 60 * 60 * 1000, // 14 days
|
|
33
|
+
httpOnly: true,
|
|
34
|
+
sameSite: 'lax',
|
|
35
|
+
},
|
|
36
|
+
short: {
|
|
37
|
+
signed: true,
|
|
38
|
+
httpOnly: true,
|
|
39
|
+
sameSite: 'lax',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Token TTLs
|
|
44
|
+
ttl: {
|
|
45
|
+
AccessToken: 3600, // 1 hour
|
|
46
|
+
AuthorizationCode: 600, // 10 minutes
|
|
47
|
+
IdToken: 3600, // 1 hour
|
|
48
|
+
RefreshToken: 14 * 24 * 3600, // 14 days
|
|
49
|
+
Interaction: 3600, // 1 hour
|
|
50
|
+
Session: 14 * 24 * 3600, // 14 days
|
|
51
|
+
Grant: 14 * 24 * 3600, // 14 days
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Features - configure for Solid-OIDC
|
|
55
|
+
features: {
|
|
56
|
+
// Disable dev interactions - we provide our own
|
|
57
|
+
devInteractions: {
|
|
58
|
+
enabled: false,
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// DPoP is REQUIRED for Solid-OIDC
|
|
62
|
+
dPoP: {
|
|
63
|
+
enabled: true,
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Dynamic client registration (Solid apps need this)
|
|
67
|
+
registration: {
|
|
68
|
+
enabled: true,
|
|
69
|
+
idFactory: () => {
|
|
70
|
+
// Generate random client ID
|
|
71
|
+
return `client_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
72
|
+
},
|
|
73
|
+
initialAccessToken: false, // Allow public registration
|
|
74
|
+
policies: undefined, // No restrictions
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Client credentials for machine-to-machine
|
|
78
|
+
clientCredentials: {
|
|
79
|
+
enabled: true,
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Token introspection for resource servers
|
|
83
|
+
introspection: {
|
|
84
|
+
enabled: true,
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Token revocation
|
|
88
|
+
revocation: {
|
|
89
|
+
enabled: true,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Device flow (optional, but useful for CLI apps)
|
|
93
|
+
deviceFlow: {
|
|
94
|
+
enabled: false, // Keep disabled for MVP
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Allow resource parameter
|
|
98
|
+
resourceIndicators: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
defaultResource: () => undefined,
|
|
101
|
+
getResourceServerInfo: () => ({
|
|
102
|
+
scope: 'openid webid profile email offline_access',
|
|
103
|
+
accessTokenFormat: 'jwt',
|
|
104
|
+
}),
|
|
105
|
+
useGrantedResource: () => true,
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// userinfo endpoint
|
|
109
|
+
userinfo: {
|
|
110
|
+
enabled: true,
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// Allow backchannel logout
|
|
114
|
+
backchannelLogout: {
|
|
115
|
+
enabled: false,
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// RP-initiated logout
|
|
119
|
+
rpInitiatedLogout: {
|
|
120
|
+
enabled: true,
|
|
121
|
+
postLogoutSuccessSource: async (ctx) => {
|
|
122
|
+
ctx.body = `
|
|
123
|
+
<!DOCTYPE html>
|
|
124
|
+
<html>
|
|
125
|
+
<head><title>Logged Out</title></head>
|
|
126
|
+
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
127
|
+
<h1>You have been logged out</h1>
|
|
128
|
+
<p>You can close this window.</p>
|
|
129
|
+
</body>
|
|
130
|
+
</html>
|
|
131
|
+
`;
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Token format - JWT for Solid-OIDC
|
|
137
|
+
formats: {
|
|
138
|
+
AccessToken: 'jwt',
|
|
139
|
+
ClientCredentials: 'jwt',
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Scopes supported
|
|
143
|
+
scopes: ['openid', 'webid', 'profile', 'email', 'offline_access'],
|
|
144
|
+
|
|
145
|
+
// Claims configuration
|
|
146
|
+
claims: {
|
|
147
|
+
openid: ['sub'],
|
|
148
|
+
webid: ['webid'],
|
|
149
|
+
profile: ['name'],
|
|
150
|
+
email: ['email', 'email_verified'],
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Find account by ID (for token generation)
|
|
154
|
+
findAccount: async (ctx, id) => {
|
|
155
|
+
return getAccountForProvider(id);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Extra access token claims for Solid-OIDC
|
|
159
|
+
extraTokenClaims: async (ctx, token) => {
|
|
160
|
+
if (token.accountId) {
|
|
161
|
+
const account = await getAccountForProvider(token.accountId);
|
|
162
|
+
if (account) {
|
|
163
|
+
const claims = await account.claims('access_token', token.scopes, {}, []);
|
|
164
|
+
return {
|
|
165
|
+
webid: claims.webid,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {};
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Interaction URL for login/consent
|
|
173
|
+
interactions: {
|
|
174
|
+
url: (ctx, interaction) => {
|
|
175
|
+
return `/idp/interaction/${interaction.uid}`;
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Enable refresh token rotation
|
|
180
|
+
rotateRefreshToken: (ctx) => {
|
|
181
|
+
return true;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// Client defaults
|
|
185
|
+
clientDefaults: {
|
|
186
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
187
|
+
response_types: ['code'],
|
|
188
|
+
token_endpoint_auth_method: 'none', // Public clients by default
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// Response modes
|
|
192
|
+
responseModes: ['query', 'fragment', 'form_post'],
|
|
193
|
+
|
|
194
|
+
// Subject types
|
|
195
|
+
subjectTypes: ['public'],
|
|
196
|
+
|
|
197
|
+
// PKCE methods - require PKCE for public clients
|
|
198
|
+
pkceMethods: ['S256'],
|
|
199
|
+
pkce: {
|
|
200
|
+
required: () => true,
|
|
201
|
+
methods: ['S256'],
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// Enable request parameter
|
|
205
|
+
requestObjects: {
|
|
206
|
+
request: false,
|
|
207
|
+
requestUri: false,
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
// Clock tolerance for token validation
|
|
211
|
+
clockTolerance: 60, // 60 seconds
|
|
212
|
+
|
|
213
|
+
// Render errors
|
|
214
|
+
renderError: async (ctx, out, error) => {
|
|
215
|
+
ctx.type = 'html';
|
|
216
|
+
ctx.body = `
|
|
217
|
+
<!DOCTYPE html>
|
|
218
|
+
<html>
|
|
219
|
+
<head>
|
|
220
|
+
<title>Error</title>
|
|
221
|
+
<style>
|
|
222
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 40px; max-width: 600px; margin: 0 auto; }
|
|
223
|
+
.error { background: #fee; border: 1px solid #fcc; padding: 20px; border-radius: 8px; }
|
|
224
|
+
h1 { color: #c00; margin-top: 0; }
|
|
225
|
+
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
|
|
226
|
+
</style>
|
|
227
|
+
</head>
|
|
228
|
+
<body>
|
|
229
|
+
<div class="error">
|
|
230
|
+
<h1>Authentication Error</h1>
|
|
231
|
+
<p><strong>${out.error}</strong></p>
|
|
232
|
+
<p>${out.error_description || ''}</p>
|
|
233
|
+
</div>
|
|
234
|
+
</body>
|
|
235
|
+
</html>
|
|
236
|
+
`;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const provider = new Provider(issuer, configuration);
|
|
241
|
+
|
|
242
|
+
// Allow localhost for development
|
|
243
|
+
provider.proxy = true;
|
|
244
|
+
|
|
245
|
+
return provider;
|
|
246
|
+
}
|
package/src/idp/views.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML templates for IdP login/consent pages
|
|
3
|
+
* Minimal, functional design
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const styles = `
|
|
7
|
+
* { box-sizing: border-box; }
|
|
8
|
+
body {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
background: #f5f5f5;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 40px 20px;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
}
|
|
15
|
+
.container {
|
|
16
|
+
max-width: 400px;
|
|
17
|
+
margin: 0 auto;
|
|
18
|
+
background: white;
|
|
19
|
+
border-radius: 12px;
|
|
20
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
21
|
+
padding: 40px;
|
|
22
|
+
}
|
|
23
|
+
h1 {
|
|
24
|
+
margin: 0 0 8px 0;
|
|
25
|
+
font-size: 24px;
|
|
26
|
+
color: #333;
|
|
27
|
+
}
|
|
28
|
+
.subtitle {
|
|
29
|
+
color: #666;
|
|
30
|
+
margin: 0 0 30px 0;
|
|
31
|
+
font-size: 14px;
|
|
32
|
+
}
|
|
33
|
+
.client-info {
|
|
34
|
+
background: #f8f9fa;
|
|
35
|
+
border-radius: 8px;
|
|
36
|
+
padding: 16px;
|
|
37
|
+
margin-bottom: 24px;
|
|
38
|
+
}
|
|
39
|
+
.client-name {
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
color: #333;
|
|
42
|
+
}
|
|
43
|
+
.client-uri {
|
|
44
|
+
font-size: 12px;
|
|
45
|
+
color: #666;
|
|
46
|
+
word-break: break-all;
|
|
47
|
+
}
|
|
48
|
+
label {
|
|
49
|
+
display: block;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
font-weight: 500;
|
|
52
|
+
color: #333;
|
|
53
|
+
margin-bottom: 6px;
|
|
54
|
+
}
|
|
55
|
+
input[type="email"],
|
|
56
|
+
input[type="password"] {
|
|
57
|
+
width: 100%;
|
|
58
|
+
padding: 12px;
|
|
59
|
+
border: 1px solid #ddd;
|
|
60
|
+
border-radius: 8px;
|
|
61
|
+
font-size: 16px;
|
|
62
|
+
margin-bottom: 16px;
|
|
63
|
+
transition: border-color 0.2s;
|
|
64
|
+
}
|
|
65
|
+
input:focus {
|
|
66
|
+
outline: none;
|
|
67
|
+
border-color: #0066cc;
|
|
68
|
+
}
|
|
69
|
+
.error {
|
|
70
|
+
background: #fee;
|
|
71
|
+
border: 1px solid #fcc;
|
|
72
|
+
color: #c00;
|
|
73
|
+
padding: 12px;
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
margin-bottom: 20px;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
}
|
|
78
|
+
.btn {
|
|
79
|
+
display: inline-block;
|
|
80
|
+
padding: 12px 24px;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
font-size: 16px;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
border: none;
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
text-align: center;
|
|
88
|
+
transition: background-color 0.2s;
|
|
89
|
+
}
|
|
90
|
+
.btn-primary {
|
|
91
|
+
background: #0066cc;
|
|
92
|
+
color: white;
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
.btn-primary:hover {
|
|
96
|
+
background: #0052a3;
|
|
97
|
+
}
|
|
98
|
+
.btn-secondary {
|
|
99
|
+
background: #f0f0f0;
|
|
100
|
+
color: #333;
|
|
101
|
+
margin-top: 12px;
|
|
102
|
+
width: 100%;
|
|
103
|
+
}
|
|
104
|
+
.btn-secondary:hover {
|
|
105
|
+
background: #e0e0e0;
|
|
106
|
+
}
|
|
107
|
+
.scopes {
|
|
108
|
+
margin: 20px 0;
|
|
109
|
+
}
|
|
110
|
+
.scope {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
padding: 12px;
|
|
114
|
+
background: #f8f9fa;
|
|
115
|
+
border-radius: 8px;
|
|
116
|
+
margin-bottom: 8px;
|
|
117
|
+
}
|
|
118
|
+
.scope-icon {
|
|
119
|
+
width: 24px;
|
|
120
|
+
height: 24px;
|
|
121
|
+
margin-right: 12px;
|
|
122
|
+
opacity: 0.6;
|
|
123
|
+
}
|
|
124
|
+
.scope-name {
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
}
|
|
127
|
+
.scope-desc {
|
|
128
|
+
font-size: 12px;
|
|
129
|
+
color: #666;
|
|
130
|
+
}
|
|
131
|
+
.actions {
|
|
132
|
+
margin-top: 24px;
|
|
133
|
+
}
|
|
134
|
+
.logo {
|
|
135
|
+
text-align: center;
|
|
136
|
+
margin-bottom: 24px;
|
|
137
|
+
}
|
|
138
|
+
.logo svg {
|
|
139
|
+
width: 48px;
|
|
140
|
+
height: 48px;
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const solidLogo = `
|
|
145
|
+
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
146
|
+
<circle cx="50" cy="50" r="45" fill="#7C4DFF" />
|
|
147
|
+
<path d="M30 50 L45 65 L70 40" stroke="white" stroke-width="8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
148
|
+
</svg>
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const scopeDescriptions = {
|
|
152
|
+
openid: 'Access your identity',
|
|
153
|
+
webid: 'Access your WebID',
|
|
154
|
+
profile: 'Access your name',
|
|
155
|
+
email: 'Access your email address',
|
|
156
|
+
offline_access: 'Stay logged in',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Login page HTML
|
|
161
|
+
*/
|
|
162
|
+
export function loginPage(uid, clientId, error = null) {
|
|
163
|
+
return `
|
|
164
|
+
<!DOCTYPE html>
|
|
165
|
+
<html lang="en">
|
|
166
|
+
<head>
|
|
167
|
+
<meta charset="UTF-8">
|
|
168
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
169
|
+
<title>Sign In - Solid IdP</title>
|
|
170
|
+
<style>${styles}</style>
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<div class="container">
|
|
174
|
+
<div class="logo">${solidLogo}</div>
|
|
175
|
+
<h1>Sign In</h1>
|
|
176
|
+
<p class="subtitle">Sign in to your Solid Pod</p>
|
|
177
|
+
|
|
178
|
+
${error ? `<div class="error">${escapeHtml(error)}</div>` : ''}
|
|
179
|
+
|
|
180
|
+
<form method="POST" action="/idp/interaction/${uid}/login">
|
|
181
|
+
<label for="email">Email</label>
|
|
182
|
+
<input type="email" id="email" name="email" required autofocus placeholder="you@example.com">
|
|
183
|
+
|
|
184
|
+
<label for="password">Password</label>
|
|
185
|
+
<input type="password" id="password" name="password" required placeholder="Your password">
|
|
186
|
+
|
|
187
|
+
<button type="submit" class="btn btn-primary">Sign In</button>
|
|
188
|
+
</form>
|
|
189
|
+
|
|
190
|
+
<form method="POST" action="/idp/interaction/${uid}/abort">
|
|
191
|
+
<button type="submit" class="btn btn-secondary">Cancel</button>
|
|
192
|
+
</form>
|
|
193
|
+
</div>
|
|
194
|
+
</body>
|
|
195
|
+
</html>
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Consent page HTML
|
|
201
|
+
*/
|
|
202
|
+
export function consentPage(uid, client, params, account) {
|
|
203
|
+
const scopes = (params.scope || 'openid').split(' ').filter(Boolean);
|
|
204
|
+
const clientName = client?.clientName || client?.client_id || 'Unknown App';
|
|
205
|
+
const clientUri = client?.clientUri || client?.redirect_uris?.[0] || '';
|
|
206
|
+
|
|
207
|
+
const scopeItems = scopes.map(scope => `
|
|
208
|
+
<div class="scope">
|
|
209
|
+
<div class="scope-icon">✓</div>
|
|
210
|
+
<div>
|
|
211
|
+
<div class="scope-name">${escapeHtml(scope)}</div>
|
|
212
|
+
<div class="scope-desc">${escapeHtml(scopeDescriptions[scope] || 'Access requested')}</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
`).join('');
|
|
216
|
+
|
|
217
|
+
return `
|
|
218
|
+
<!DOCTYPE html>
|
|
219
|
+
<html lang="en">
|
|
220
|
+
<head>
|
|
221
|
+
<meta charset="UTF-8">
|
|
222
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
223
|
+
<title>Authorize - Solid IdP</title>
|
|
224
|
+
<style>${styles}</style>
|
|
225
|
+
</head>
|
|
226
|
+
<body>
|
|
227
|
+
<div class="container">
|
|
228
|
+
<div class="logo">${solidLogo}</div>
|
|
229
|
+
<h1>Authorize Access</h1>
|
|
230
|
+
<p class="subtitle">Allow this app to access your data?</p>
|
|
231
|
+
|
|
232
|
+
<div class="client-info">
|
|
233
|
+
<div class="client-name">${escapeHtml(clientName)}</div>
|
|
234
|
+
${clientUri ? `<div class="client-uri">${escapeHtml(clientUri)}</div>` : ''}
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
${account ? `<p>Signed in as <strong>${escapeHtml(account.email)}</strong></p>` : ''}
|
|
238
|
+
|
|
239
|
+
<div class="scopes">
|
|
240
|
+
<label>This app is requesting access to:</label>
|
|
241
|
+
${scopeItems}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div class="actions">
|
|
245
|
+
<form method="POST" action="/idp/interaction/${uid}/confirm">
|
|
246
|
+
<button type="submit" class="btn btn-primary">Allow Access</button>
|
|
247
|
+
</form>
|
|
248
|
+
|
|
249
|
+
<form method="POST" action="/idp/interaction/${uid}/abort">
|
|
250
|
+
<button type="submit" class="btn btn-secondary">Deny</button>
|
|
251
|
+
</form>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</body>
|
|
255
|
+
</html>
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Error page HTML
|
|
261
|
+
*/
|
|
262
|
+
export function errorPage(title, message) {
|
|
263
|
+
return `
|
|
264
|
+
<!DOCTYPE html>
|
|
265
|
+
<html lang="en">
|
|
266
|
+
<head>
|
|
267
|
+
<meta charset="UTF-8">
|
|
268
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
269
|
+
<title>Error - Solid IdP</title>
|
|
270
|
+
<style>${styles}</style>
|
|
271
|
+
</head>
|
|
272
|
+
<body>
|
|
273
|
+
<div class="container">
|
|
274
|
+
<div class="logo">${solidLogo}</div>
|
|
275
|
+
<h1 style="color: #c00;">${escapeHtml(title)}</h1>
|
|
276
|
+
<p>${escapeHtml(message)}</p>
|
|
277
|
+
<a href="/" class="btn btn-secondary">Go Home</a>
|
|
278
|
+
</div>
|
|
279
|
+
</body>
|
|
280
|
+
</html>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Escape HTML to prevent XSS
|
|
286
|
+
*/
|
|
287
|
+
function escapeHtml(text) {
|
|
288
|
+
if (!text) return '';
|
|
289
|
+
return String(text)
|
|
290
|
+
.replace(/&/g, '&')
|
|
291
|
+
.replace(/</g, '<')
|
|
292
|
+
.replace(/>/g, '>')
|
|
293
|
+
.replace(/"/g, '"')
|
|
294
|
+
.replace(/'/g, ''');
|
|
295
|
+
}
|
package/src/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { handlePost, handleCreatePod } from './handlers/container.js';
|
|
|
4
4
|
import { getCorsHeaders } from './ldp/headers.js';
|
|
5
5
|
import { authorize, handleUnauthorized } from './auth/middleware.js';
|
|
6
6
|
import { notificationsPlugin } from './notifications/index.js';
|
|
7
|
+
import { idpPlugin } from './idp/index.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Create and configure Fastify server
|
|
@@ -11,6 +12,8 @@ import { notificationsPlugin } from './notifications/index.js';
|
|
|
11
12
|
* @param {boolean} options.logger - Enable logging (default true)
|
|
12
13
|
* @param {boolean} options.conneg - Enable content negotiation for RDF (default false)
|
|
13
14
|
* @param {boolean} options.notifications - Enable WebSocket notifications (default false)
|
|
15
|
+
* @param {boolean} options.idp - Enable built-in Identity Provider (default false)
|
|
16
|
+
* @param {string} options.idpIssuer - IdP issuer URL (default: server URL)
|
|
14
17
|
* @param {object} options.ssl - SSL configuration { key, cert } (default null)
|
|
15
18
|
* @param {string} options.root - Data directory path (default from env or ./data)
|
|
16
19
|
*/
|
|
@@ -19,6 +22,9 @@ export function createServer(options = {}) {
|
|
|
19
22
|
const connegEnabled = options.conneg ?? false;
|
|
20
23
|
// WebSocket notifications are OFF by default
|
|
21
24
|
const notificationsEnabled = options.notifications ?? false;
|
|
25
|
+
// Identity Provider is OFF by default
|
|
26
|
+
const idpEnabled = options.idp ?? false;
|
|
27
|
+
const idpIssuer = options.idpIssuer;
|
|
22
28
|
|
|
23
29
|
// Set data root via environment variable if provided
|
|
24
30
|
if (options.root) {
|
|
@@ -51,9 +57,11 @@ export function createServer(options = {}) {
|
|
|
51
57
|
// Attach server config to requests
|
|
52
58
|
fastify.decorateRequest('connegEnabled', null);
|
|
53
59
|
fastify.decorateRequest('notificationsEnabled', null);
|
|
60
|
+
fastify.decorateRequest('idpEnabled', null);
|
|
54
61
|
fastify.addHook('onRequest', async (request) => {
|
|
55
62
|
request.connegEnabled = connegEnabled;
|
|
56
63
|
request.notificationsEnabled = notificationsEnabled;
|
|
64
|
+
request.idpEnabled = idpEnabled;
|
|
57
65
|
});
|
|
58
66
|
|
|
59
67
|
// Register WebSocket notifications plugin if enabled
|
|
@@ -61,6 +69,11 @@ export function createServer(options = {}) {
|
|
|
61
69
|
fastify.register(notificationsPlugin);
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
// Register Identity Provider plugin if enabled
|
|
73
|
+
if (idpEnabled) {
|
|
74
|
+
fastify.register(idpPlugin, { issuer: idpIssuer });
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
// Global CORS preflight
|
|
65
78
|
fastify.addHook('onRequest', async (request, reply) => {
|
|
66
79
|
// Add CORS headers to all responses
|
|
@@ -78,8 +91,11 @@ export function createServer(options = {}) {
|
|
|
78
91
|
// Authorization hook - check WAC permissions
|
|
79
92
|
// Skip for pod creation endpoint (needs special handling)
|
|
80
93
|
fastify.addHook('preHandler', async (request, reply) => {
|
|
81
|
-
// Skip auth for pod creation and
|
|
82
|
-
if (request.url === '/.pods' ||
|
|
94
|
+
// Skip auth for pod creation, OPTIONS, IdP routes, and well-known endpoints
|
|
95
|
+
if (request.url === '/.pods' ||
|
|
96
|
+
request.method === 'OPTIONS' ||
|
|
97
|
+
request.url.startsWith('/idp/') ||
|
|
98
|
+
request.url.startsWith('/.well-known/')) {
|
|
83
99
|
return;
|
|
84
100
|
}
|
|
85
101
|
|