javascript-solid-server 0.0.35 → 0.0.37

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,283 @@
1
+ # Adding Git Support to a Solid Server
2
+
3
+ This guide explains how to add Git HTTP backend support to a Solid server, enabling `git clone` and `git push` operations on pod containers.
4
+
5
+ ## Overview
6
+
7
+ The Git HTTP protocol allows clients to clone and push to repositories over HTTP. This is implemented using Git's built-in `git http-backend` CGI program - the same one used by Apache and Nginx.
8
+
9
+ ### How It Works
10
+
11
+ ```
12
+ ┌─────────────┐ HTTP ┌──────────────┐ CGI ┌─────────────────┐
13
+ │ Git Client │ ─────────────▶│ Solid Server │ ────────────▶│ git http-backend│
14
+ │ │◀───────────── │ │◀──────────── │ │
15
+ └─────────────┘ └──────────────┘ └─────────────────┘
16
+ ```
17
+
18
+ **Clone flow:**
19
+ 1. `GET /repo/info/refs?service=git-upload-pack` - Discovery
20
+ 2. `POST /repo/git-upload-pack` - Fetch objects
21
+
22
+ **Push flow:**
23
+ 1. `GET /repo/info/refs?service=git-receive-pack` - Discovery
24
+ 2. `POST /repo/git-receive-pack` - Send objects
25
+
26
+ ## Implementation
27
+
28
+ ### 1. Detect Git Requests
29
+
30
+ Git protocol requests are identified by URL patterns:
31
+
32
+ ```javascript
33
+ function isGitRequest(urlPath) {
34
+ return urlPath.includes('/info/refs') ||
35
+ urlPath.includes('/git-upload-pack') ||
36
+ urlPath.includes('/git-receive-pack');
37
+ }
38
+
39
+ function isGitWriteOperation(urlPath) {
40
+ return urlPath.includes('/git-receive-pack');
41
+ }
42
+ ```
43
+
44
+ ### 2. Security: Block Direct .git Access
45
+
46
+ **Important:** Git protocol requests should be allowed, but direct file access to `.git/` contents must be blocked:
47
+
48
+ ```javascript
49
+ // BLOCK: Direct access to .git contents (security risk)
50
+ GET /.git/config → 403 Forbidden
51
+ GET /.git/objects/abc123 → 403 Forbidden
52
+
53
+ // ALLOW: Git protocol (handled by git http-backend)
54
+ GET /repo/info/refs?service=git-upload-pack → 200 OK
55
+ POST /repo/git-upload-pack → 200 OK
56
+ ```
57
+
58
+ ### 3. Authorization with WAC
59
+
60
+ Check permissions before allowing git operations:
61
+
62
+ ```javascript
63
+ // Clone/fetch requires Read access
64
+ // Push requires Write access
65
+
66
+ const needsWrite = isGitWriteOperation(request.url);
67
+ const requiredMode = needsWrite ? 'write' : 'read';
68
+
69
+ const { allowed } = await checkAccess({
70
+ resourceUrl,
71
+ resourcePath,
72
+ agentWebId: request.webId,
73
+ requiredMode
74
+ });
75
+
76
+ if (!allowed) {
77
+ return reply.code(needsWrite ? 403 : 401).send({
78
+ error: needsWrite ? 'Write access required' : 'Read access required'
79
+ });
80
+ }
81
+ ```
82
+
83
+ ### 4. Git HTTP Backend Handler
84
+
85
+ The core handler spawns `git http-backend` with CGI environment variables:
86
+
87
+ ```javascript
88
+ import { spawn } from 'child_process';
89
+
90
+ async function handleGit(request, reply) {
91
+ const urlPath = decodeURIComponent(request.url.split('?')[0]);
92
+ const queryString = request.url.split('?')[1] || '';
93
+
94
+ // Build CGI environment
95
+ const env = {
96
+ ...process.env,
97
+ GIT_PROJECT_ROOT: dataRoot, // Where repos are stored
98
+ GIT_HTTP_EXPORT_ALL: '', // Allow read access
99
+ GIT_HTTP_RECEIVE_PACK: 'true', // Enable push
100
+ PATH_INFO: urlPath,
101
+ REQUEST_METHOD: request.method,
102
+ CONTENT_TYPE: request.headers['content-type'] || '',
103
+ QUERY_STRING: queryString,
104
+ CONTENT_LENGTH: request.headers['content-length'] || '0',
105
+ };
106
+
107
+ // For non-bare repos, set GIT_DIR to .git subdirectory
108
+ if (isRegularRepo) {
109
+ env.GIT_DIR = path.join(repoPath, '.git');
110
+ }
111
+
112
+ // Spawn git http-backend
113
+ const child = spawn('git', ['http-backend'], { env });
114
+
115
+ // Send request body (for POST requests)
116
+ if (request.body && request.body.length > 0) {
117
+ child.stdin.write(request.body);
118
+ }
119
+ child.stdin.end();
120
+
121
+ // Parse CGI response and send to client
122
+ // ... (see full implementation below)
123
+ }
124
+ ```
125
+
126
+ ### 5. CGI Response Parsing
127
+
128
+ Git http-backend outputs CGI format (headers + body). Parse and forward:
129
+
130
+ ```javascript
131
+ let buffer = Buffer.alloc(0);
132
+ let headersSent = false;
133
+
134
+ child.stdout.on('data', (data) => {
135
+ buffer = Buffer.concat([buffer, data]);
136
+
137
+ if (!headersSent) {
138
+ // Find header/body separator (try both \r\n\r\n and \n\n)
139
+ let headerEnd = buffer.indexOf('\r\n\r\n');
140
+ let sep = '\r\n';
141
+ let sepLen = 4;
142
+
143
+ if (headerEnd === -1) {
144
+ headerEnd = buffer.indexOf('\n\n');
145
+ sep = '\n';
146
+ sepLen = 2;
147
+ }
148
+
149
+ if (headerEnd !== -1) {
150
+ const headerSection = buffer.subarray(0, headerEnd).toString();
151
+ const bodySection = buffer.subarray(headerEnd + sepLen);
152
+
153
+ // Parse CGI headers
154
+ for (const line of headerSection.split(sep)) {
155
+ const colonIdx = line.indexOf(':');
156
+ if (colonIdx > 0) {
157
+ const key = line.substring(0, colonIdx).trim();
158
+ const value = line.substring(colonIdx + 1).trim();
159
+
160
+ if (key.toLowerCase() === 'status') {
161
+ statusCode = parseInt(value.split(' ')[0], 10);
162
+ } else {
163
+ reply.raw.setHeader(key, value);
164
+ }
165
+ }
166
+ }
167
+
168
+ reply.raw.writeHead(statusCode);
169
+ reply.raw.write(bodySection);
170
+ headersSent = true;
171
+ }
172
+ } else {
173
+ reply.raw.write(buffer);
174
+ }
175
+ buffer = Buffer.alloc(0);
176
+ });
177
+
178
+ child.stdout.on('end', () => {
179
+ reply.raw.end();
180
+ });
181
+ ```
182
+
183
+ ## Repository Setup
184
+
185
+ ### Regular Repository (with working directory)
186
+
187
+ ```bash
188
+ cd /path/to/pod/myrepo
189
+ git init
190
+ echo "# My Project" > README.md
191
+ git add .
192
+ git commit -m "Initial commit"
193
+ ```
194
+
195
+ ### Bare Repository (server-only, more efficient)
196
+
197
+ ```bash
198
+ cd /path/to/pod
199
+ git init --bare myrepo.git
200
+ ```
201
+
202
+ ### ACL for Public Clone
203
+
204
+ Create `/path/to/pod/myrepo/.acl`:
205
+
206
+ ```turtle
207
+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
208
+ @prefix foaf: <http://xmlns.com/foaf/0.1/>.
209
+
210
+ <#public>
211
+ a acl:Authorization;
212
+ acl:agentClass foaf:Agent;
213
+ acl:accessTo <./>;
214
+ acl:default <./>;
215
+ acl:mode acl:Read.
216
+ ```
217
+
218
+ ### ACL for Authenticated Push
219
+
220
+ ```turtle
221
+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
222
+ @prefix foaf: <http://xmlns.com/foaf/0.1/>.
223
+
224
+ <#owner>
225
+ a acl:Authorization;
226
+ acl:agent <https://alice.example.com/#me>;
227
+ acl:accessTo <./>;
228
+ acl:default <./>;
229
+ acl:mode acl:Read, acl:Write, acl:Control.
230
+
231
+ <#public>
232
+ a acl:Authorization;
233
+ acl:agentClass foaf:Agent;
234
+ acl:accessTo <./>;
235
+ acl:default <./>;
236
+ acl:mode acl:Read.
237
+ ```
238
+
239
+ ## Usage
240
+
241
+ ### Server
242
+
243
+ ```bash
244
+ # Start server with git support enabled
245
+ jss start --git
246
+
247
+ # Or via environment variable
248
+ JSS_GIT=true jss start
249
+ ```
250
+
251
+ ### Client
252
+
253
+ ```bash
254
+ # Clone
255
+ git clone http://localhost:3000/myrepo
256
+
257
+ # Clone with authentication (if required)
258
+ git clone http://localhost:3000/myrepo
259
+ # Git will prompt for credentials
260
+
261
+ # Push (requires write access)
262
+ cd myrepo
263
+ echo "New content" >> README.md
264
+ git add .
265
+ git commit -m "Update readme"
266
+ git push
267
+ ```
268
+
269
+ ## Complete Handler Code
270
+
271
+ See `src/handlers/git.js` in the JSS repository for the full implementation.
272
+
273
+ ## References
274
+
275
+ - [Git HTTP Protocol](https://git-scm.com/book/en/v2/Git-on-the-Server-Smart-HTTP)
276
+ - [git-http-backend documentation](https://git-scm.com/docs/git-http-backend)
277
+ - [CGI Specification](https://www.rfc-editor.org/rfc/rfc3875)
278
+ - [Web Access Control (WAC)](https://solidproject.org/TR/wac)
279
+
280
+ ## Prior Art
281
+
282
+ - [nosdav/server](https://github.com/nosdav/server) - Git support implementation
283
+ - [QuitStore](https://github.com/AKSW/QuitStore) - Git + RDF versioning
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,207 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync, statSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ /**
6
+ * Check if a URL path is a Git protocol request
7
+ * @param {string} urlPath - The URL path
8
+ * @returns {boolean}
9
+ */
10
+ export function isGitRequest(urlPath) {
11
+ return urlPath.includes('/info/refs') ||
12
+ urlPath.includes('/git-upload-pack') ||
13
+ urlPath.includes('/git-receive-pack');
14
+ }
15
+
16
+ /**
17
+ * Determine if this is a write operation (push)
18
+ * @param {string} urlPath - The URL path
19
+ * @returns {boolean}
20
+ */
21
+ export function isGitWriteOperation(urlPath) {
22
+ return urlPath.includes('/git-receive-pack');
23
+ }
24
+
25
+ /**
26
+ * Extract the repository path from the URL
27
+ * @param {string} urlPath - The URL path
28
+ * @returns {string|null} The repository relative path or null
29
+ */
30
+ function extractRepoPath(urlPath) {
31
+ // Remove git service suffixes to get the repo path
32
+ const cleanPath = urlPath
33
+ .replace(/\/info\/refs.*$/, '')
34
+ .replace(/\/git-upload-pack$/, '')
35
+ .replace(/\/git-receive-pack$/, '');
36
+
37
+ // Remove leading slash
38
+ return cleanPath.replace(/^\//, '') || null;
39
+ }
40
+
41
+ /**
42
+ * Find the git directory for a path
43
+ * @param {string} repoPath - Absolute path to check
44
+ * @returns {{gitDir: string, isRegular: boolean}|null}
45
+ */
46
+ function findGitDir(repoPath) {
47
+ if (!existsSync(repoPath) || !statSync(repoPath).isDirectory()) {
48
+ return null;
49
+ }
50
+
51
+ // Check for regular repo with .git subdirectory
52
+ const dotGitPath = join(repoPath, '.git');
53
+ if (existsSync(dotGitPath) && statSync(dotGitPath).isDirectory()) {
54
+ return { gitDir: dotGitPath, isRegular: true };
55
+ }
56
+
57
+ // Check for bare repository
58
+ const objectsPath = join(repoPath, 'objects');
59
+ const refsPath = join(repoPath, 'refs');
60
+ if (existsSync(objectsPath) && existsSync(refsPath)) {
61
+ return { gitDir: repoPath, isRegular: false };
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * Handle Git HTTP requests using git http-backend
69
+ * @param {FastifyRequest} request
70
+ * @param {FastifyReply} reply
71
+ */
72
+ export async function handleGit(request, reply) {
73
+ const urlPath = decodeURIComponent(request.url.split('?')[0]);
74
+ const queryString = request.url.split('?')[1] || '';
75
+
76
+ // Extract repository path
77
+ const repoRelative = extractRepoPath(urlPath);
78
+ if (!repoRelative) {
79
+ return reply.code(400).send({ error: 'Invalid git request' });
80
+ }
81
+
82
+ // Handle subdomain mode
83
+ let dataRoot = process.env.DATA_ROOT || './data';
84
+ if (request.podName) {
85
+ dataRoot = join(dataRoot, request.podName);
86
+ }
87
+
88
+ const repoAbs = join(dataRoot, repoRelative);
89
+
90
+ // Find git directory
91
+ const gitInfo = findGitDir(repoAbs);
92
+ if (!gitInfo) {
93
+ return reply.code(404).send({ error: 'Not a git repository' });
94
+ }
95
+
96
+ // Build CGI environment
97
+ const env = {
98
+ ...process.env,
99
+ GIT_PROJECT_ROOT: dataRoot,
100
+ GIT_HTTP_EXPORT_ALL: '', // Allow read access
101
+ GIT_HTTP_RECEIVE_PACK: 'true', // Enable push
102
+ GIT_CONFIG_PARAMETERS: "'uploadpack.allowTipSHA1InWant=true'",
103
+ PATH_INFO: urlPath,
104
+ REQUEST_METHOD: request.method,
105
+ CONTENT_TYPE: request.headers['content-type'] || '',
106
+ QUERY_STRING: queryString,
107
+ REMOTE_USER: request.webId || '', // Pass authenticated user
108
+ CONTENT_LENGTH: request.headers['content-length'] || '0',
109
+ };
110
+
111
+ // For regular repositories, set GIT_DIR
112
+ if (gitInfo.isRegular) {
113
+ env.GIT_DIR = gitInfo.gitDir;
114
+ }
115
+
116
+ // Spawn git http-backend
117
+ return new Promise((resolve, reject) => {
118
+ const child = spawn('git', ['http-backend'], { env });
119
+
120
+ let buffer = Buffer.alloc(0);
121
+ let headersSent = false;
122
+
123
+ child.stdout.on('data', (data) => {
124
+ buffer = Buffer.concat([buffer, data]);
125
+
126
+ if (!headersSent) {
127
+ // Look for end of CGI headers (try both \r\n\r\n and \n\n)
128
+ let headerEnd = buffer.indexOf('\r\n\r\n');
129
+ let headerSep = '\r\n';
130
+ let headerEndLen = 4;
131
+
132
+ if (headerEnd === -1) {
133
+ headerEnd = buffer.indexOf('\n\n');
134
+ headerSep = '\n';
135
+ headerEndLen = 2;
136
+ }
137
+
138
+ if (headerEnd !== -1) {
139
+ const headerSection = buffer.subarray(0, headerEnd).toString();
140
+ const bodySection = buffer.subarray(headerEnd + headerEndLen);
141
+
142
+ // Parse CGI headers and set on raw response
143
+ const lines = headerSection.split(headerSep);
144
+ let statusCode = 200;
145
+
146
+ for (const line of lines) {
147
+ const colonIndex = line.indexOf(':');
148
+ if (colonIndex > 0) {
149
+ const key = line.substring(0, colonIndex).trim();
150
+ const value = line.substring(colonIndex + 1).trim();
151
+
152
+ // Handle Status header specially
153
+ if (key.toLowerCase() === 'status') {
154
+ statusCode = parseInt(value.split(' ')[0], 10);
155
+ } else {
156
+ reply.raw.setHeader(key, value);
157
+ }
158
+ }
159
+ }
160
+
161
+ reply.raw.writeHead(statusCode);
162
+ headersSent = true;
163
+ reply.raw.write(bodySection);
164
+ buffer = Buffer.alloc(0);
165
+ }
166
+ } else {
167
+ reply.raw.write(buffer);
168
+ buffer = Buffer.alloc(0);
169
+ }
170
+ });
171
+
172
+ child.stdout.on('end', () => {
173
+ reply.raw.end();
174
+ resolve();
175
+ });
176
+
177
+ // Send request body to git
178
+ // For POST requests, Fastify has already parsed the body into request.body
179
+ if (request.body && request.body.length > 0) {
180
+ child.stdin.write(request.body);
181
+ child.stdin.end();
182
+ } else {
183
+ // For GET requests or empty bodies, just close stdin
184
+ child.stdin.end();
185
+ }
186
+
187
+ // Log errors
188
+ child.stderr.on('data', (data) => {
189
+ console.error('git http-backend stderr:', data.toString());
190
+ });
191
+
192
+ child.on('error', (err) => {
193
+ console.error('Failed to spawn git http-backend:', err);
194
+ if (!headersSent) {
195
+ reply.code(500).send({ error: 'Git backend error' });
196
+ }
197
+ resolve();
198
+ });
199
+
200
+ child.on('close', (code) => {
201
+ if (code !== 0 && !headersSent) {
202
+ reply.code(500).send({ error: 'Git operation failed' });
203
+ }
204
+ resolve();
205
+ });
206
+ });
207
+ }
@@ -75,36 +75,57 @@ export async function handleGet(request, reply) {
75
75
  acceptHeader.includes('text/n3') ||
76
76
  acceptHeader.includes('application/n-triples')
77
77
  );
78
+ const wantsJsonLd = connegEnabled && (
79
+ acceptHeader.includes('application/ld+json') ||
80
+ acceptHeader.includes('application/json')
81
+ );
78
82
 
79
- if (wantsTurtle) {
80
- // Extract JSON-LD from HTML and convert to Turtle
83
+ if (wantsTurtle || wantsJsonLd) {
84
+ // Extract JSON-LD from HTML data island
81
85
  try {
82
86
  const htmlStr = content.toString();
83
- const jsonLdMatch = htmlStr.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/);
87
+ const jsonLdMatch = htmlStr.match(/<script type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/);
84
88
  if (jsonLdMatch) {
85
89
  const jsonLd = JSON.parse(jsonLdMatch[1]);
86
- const { content: turtleContent } = await fromJsonLd(
87
- jsonLd,
88
- 'text/turtle',
89
- resourceUrl,
90
- true
91
- );
92
-
93
- const headers = getAllHeaders({
94
- isContainer: true,
95
- etag: indexStats?.etag || stats.etag,
96
- contentType: 'text/turtle',
97
- origin,
98
- resourceUrl,
99
- connegEnabled
100
- });
101
90
 
102
- Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
103
- return reply.send(turtleContent);
91
+ if (wantsTurtle) {
92
+ // Convert to Turtle
93
+ const { content: turtleContent } = await fromJsonLd(
94
+ jsonLd,
95
+ 'text/turtle',
96
+ resourceUrl,
97
+ true
98
+ );
99
+
100
+ const headers = getAllHeaders({
101
+ isContainer: true,
102
+ etag: indexStats?.etag || stats.etag,
103
+ contentType: 'text/turtle',
104
+ origin,
105
+ resourceUrl,
106
+ connegEnabled
107
+ });
108
+
109
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
110
+ return reply.send(turtleContent);
111
+ } else {
112
+ // Return JSON-LD directly
113
+ const headers = getAllHeaders({
114
+ isContainer: true,
115
+ etag: indexStats?.etag || stats.etag,
116
+ contentType: 'application/ld+json',
117
+ origin,
118
+ resourceUrl,
119
+ connegEnabled
120
+ });
121
+
122
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
123
+ return reply.send(JSON.stringify(jsonLd, null, 2));
124
+ }
104
125
  }
105
126
  } catch (err) {
106
127
  // Fall through to serve HTML if conversion fails
107
- console.error('Failed to convert profile to Turtle:', err.message);
128
+ console.error('Failed to convert profile to RDF:', err.message);
108
129
  }
109
130
  }
110
131
 
@@ -329,14 +350,45 @@ export async function handleHead(request, reply) {
329
350
  }
330
351
 
331
352
  const origin = request.headers.origin;
332
- const contentType = stats.isDirectory ? 'application/ld+json' : getContentType(storagePath);
353
+ const connegEnabled = request.connegEnabled || false;
354
+ let contentType;
355
+
356
+ if (stats.isDirectory) {
357
+ // For directories with index.html, determine content type based on Accept header
358
+ const indexPath = storagePath.endsWith('/') ? `${storagePath}index.html` : `${storagePath}/index.html`;
359
+ const indexExists = await storage.exists(indexPath);
360
+
361
+ if (indexExists && connegEnabled) {
362
+ const acceptHeader = request.headers.accept || '';
363
+ const wantsTurtle = acceptHeader.includes('text/turtle') ||
364
+ acceptHeader.includes('text/n3') ||
365
+ acceptHeader.includes('application/n-triples');
366
+ const wantsJsonLd = acceptHeader.includes('application/ld+json') ||
367
+ acceptHeader.includes('application/json');
368
+
369
+ if (wantsTurtle) {
370
+ contentType = 'text/turtle';
371
+ } else if (wantsJsonLd) {
372
+ contentType = 'application/ld+json';
373
+ } else {
374
+ contentType = 'text/html';
375
+ }
376
+ } else if (indexExists) {
377
+ contentType = 'text/html';
378
+ } else {
379
+ contentType = 'application/ld+json';
380
+ }
381
+ } else {
382
+ contentType = getContentType(storagePath);
383
+ }
333
384
 
334
385
  const headers = getAllHeaders({
335
386
  isContainer: stats.isDirectory,
336
387
  etag: stats.etag,
337
388
  contentType,
338
389
  origin,
339
- resourceUrl
390
+ resourceUrl,
391
+ connegEnabled
340
392
  });
341
393
 
342
394
  if (!stats.isDirectory) {
@@ -106,7 +106,8 @@ export async function handleCredentials(request, reply, issuer) {
106
106
  // Always generate a proper JWT - CTH requires JWT format
107
107
  const jwks = await getJwks();
108
108
  const signingKey = jwks.keys[0];
109
- const privateKey = await jose.importJWK(signingKey, 'ES256');
109
+ const signingAlg = signingKey.alg || 'ES256'; // Use key's algorithm
110
+ const privateKey = await jose.importJWK(signingKey, signingAlg);
110
111
 
111
112
  const now = Math.floor(Date.now() / 1000);
112
113
  const tokenPayload = {
@@ -131,7 +132,7 @@ export async function handleCredentials(request, reply, issuer) {
131
132
  }
132
133
 
133
134
  const accessToken = await new jose.SignJWT(tokenPayload)
134
- .setProtectedHeader({ alg: 'ES256', kid: signingKey.kid })
135
+ .setProtectedHeader({ alg: signingAlg, kid: signingKey.kid })
135
136
  .sign(privateKey);
136
137
 
137
138
  // Response
package/src/idp/index.js CHANGED
@@ -179,7 +179,7 @@ export async function idpPlugin(fastify, options) {
179
179
  response_modes_supported: ['query', 'fragment', 'form_post'],
180
180
  grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials'],
181
181
  subject_types_supported: ['public'],
182
- id_token_signing_alg_values_supported: ['ES256'],
182
+ id_token_signing_alg_values_supported: ['RS256', 'ES256'],
183
183
  token_endpoint_auth_methods_supported: ['none', 'client_secret_basic', 'client_secret_post'],
184
184
  claims_supported: ['sub', 'webid', 'name', 'email', 'email_verified'],
185
185
  code_challenge_methods_supported: ['S256'],