Temporal is a powerhouse for orchestrating workflows, offering durability and reliability out of the box. But when you’re running a self-hosted Temporal deployment, security becomes a critical consideration. How do you ensure that only authorized users and services can access specific workflows? The answer lies in implementing robust role-based authentication.
In this post, we’ll break down how to build a custom authentication system for Temporal. You’ll learn how to create a custom authorizer and claim mapper, integrate them into the Temporal server, and deploy everything using Docker and Helm. Let’s dive in.
Why custom authentication for Temporal?
By default, self-hosted Temporal doesn’t enforce authentication, making it flexible but potentially vulnerable in multi-tenant environments. Without authentication, any service or user can interact with workflows, which isn’t ideal in production settings.
A custom authentication layer solves this by:
- Restricting access based on roles and permissions.
- Ensuring users and services authenticate via OpenID Connect (OIDC).
- Keeping security tight without compromising Temporal’s resilience.
How Custom Authentication Works in Temporal
Here’s the plan for adding authentication to your Temporal deployment:
-
Implement a claim mapper – This component extracts and translates authentication tokens into Temporal’s expected format.
-
Build an authorizer – The authorizer validates permissions and enforces access controls.
-
Modify the Temporal server – We’ll hook in our custom authentication components.
-
Package everything in Docker and deploy with Helm – A clean, scalable way to roll out our solution.
Implementing a Claim Mapper
The claim mapper is responsible for converting OIDC authentication tokens into a format that Temporal understands. This allows the system to recognize users and assign appropriate permissions.
type OIDCClaimMapper struct {
issuerURL string
clientID string
jwksURL string
keySet jwk.Set
}
func NewOIDCClaimMapper() *OIDCClaimMapper {
issuerURL := os.Getenv("TEMPORAL_OIDC_ISSUER_URL")
clientID := os.Getenv("TEMPORAL_OIDC_CLIENT_ID")
jwksURL := issuerURL + "/.well-known/jwks.json"
keySet, err := jwk.Fetch(context.Background(), jwksURL)
if err != nil {
return nil
}
return &OIDCClaimMapper{
issuerURL: issuerURL,
clientID: clientID,
jwksURL: jwksURL,
keySet: keySet,
}
}
This mapper ensures that users are correctly identified based on their authentication tokens and assigned roles accordingly.
Implementing an Authorizer
The authorizer is where access control happens. It checks whether a user has permission to perform a requested operation.
type OIDCAuthorizer struct{}
func (a *OIDCAuthorizer) Authorize(ctx context.Context, claims *authorization.Claims, target *authorization.CallTarget) (authorization.Result, error) {
if authorization.IsHealthCheckAPI(target.APIName) {
return decisionAllow, nil
}
if claims == nil || target.Namespace == "" {
return decisionAllow, nil
}
metadata := api.GetMethodMetadata(target.APIName)
var userRole authorization.Role
switch metadata.Scope {
case api.ScopeCluster:
userRole = claims.System
case api.ScopeNamespace:
userRole = claims.System | claims.Namespaces[target.Namespace]
default:
return decisionDeny, nil
}
requiredRole := getRequiredRole(metadata.Access)
if userRole >= requiredRole {
return decisionAllow, nil
}
return decisionDeny, nil
}
With this setup, Temporal ensures that only authorized users can access specific workflows and namespaces.
Integrate Claim Mapper and Authorizer with Temporal Server
Next, we will create a custom Temporal Server that implements the Claim Mapper & Authorizer:
func main() {
log.Println("🚀 Starting Temporal Server with OIDC Authentication...")
cfg, err := config.LoadConfig("development", "./config", "")
if err != nil {
log.Fatal(err)
}
s, err := temporal.NewServer(
temporal.ForServices(temporal.DefaultServices),
temporal.WithConfig(cfg),
temporal.InterruptOn(temporal.InterruptCh()),
// Inject Custom ClaimMapper
temporal.WithClaimMapper(func(cfg *config.Config) authorization.ClaimMapper {
return NewOIDCClaimMapper()
}),
// Inject Custom Authorizer
temporal.WithAuthorizer(&OIDCAuthorizer{}),
)
if err != nil {
log.Fatal(err)
}
err = s.Start()
if err != nil {
log.Fatal(err)
}
log.Println("Temporal Server Stopped.")
}
Deploying a Custom Authentication Solution in Temporal
Building and Deploying the Docker Image
Now that we’ve built our customer server with the authentication components integrated, let’s package them into a Docker image:
docker build -t temporal-auth-server .
docker push temporal-auth-server:latest
Configuring Helm for Deployment
Next, modify the Helm values file to deploy our custom server and hook up our OIDC Provider:
server:
image:
repository: temporal-auth-server
tag: latest
pullPolicy: Never
command: ["/app/temporal-auth-server"]
additionalEnv:
- name: TEMPORAL_OIDC_ISSUER_URL
valueFrom:
secretKeyRef:
name: temporal-auth-secrets
key: issuer_url
- name: TEMPORAL_OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: temporal-auth-secrets
key: client_id
Finally, apply the Helm chart:
helm upgrade install temporal temporal/temporal -f ./k8s/dev/values-dev.yaml -n temporal
Is self-hosted Temporal the right approach for your project?
If you are deploying self-hosted Temporal in production, you should always use OIDC to restrict user access.
Consider using this claim mapper and authorizer approach if:
-
You need fine-grained access control for different teams and workflows.
-
Your system relies on OIDC authentication to integrate with identity providers.
-
You want to enforce security boundaries across multiple namespaces.
Need help implementing authentication for Temporal?
Security is critical when deploying Temporal at scale, and getting authentication right can be complex. If you’re looking for expert guidance, Bitovi’s Temporal consultants can help.
Tell us about your project, and we’ll set up a free consultation to help you design a secure, scalable Temporal deployment tailored to your needs.
Previous Post