// Test-only identity provider. Never imported or started by application code. import { createServer } from 'node:http'; import { createHash, randomBytes } from 'node:crypto'; import { pathToFileURL } from 'node:url'; import { generateKeyPair, exportJWK, SignJWT } from 'jose'; export async function startMockOidc(port = 0) { const { privateKey, publicKey } = await generateKeyPair('RS256'); const jwk = { ...await exportJWK(publicKey), kid: 'test-key', use: 'sig', alg: 'RS256' }; const codes = new Map(), refreshTokens = new Set(), accessTokens = new Set(); const controls = { badNonce: false, denyRefresh: false, refreshCount: 0, omitName: false, omitRefreshToken: false, subject: 'synology-test-user', groups: ['model-delivery-users'] }; let issuer = ''; const server = createServer(async (req, res) => { const profile = { sub: controls.subject, username: 'test.sso', ...(controls.omitName ? {} : { name: 'טום בדיקה' }), email: 'test@example.invalid', groups: controls.groups }; const json = (data, status = 200) => { res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(data)); }; const url = new URL(req.url, issuer); try { if (url.pathname === '/.well-known/openid-configuration') return json({ issuer, authorization_endpoint: `${issuer}/authorize`, token_endpoint: `${issuer}/token`, userinfo_endpoint: `${issuer}/userinfo`, jwks_uri: `${issuer}/jwks`, response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'refresh_token'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: ['RS256'], token_endpoint_auth_methods_supported: ['client_secret_post'], scopes_supported: ['openid', 'profile', 'email', 'groups', 'offline_access'], claims_supported: ['sub', 'username', 'name', 'email', 'groups'], code_challenge_methods_supported: ['S256'] }); if (url.pathname === '/jwks') return json({ keys: [jwk] }); if (url.pathname === '/authorize') { const values = Object.fromEntries(url.searchParams); if (values.client_id !== 'model-test' || values.redirect_uri !== 'http://localhost:4174/api/auth/callback' || values.code_challenge_method !== 'S256' || !values.code_challenge || !values.state || !values.nonce) return json({ error: 'invalid_request' }, 400); const code = randomBytes(24).toString('hex'); codes.set(code, values); const redirect = new URL(values.redirect_uri); redirect.searchParams.set('code', code); redirect.searchParams.set('state', values.state); res.writeHead(302, { location: redirect.href }); return res.end(); } if (url.pathname === '/token' && req.method === 'POST') { let raw = ''; for await (const chunk of req) raw += chunk; const fields = new URLSearchParams(raw); if (fields.get('client_id') !== 'model-test' || fields.get('client_secret') !== 'mock-test-secret') return json({ error: 'invalid_client' }, 401); let values; if (fields.get('grant_type') === 'authorization_code') { values = codes.get(fields.get('code')); codes.delete(fields.get('code')); if (!values || values.redirect_uri !== fields.get('redirect_uri') || createHash('sha256').update(fields.get('code_verifier') ?? '').digest('base64url') !== values.code_challenge) return json({ error: 'invalid_grant' }, 400); } else if (fields.get('grant_type') === 'refresh_token') { if (controls.denyRefresh || !refreshTokens.delete(fields.get('refresh_token'))) return json({ error: 'invalid_grant' }, 400); controls.refreshCount++; } else return json({ error: 'unsupported_grant_type' }, 400); const access_token = randomBytes(32).toString('hex'), refresh_token = randomBytes(32).toString('hex'); accessTokens.add(access_token); refreshTokens.add(refresh_token); const id_token = values ? await new SignJWT({ ...profile, nonce: controls.badNonce ? 'wrong-nonce' : values.nonce }).setProtectedHeader({ alg: 'RS256', kid: jwk.kid }).setIssuer(issuer).setAudience('model-test').setIssuedAt().setExpirationTime('1h').sign(privateKey) : undefined; return json({ access_token, ...(!controls.omitRefreshToken ? { refresh_token } : {}), id_token, token_type: 'Bearer', expires_in: 3600, scope: 'openid profile email groups offline_access' }); } if (url.pathname === '/userinfo' && accessTokens.has(req.headers.authorization?.replace(/^Bearer /, ''))) return json(profile); return json({ error: 'not_found' }, 404); } catch { return json({ error: 'server_error' }, 500); } }); await new Promise(resolve => server.listen(port, '127.0.0.1', resolve)); issuer = `http://localhost:${server.address().port}`; return { issuer, controls, close: () => new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())) }; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const provider = await startMockOidc(4005); console.log(`Test OIDC provider ready at ${provider.issuer}`); }