Skip to main content
The Lemline gateway supports JWT-based authentication and authorization to secure access to workflow APIs.

Authentication flow

  1. Client obtains a JWT token from an identity provider (e.g., Auth0, Keycloak, Cognito)
  2. Client includes the token in gRPC metadata: Authorization: Bearer <token>
  3. Gateway validates the token signature and claims
  4. Gateway checks required scopes and namespace access
  5. If authorized, gateway processes the request
By default, authentication is disabled. Enable it in production to secure your gateway.

Enabling authentication

Set lemline.gateway.authentication.enabled=true in your configuration:
.lemline.yaml
lemline:
  gateway:
    authentication:
      enabled: true
      jwt:
        issuer: https://your-auth-provider.com/
        audience: lemline-api
        public-key: |
          -----BEGIN PUBLIC KEY-----
          MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
          -----END PUBLIC KEY-----

Configuration parameters

authentication.enabled
boolean
default:"false"
Enable JWT authentication. When false, all requests are allowed.
authentication.jwt.issuer
string
required
JWT issuer URL. Must match the iss claim in tokens.Example: https://auth.example.com/
authentication.jwt.audience
string
required
JWT audience. Must match the aud claim in tokens.Example: lemline-api, https://api.example.com
authentication.jwt.public-key
string
required
PEM-encoded RSA public key for verifying token signatures. Supports RSA256, RSA384, RSA512.

Authorization model

The gateway uses scope-based and namespace-based authorization:

Required scopes

Each RPC requires specific scopes in the JWT scope claim:
RPCRequired ScopeDescription
StartWorkflowworkflow:startStart workflow instances
WatchWorkflowworkflow:watchStream workflow events
ListNamespacesworkflow:readList namespaces
ListDefinitionsworkflow:readList workflow definitions
GetDefinitionworkflow:readGet workflow definition
GetDefinitionStatsworkflow:readGet workflow statistics
ListInstancesworkflow:readList workflow instances
GetInstanceTimelineworkflow:readGet instance timeline
WatchDefinitionStatsworkflow:readStream definition stats
WatchInstancesworkflow:readStream instance updates

Namespace access

JWT tokens can restrict access to specific namespaces using the namespaces claim:
JWT payload
{
  "iss": "https://auth.example.com/",
  "aud": "lemline-api",
  "sub": "user-123",
  "scope": "workflow:start workflow:read",
  "namespaces": ["production", "staging"],
  "exp": 1709136896
}
  • If namespaces is present, user can only access those namespaces
  • If namespaces is absent, user can access all namespaces (admin access)

Example JWT payloads

Admin user (all namespaces)

{
  "iss": "https://auth.example.com/",
  "aud": "lemline-api",
  "sub": "admin-456",
  "scope": "workflow:start workflow:read",
  "exp": 1709136896
}

Production-only user

{
  "iss": "https://auth.example.com/",
  "aud": "lemline-api",
  "sub": "user-789",
  "scope": "workflow:start workflow:read",
  "namespaces": ["production"],
  "exp": 1709136896
}

Read-only user

{
  "iss": "https://auth.example.com/",
  "aud": "lemline-api",
  "sub": "viewer-012",
  "scope": "workflow:read",
  "namespaces": ["production", "staging"],
  "exp": 1709136896
}

Client examples

Using grpcurl with JWT

# Set your JWT token
export JWT_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

# Start workflow with authentication
grpcurl -plaintext \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -d '{
    "workflow_id": "wf-123",
    "namespace": "production",
    "name": "order-fulfillment",
    "version": "1.0.0"
  }' \
  localhost:9000 lemline.gateway.v1.WorkflowGateway/StartWorkflow

Using TypeScript (Connect-ES)

import { createPromiseClient } from "@connectrpc/connect";
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { WorkflowGateway } from "./gen/lemline/gateway/v1/workflow_gateway_connect";

const jwtToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";

const transport = createGrpcWebTransport({
  baseUrl: "http://localhost:9000",
  interceptors: [
    (next) => async (req) => {
      req.header.set("Authorization", `Bearer ${jwtToken}`);
      return next(req);
    },
  ],
});

const client = createPromiseClient(WorkflowGateway, transport);

const response = await client.startWorkflow({
  workflowId: "wf-123",
  namespace: "production",
  name: "order-fulfillment",
  version: "1.0.0",
});

Using Python (grpcio)

import grpc
from lemline.gateway.v1 import workflow_gateway_pb2, workflow_gateway_pb2_grpc

jwt_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

# Create channel with credentials
credentials = grpc.composite_channel_credentials(
    grpc.ssl_channel_credentials(),
    grpc.access_token_call_credentials(jwt_token)
)

channel = grpc.secure_channel('gateway.example.com:9000', credentials)
stub = workflow_gateway_pb2_grpc.WorkflowGatewayStub(channel)

# Start workflow
request = workflow_gateway_pb2.StartWorkflowRequest(
    workflow_id="wf-123",
    namespace="production",
    name="order-fulfillment",
    version="1.0.0"
)

response = stub.StartWorkflow(request)

TLS/SSL configuration

In production, combine JWT authentication with TLS encryption:
.lemline.yaml
lemline:
  gateway:
    tls:
      enabled: true
      certificate: /path/to/server.crt
      private-key: /path/to/server.key
    authentication:
      enabled: true
      jwt:
        issuer: https://auth.example.com/
        audience: lemline-api
        public-key: |
          -----BEGIN PUBLIC KEY-----
          ...
          -----END PUBLIC KEY-----
See Gateway Configuration for TLS setup details.

Error responses

UNAUTHENTICATED
error
Missing or invalid JWT token
grpc-status: 16
grpc-message: Missing Authorization header
PERMISSION_DENIED
error
Token lacks required scopes or namespace access
grpc-status: 7
grpc-message: Insufficient scopes: requires 'workflow:start'

Troubleshooting

Symptoms: UNAUTHENTICATED errors even with valid-looking tokensCheck:
  1. Verify issuer and audience match JWT claims exactly
  2. Ensure public key is PEM-encoded and matches the signing key
  3. Check token expiration (exp claim)
  4. Verify algorithm (RSA256/384/512 supported, not HS256)
Debug:
# Decode JWT to inspect claims
echo "$JWT_TOKEN" | cut -d. -f2 | base64 -d | jq .
Symptoms: PERMISSION_DENIED for specific namespacesCheck:
  1. Verify namespaces claim includes the target namespace
  2. If no namespaces claim, user should have admin access
  3. Ensure namespace name matches exactly (case-sensitive)
Symptoms: PERMISSION_DENIED: Insufficient scopesCheck:
  1. Verify scope claim includes required scope (e.g., workflow:start)
  2. Scopes are space-separated in the JWT claim
  3. Scope names are case-sensitive
Example:
{
  "scope": "workflow:start workflow:read"  // Correct
}

Best practices

Use short-lived tokens

Set token expiration (exp) to 1 hour or less for better security

Rotate signing keys

Periodically rotate JWT signing keys and update gateway public key

Principle of least privilege

Grant only required scopes and namespace access per user

Enable TLS in production

Always use TLS encryption with JWT authentication

Next steps

Gateway configuration

Full gateway configuration reference

Start workflows

Start workflow instances via API

TLS setup

Configure TLS encryption

Gateway CLI

Gateway command-line reference

Build docs developers (and LLMs) love