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.
@@ -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
+ }
@@ -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, '&amp;')
291
+ .replace(/</g, '&lt;')
292
+ .replace(/>/g, '&gt;')
293
+ .replace(/"/g, '&quot;')
294
+ .replace(/'/g, '&#039;');
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 OPTIONS
82
- if (request.url === '/.pods' || request.method === 'OPTIONS') {
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