If you’re using Temporal and wonder how to enable security features using Azure Active Directory (AAD), you’re at the right place. In this article, we will learn about:
- How to create an app registration in AAD for authentication
- How to create app roles to set users’ permissions per namespace
- How to enable single sign-on (SSO) for Temporal Web UI
- How to query an access token used for Temporal Worker apps and CLI (tctl)
Breaking changes about Auth in version 1.20
Prior to Temporal v1.20, the worker service in Temporal cluster can authorize with the frontend service via its namespace (temporal-system).
Due to the removal of this code in v1.20, it’s required to have mTLS enabled so that the worker service can authorize itself (via the internal-frontend
service).
To sum up:
- If you use v1.19 or older versions: mTLS isn’t required for authorization.
- If you use v1.20 or newer versions: mTLS must be enabled or the system service will fail to authorize.
For all sample code in this article, we will use Temporal v1.19 to simplify the need of mTLS setup.
If you want to learn about how to set up mTLS for Temporal, check out this great blog post from my colleague.
Creating an App Registration in AAD
Step 1 (Create app registration)
In Azure portal, select “Azure Active Directory” and then “App registrations”.
Click on “New registration” and fill in the following info:
- Name: “temporal-auth-demo”
- Other fields: leave default
Click on “Register” to create application.
Step 2 (Create Redirect URIs)
Go to “Authentication” menu, click on “Add a platform” and select “Web”.
Later in this post, we will be running the Temporal Web UI locally at port 8080 so to enable SSO, we need to add a redirect URI for it:
http://localhost:8080/auth/sso/callback
Remember to check “Access tokens” and click on “Configure”.
Click on “Add URI” to add another one for Postman, which will be used later for querying access tokens for Temporal CLI:
https://oauth.pstmn.io/v1/callback
Click on “Save” to finish.
Step 3 (Create a default scope)
Next we will need to create a default scope for the app registration.
Navigate to “Expose an API” menu, click on “Add a scope”.
Leave the application ID URI as its default value: “api://{Client ID}” and click on “Save and continue”.
Set the scope name to “default” and fill in other required fields.
Step 4 (Create a client secret)
Go to “Certificates & secrets” menu, click on “New client secret”, enter a description for the secret and click on “Add”.
Remember to copy the client secret’s value for later use.
Step 5 (Testing with Postman)
At this point, we can try querying an access token using Postman.
Open Postman and go to “Authorization” tab, then select:
- Type: “OAuth 2.0”
- Grant Type: “Authorization Code”
- Check “Authorize using browser”
Fill in the required info as below:
- Auth URL: “https://login.microsoftonline.com/{Tenant ID}/oauth2/v2.0/authorize”
- Access Token URL: “https://login.microsoftonline.com/{Tenant ID}/oauth2/v2.0/token”
- Client ID: The application (client) ID
- Client Secret: The application (client) secret
- Scope: “api://{Client ID}/default”
Then click on “Get New Access Token” and sign in using your browser:
Make sure that your browser allows pop-up windows and can communicate with Postman to provide access tokens. If everything works properly, you can click on “Proceed” in Postman to see the returned access token:
Finally, go to jwt.ms to decode the access token and check all available claims. We will need to add more claims later for authorization.
Creating App Roles for User Permissions
Basing on the security docs on the Temporal website, the access token needs to have a “permissions” claim containing a collection of roles for the caller:
"permissions":[
"system:read",
"default:write"
]
In the example above, the caller have the “read” permission to the “system” namespace and the “write” permission to the “default” namespace.
To create these claims in AAD, we can use App roles.
Step 1 (Create App roles)
In Azure portal, go to the “temporal-auth-demo” app registration.
Navigate to “App roles” menu, click on “Create app role” and fill in the following info to create a new role:
- Display name: “system:read”
- Allowed member types: Users/Groups
- Value: “system:read”
- Description: “system:read”
- Check “Do you want to enable this app role?”
- Click on “Apply”
Next click on “Create app role” again to add another role for “default:write”:
Step 2 (Assign roles to a user)
To assign roles to a user, click on “How do I assign App roles” and then “Enterprise applications”:
Then click on “Assign users and groups”:
Click on “Add user/group” to start adding assignment. Select a user and assign him/her both roles: “system:read” and “default:write”.
Step 3 (Testing with Postman)
Now try querying an access token using Postman again and notice the following new claims have been added:
Now you may wonder that the claim name is “roles” instead of “permissions”, which is expected by Temporal. We will address this issue in the next section.
Running Temporal Server with Authorization
In this example, we will be using the docker-compose repository and running the Temporal server locally.
After cloning the repo, make the following changes:
- In the .env file, change “TEMPORAL_VERSION” to “1.19” so that we don’t need to deal with mTLS.
- In the docker-compose.yml file, enable authorization in the Temporal server:
temporal:
container_name: temporal
depends_on:
- postgresql
- elasticsearch
environment:
- "DB=postgresql"
- "DB_PORT=5432"
- "POSTGRES_USER=temporal"
- "POSTGRES_PWD=temporal"
- "POSTGRES_SEEDS=postgresql"
- "DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml"
- "ENABLE_ES=true"
- "ES_SEEDS=elasticsearch"
- "ES_VERSION=v7"
# Enable default authorizer and claim mapper
- "TEMPORAL_AUTH_AUTHORIZER=default"
- "TEMPORAL_AUTH_CLAIM_MAPPER=default"
# Use claim name "roles" instead of "permissions"
- "TEMPORAL_JWT_PERMISSIONS_CLAIM=roles"
# JWKS containing the public keys used to verify access tokens
- "TEMPORAL_JWT_KEY_SOURCE1=https://login.microsoftonline.com/{Tenant ID}/discovery/v2.0/keys"
- "TEMPORAL_JWT_KEY_REFRESH=30m"
3. In the docker-compose.yml file, enable SSO in the Temporal Web UI:
temporal-ui:
container_name: temporal-ui
depends_on:
- temporal
environment:
- "TEMPORAL_ADDRESS=temporal:7233"
- "TEMPORAL_CORS_ORIGINS=http://localhost:3000"
# Enable authorization
- "TEMPORAL_AUTH_ENABLED=true"
# Specify authorization server and issuer
- "TEMPORAL_AUTH_PROVIDER_URL=https://login.microsoftonline.com/{Tenant ID}/v2.0"
- "TEMPORAL_AUTH_ISSUER_URL=https://login.microsoftonline.com/{Tenant ID}/v2.0"
# Specify client ID and secret
- "TEMPORAL_AUTH_CLIENT_ID={Client ID}"
- "TEMPORAL_AUTH_CLIENT_SECRET={Client Secret}"
# Specify callback URL which is the redirect URI in the app registration
- "TEMPORAL_AUTH_CALLBACK_URL=http://localhost:8080/auth/sso/callback"
# Specify the default scope
- "TEMPORAL_AUTH_SCOPES=openid,api://{Client ID}/default"
4. Run “docker compose up” command to start the Temporal server and other components. Notice that the default namespace registration has failed since the tctl command in the auto-setup.sh script doesn’t provide any access token:
To fix this issue, use Postman to grab an access token and run the following command to create the default namespace:
# Replace the access token below
auth="Bearer {Access Token}"
tctl \
--auth "$auth" \
namespace \
register default
Now you can test the Temporal Web UI at http://localhost:8080. It does require users to login with their AAD credentials:
After signing in successfully, users can only access the namespaces that are granted to them. In the following screenshot, you can see all workflows in the default namespace since you have the “default:write” value in the permission claim:
Finally, to authenticate with the Temporal server from inside a worker application, implement the code to query an access token from AAD and provide it to the WorkflowServiceStubs object:
// Implement code to retrieve an AAD token, then provide it below.
AuthorizationTokenSupplier tokenSupplier =
() -> "Bearer {Access Token}";
WorkflowServiceStubs service =
WorkflowServiceStubs.newServiceStubs(
WorkflowServiceStubsOptions.newBuilder()
.addGrpcMetadataProvider(new AuthorizationGrpcMetadataProvider(tokenSupplier))
.setTarget(temporalServerUrl).build());
WorkflowClient client = WorkflowClient.newInstance(service);
Updates for Temporal v1.20+
cassandra:
image: cassandra:3.11
ports:
- "9042:9042"
temporal:
image: temporalio/auto-setup:${SERVER_TAG:-latest}
ports:
- "7233:7233"
volumes:
- ${DYNAMIC_CONFIG_DIR:-../config/dynamicconfig}:/etc/temporal/config/dynamicconfig
- ${TEMPORAL_LOCAL_CERT_DIR}:${TEMPORAL_TLS_CERTS_DIR}
depends_on:
- cassandra
environment:
- "CASSANDRA_SEEDS=cassandra"
- "DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development.yaml"
- "SERVICES=frontend:matching:history:internal-frontend:worker"
- "USE_INTERNAL_FRONTEND=true"
- "TEMPORAL_TLS_SERVER_CA_CERT=${TEMPORAL_TLS_CERTS_DIR}/ca.cert"
- "TEMPORAL_TLS_SERVER_CERT=${TEMPORAL_TLS_CERTS_DIR}/cluster.cert"
- "TEMPORAL_TLS_SERVER_KEY=${TEMPORAL_TLS_CERTS_DIR}/cluster.key"
- "TEMPORAL_TLS_REQUIRE_CLIENT_AUTH=true"
- "TEMPORAL_TLS_FRONTEND_CERT=${TEMPORAL_TLS_CERTS_DIR}/cluster.cert"
- "TEMPORAL_TLS_FRONTEND_KEY=${TEMPORAL_TLS_CERTS_DIR}/cluster.key"
- "TEMPORAL_TLS_CLIENT1_CA_CERT=${TEMPORAL_TLS_CERTS_DIR}/ca.cert"
- "TEMPORAL_TLS_CLIENT2_CA_CERT=${TEMPORAL_TLS_CERTS_DIR}/ca.cert"
- "TEMPORAL_TLS_INTERNODE_SERVER_NAME=tls-sample"
- "TEMPORAL_TLS_FRONTEND_SERVER_NAME=tls-sample"
- "TEMPORAL_TLS_FRONTEND_DISABLE_HOST_VERIFICATION=false"
- "TEMPORAL_TLS_INTERNODE_DISABLE_HOST_VERIFICATION=false"
- "TEMPORAL_CLI_ADDRESS=temporal:7236"
- "TEMPORAL_CLI_TLS_CA=${TEMPORAL_TLS_CERTS_DIR}/ca.cert"
- "TEMPORAL_CLI_TLS_CERT=${TEMPORAL_TLS_CERTS_DIR}/cluster.cert"
- "TEMPORAL_CLI_TLS_KEY=${TEMPORAL_TLS_CERTS_DIR}/cluster.key"
- "TEMPORAL_CLI_TLS_ENABLE_HOST_VERIFICATION=true"
- "TEMPORAL_CLI_TLS_SERVER_NAME=tls-sample"
- "TEMPORAL_AUTH_AUTHORIZER=default"
- "TEMPORAL_AUTH_CLAIM_MAPPER=default"
- "TEMPORAL_JWT_PERMISSIONS_CLAIM=roles"
- "TEMPORAL_JWT_KEY_SOURCE1=https://login.microsoftonline.com/{Tenant ID}/discovery/v2.0/keys"
- "TEMPORAL_JWT_KEY_REFRESH=30m"
temporal-ui:
image: temporalio/ui:${UI_TAG:-latest}
ports:
- "8080:8080"
volumes:
- ${TEMPORAL_LOCAL_CERT_DIR}:${TEMPORAL_TLS_CERTS_DIR}
depends_on:
- temporal
environment:
- "TEMPORAL_ADDRESS=temporal:7233"
- "TEMPORAL_TLS_CA=${TEMPORAL_TLS_CERTS_DIR}/ca.cert"
- "TEMPORAL_TLS_CERT=${TEMPORAL_TLS_CERTS_DIR}/cluster.cert"
- "TEMPORAL_TLS_KEY=${TEMPORAL_TLS_CERTS_DIR}/cluster.key"
- "TEMPORAL_TLS_ENABLE_HOST_VERIFICATION=true"
- "TEMPORAL_TLS_SERVER_NAME=tls-sample"
- "TEMPORAL_AUTH_ENABLED=true"
- "TEMPORAL_AUTH_PROVIDER_URL=https://login.microsoftonline.com/{Tenant ID}/v2.0"
- "TEMPORAL_AUTH_ISSUER_URL=https://login.microsoftonline.com/{Tenant ID}/v2.0"
- "TEMPORAL_AUTH_CLIENT_ID={Client ID}"
- "TEMPORAL_AUTH_CLIENT_SECRET={Client Secret}"
- "TEMPORAL_AUTH_CALLBACK_URL=http://localhost:8080/auth/sso/callback"
- "TEMPORAL_AUTH_SCOPES=openid,api://{Client ID}/default"