Skip to content

Fix: ES256 JWT key format mismatch — 'secretOrPrivateKey must be an asymmetric key' #526

Description

@easonLiangWorldedtech

Fix: ES256 JWT key format mismatch — secretOrPrivateKey must be an asymmetric key

Problem

Self-hosted Roomote fails to start with error:

secretOrPrivateKey must be an asymmetric key when using ES256

This occurs because the auth tokens (job, preview, run) and sandbox OIDC all use ES256 algorithm via jsonwebtoken, but the library expects PEM format keys while Roomote passes raw DER bytes.

Root Cause

Roomote stores auth keys in environment variables as Base64-encoded P-256 DER bytes. The code decodes them like this:

// packages/auth/src/auth-token.ts (line 63)
const privateKey = Buffer.from(getJobAuthPrivateKey(), 'base64').toString('utf-8');
return jwt.sign(payload, privateKey, { algorithm: 'ES256' });

This produces raw DER bytes as a string. However, jsonwebtoken ES256 requires PEM format (with -----BEGIN PUBLIC KEY----- / -----END PUBLIC KEY----- headers). Passing raw DER bytes causes the error above.

The same pattern appears in 4 files:

  • packages/auth/src/auth-token.ts — job auth tokens (sign + verify)
  • packages/auth/src/preview-token.ts — preview tokens (sign + verify)
  • packages/auth/src/run-token.ts — run tokens (sign + verify)
  • packages/auth/src/sandbox-oidc.ts — sandbox OIDC tokens

Fix

Convert Base64 DER → PEM before passing to jsonwebtoken. Add a helper function and use it in all 4 files:

/** Convert Base64 DER bytes to PEM format for jsonwebtoken ES256. */
function derToPem(der: Buffer, label: string): string {
  const b64 = der.toString('base64');
  const lines = b64.match(/.{1,64}/g) || [];
  return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----`;
}

// In createAuthToken / validateAuthToken:
const privateKeyDer = Buffer.from(getJobAuthPrivateKey(), 'base64');
const privateKeyPem = derToPem(privateKeyDer, 'PRIVATE KEY');
return jwt.sign(payload, privateKeyPem, { algorithm: 'ES256' });

Same pattern applied to preview-token.ts, run-token.ts, and sandbox-oidc.ts.

Files Changed

File Change
packages/auth/src/auth-token.ts Add derToPem(), convert private/public keys in sign/verify
packages/auth/src/preview-token.ts Same pattern for preview tokens
packages/auth/src/run-token.ts Same pattern for run tokens
packages/auth/src/sandbox-oidc.ts Add derToPem() + decodeDerToPem(), use in OIDC token creation

Why This Works

  • Environment variables remain Base64 DER (Roomote's design, no docker-compose change needed)
  • The new derToPem() converts DER → PEM at runtime before passing to jsonwebtoken
  • PEM format is what jsonwebtoken ES256 expects — it parses the header/footer and extracts the key

Testing

The existing tests in packages/auth/src/__tests__/ should pass since they mock jsonwebtoken. For integration testing, deploy with a self-hosted instance using Base64 DER keys (as generated by the setup script) and verify:

  1. Roomote starts without ES256 error
  2. Auth tokens can be created and validated
  3. Preview URLs work correctly

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions