Skip to content
GuidesDocs 1.3.x

JWT Authentication ​

createJwtAuthInterceptor verifies JSON Web Tokens from the Authorization: Bearer <token> header. It supports three key resolution strategies: JWKS, HMAC secret, and public key.

JWKS (JSON Web Key Set) is the recommended approach. The interceptor fetches and caches signing keys from the identity provider automatically:

typescript
import { createJwtAuthInterceptor } from '@connectum/auth';

const jwtAuth = createJwtAuthInterceptor({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  issuer: 'https://auth.example.com/',
  audience: 'my-api',
  maxTokenAge: '1h',
  claimsMapping: {
    roles: 'realm_access.roles',
    scopes: 'scope',
  },
});

Prefer one key source (jwksUri, publicKey, or secret), then constrain the expected issuer, audience, age, and algorithms for your identity provider. Use claimsMapping only for the claims that become application identity. See JwtAuthInterceptorOptions for the complete field contract and defaults.

A token must carry an exp (expiration time) claim: a signed token without one never expires, so one that leaks stays valid until the signing key is rotated. A token without exp, or with an exp in the past, is rejected as Unauthenticated. Set requireExpiration: false only for an issuer that cannot add exp; an expired token is still rejected. See Signed tokens must carry exp when upgrading.

Errors and Skipped Methods ​

A token that fails verification (bad signature, wrong issuer or audience, expired, no exp) ends the call with Unauthenticated and the fixed text Authentication failed; the text of the verification error is not sent to the client. A valid token with no subject fails with JWT missing subject claim. For the generic createAuthInterceptor, a ConnectError thrown by verifyCredentials is the exception: it reaches the client unchanged, so write its text for the client.

skipMethods takes exact methods and Service/* patterns. The catch-all '*' is refused when the interceptor is created, because it would switch authentication off for every method; to disable the interceptor, do not register it.

HMAC Secret ​

For simple setups and testing environments where tokens are signed with a shared secret:

typescript
const jwtAuth = createJwtAuthInterceptor({
  secret: process.env.JWT_SECRET,
  algorithms: ['HS256'],
  issuer: 'my-service',
});

WARNING

HMAC secrets require both the issuer and the verifier to know the secret. For HS256, the factory requires at least 32 UTF-8 bytes; HS384 requires 48 and HS512 64 when those algorithms are configured. Generate a random secret, keep it out of source control, and check that the environment value is present. Use JWKS to avoid giving verifiers a signing secret.

Public Key ​

For asymmetric verification with a pre-loaded public key:

typescript
const publicKey = await crypto.subtle.importKey(
  'spki', keyData, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['verify'],
);

const jwtAuth = createJwtAuthInterceptor({ publicKey });

Key Resolution Priority ​

If more than one key source is set, the interceptor selects the first available source in this order:

At least one must be provided. If jwksUri is set, publicKey and secret are ignored.

Full Example ​

typescript
import { createServer } from '@connectum/core';
import { createDefaultInterceptors, createErrorHandlerInterceptor } from '@connectum/interceptors';
import { createJwtAuthInterceptor } from '@connectum/auth';

const jwtAuth = createJwtAuthInterceptor({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  issuer: 'https://auth.example.com/',
  audience: 'my-api',
  maxTokenAge: '1h',
  claimsMapping: {
    roles: 'realm_access.roles',
    scopes: 'scope',
  },
});

const server = createServer({
  services: [routes],
  interceptors: [
    createErrorHandlerInterceptor(),
    jwtAuth,
    ...createDefaultInterceptors({ errorHandler: false }),
  ],
});

await server.start();

Verify ​

Call a protected method with a token from the configured issuer and audience, then repeat without the bearer header, with an expired token, and with a token from the wrong issuer or audience. The valid token should reach the handler; the other calls should return Code.Unauthenticated. Add explicit claimsMapping.roles before checking a roles-based authorization rule: a JWT's roles claim is not mapped automatically.

If factory creation fails, check the key source and HMAC secret length first. If an intentionally public RPC still needs a token, check authentication skipMethods; a proto public marker alone does not configure JWT authentication.