feat(oidc): add CreateOidcClientSheet component and update imports
- 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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user