feat(oidc): add CreateOidcClientSheet component and update imports
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 2m25s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

- Introduced the CreateOidcClientSheet component for OIDC client creation.
- Updated the index export to include the new component.
- Modified the OIDC admin route to import and utilize the new CreateOidcClientSheet.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-11 19:40:39 +07:00
co-authored by Cursor
parent 0b6fa65b08
commit 36c4091dbf
5 changed files with 251 additions and 101 deletions
+24 -2
View File
@@ -40,8 +40,26 @@ function publicJwkFromPrivateExport(jwk: JWK, kid: string): JWK {
async function materialFromPem(
kid: string,
privatePem: string,
publicJwkJson?: string,
): Promise<OidcKeyMaterial> {
const privateKey = await importPKCS8(privatePem, 'RS256')
// jose defaults extractable=false for private keys → exportJWK throws
const privateKey = await importPKCS8(privatePem, 'RS256', {
extractable: true,
})
if (publicJwkJson) {
try {
const stored = JSON.parse(publicJwkJson) as JWK
return {
kid,
privateKey,
publicJwk: publicJwkFromPrivateExport({ ...stored, kid }, kid),
}
} catch {
// fall through to derive from private key
}
}
const full = await exportJWK(privateKey)
return {
kid,
@@ -67,7 +85,11 @@ export async function ensureOidcSigningKey(
const existing = getActiveOidcSigningKey(app.db)
if (existing) {
cached = await materialFromPem(existing.kid, existing.privatePem)
cached = await materialFromPem(
existing.kid,
existing.privatePem,
existing.publicJwkJson,
)
return cached
}
+48
View File
@@ -1,4 +1,7 @@
import { describe, expect, it } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { jwtVerify, createLocalJWKSet } from 'jose'
import { buildApp } from '../src/app.js'
import { loadConfig } from '../src/config.js'
@@ -47,6 +50,51 @@ describe('OIDC IdP', () => {
await app.close()
})
it('reloads RS256 key from SQLite after restart (extractable import)', async () => {
const dir = mkdtempSync(join(tmpdir(), 'oidc-key-'))
const dbPath = `sqlite:${join(dir, 'app.db')}`
try {
resetOidcKeyCache()
const config = loadConfig({
...process.env,
JWT_SECRET: 'test-secret-at-least-8',
ADMIN_EMAIL: '[email protected]',
ADMIN_PASSWORD: 'adminpass',
DATABASE_URL: dbPath,
ISSUER: 'https://auth.test.local',
OIDC_ISSUER: 'https://auth.test.local',
NODE_ENV: 'test',
})
const first = await buildApp({ config, databaseUrl: dbPath })
const jwks1 = await first.inject({
method: 'GET',
url: '/.well-known/jwks.json',
})
expect(jwks1.statusCode).toBe(200)
const kid = (jwks1.json() as { keys: { kid: string }[] }).keys[0]?.kid
expect(kid).toBeTruthy()
await first.close()
resetOidcKeyCache()
const second = await buildApp({ config, databaseUrl: dbPath })
const jwks2 = await second.inject({
method: 'GET',
url: '/.well-known/jwks.json',
})
expect(jwks2.statusCode).toBe(200)
expect((jwks2.json() as { keys: { kid: string }[] }).keys[0]?.kid).toBe(
kid,
)
await second.close()
} finally {
try {
rmSync(dir, { recursive: true, force: true })
} catch {
// Windows may keep better-sqlite3 handle briefly
}
}
})
it('authorization code flow issues id_token with groups', async () => {
const app = await buildTestApp()