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.
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:
Here’s the plan for adding authentication to your Temporal deployment:
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.
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.
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.")
}
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
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
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:
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.