Skip to content

Commit 2b60324

Browse files
committed
feat(security): ephemeral EdDSA key fallback in setSigningKey (dev/test, empty privateKey)
Fulfils the Plan 0 CANONICAL SEAM: when a realm enables asymmetric signing but no privateKey is configured (empty OIDC_SIGNING_KEY), generate a per-process keypair so getJwks still resolves. SigningConfig.privateKey now optional.
1 parent cb163ee commit 2b60324

1 file changed

Lines changed: 16 additions & 6 deletions

File tree

packages/alepha/src/security/providers/JwtProvider.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createRemoteJWKSet,
99
exportJWK,
1010
type FlattenedJWSInput,
11+
generateKeyPair,
1112
importPKCS8,
1213
type JSONWebKeySet,
1314
type JWK,
@@ -71,16 +72,21 @@ export class JwtProvider {
7172
* registered so this process can verify its own tokens (refresh tokens,
7273
* authorization codes, id_tokens). Public keys are published via `getJwks`.
7374
*
74-
* `privateKey` is a PKCS#8 PEM (from env). Additive: realms without a
75-
* signing key keep the HS256 path untouched.
75+
* `privateKey` is a PKCS#8 PEM (from env). When it is empty/undefined an
76+
* **ephemeral per-process keypair** is generated so dev/test work with no env
77+
* key configured — the published JWKS still resolves; tokens just don't
78+
* survive a restart. Additive: realms without a signing key keep the HS256
79+
* path untouched.
7680
*/
7781
public async setSigningKey(
7882
name: string,
7983
signing: SigningConfig,
8084
): Promise<void> {
81-
const key = await importPKCS8(signing.privateKey, signing.alg, {
82-
extractable: true,
83-
});
85+
const key = signing.privateKey
86+
? await importPKCS8(signing.privateKey, signing.alg, {
87+
extractable: true,
88+
})
89+
: (await generateKeyPair(signing.alg, { extractable: true })).privateKey;
8490
const exported = await exportJWK(key);
8591
// Strip every private member so the published JWK is public-only.
8692
const { d, p, q, dp, dq, qi, ...pub } = exported as Record<string, unknown>;
@@ -259,7 +265,11 @@ export interface SignerHolder {
259265
*/
260266
export interface SigningConfig {
261267
alg: "EdDSA" | "RS256";
262-
privateKey: string;
268+
/**
269+
* PKCS#8 PEM private key. Empty/undefined → an ephemeral per-process keypair
270+
* is generated (dev/test convenience; tokens don't survive a restart).
271+
*/
272+
privateKey?: string;
263273
kid: string;
264274
}
265275

0 commit comments

Comments
 (0)