feat(center): OIDC login (auto-provision by email) + CLI config-oidc

- OidcConfig entity (single row) + DB registration.
- OidcService: discovery, Authorization Code + PKCE, state/ticket as
  short-lived signed JWTs (no server session), userinfo/id_token claim
  resolution. Auto-provisions a passwordless user by email.
- Public endpoints /auth/oidc/{status,start,callback,exchange}; callback
  bounces a one-time ticket to the SPA in the URL fragment.
- AuthService.provisionOidcUser + issueSessionForUserId (login shape).
- CLI: 'config oidc' upserts issuer/clientId/secret/callback/redirect/
  scopes/enabled (secret masked on print).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-18 09:44:49 +01:00
parent 6afb935302
commit 2a394969d2
7 changed files with 383 additions and 4 deletions

View File

@@ -2,6 +2,7 @@ import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { AuthService } from './auth/auth.service.js';
import { OidcService } from './auth/oidc.service.js';
import { NodeAdminService } from './nodes/node-admin.service.js';
function getArg(flag: string): string | null {
@@ -15,6 +16,11 @@ function printUsageAndExit(): never {
console.error(' node dist/cli.js user create --email <email> --password <password>');
console.error(' node dist/cli.js user apikey --email <email> [--label <label>]');
console.error(' node dist/cli.js node register --node-id <id> --name <name> --endpoint <url>');
console.error(
' node dist/cli.js config oidc [--issuer <url>] [--client-id <id>] [--client-secret <s>]\n' +
' [--callback-url <url>] [--post-login-redirect <url>] [--scopes "openid email profile"]\n' +
' [--enabled true|false] (sets provided fields; prints config with secret masked)',
);
process.exit(1);
}
@@ -58,6 +64,42 @@ async function main() {
return;
}
if (subject === 'config' && action === 'oidc') {
const oidc = app.get(OidcService);
const patch: Record<string, unknown> = {};
const issuer = getArg('--issuer');
if (issuer !== null) patch.issuer = issuer;
const clientId = getArg('--client-id');
if (clientId !== null) patch.clientId = clientId;
const clientSecret = getArg('--client-secret');
if (clientSecret !== null) patch.clientSecret = clientSecret;
const callbackUrl = getArg('--callback-url');
if (callbackUrl !== null) patch.redirectUri = callbackUrl;
const postLogin = getArg('--post-login-redirect');
if (postLogin !== null) patch.postLoginRedirect = postLogin;
const scopes = getArg('--scopes');
if (scopes !== null) patch.scopes = scopes;
const enabled = getArg('--enabled');
if (enabled !== null) patch.enabled = enabled === 'true';
const saved = await oidc.setConfig(patch);
process.stdout.write(
JSON.stringify({
ok: true,
config: {
issuer: saved.issuer,
clientId: saved.clientId,
clientSecret: saved.clientSecret ? '***set***' : '',
redirectUri: saved.redirectUri,
postLoginRedirect: saved.postLoginRedirect,
scopes: saved.scopes,
enabled: saved.enabled,
},
}) + '\n',
);
return;
}
printUsageAndExit();
} finally {
await app.close();