OpenID Connect (OIDC)
Use Windmill's OIDC provider to authenticate from scripts to cloud providers and other APIs.
OIDC is an EE feature only.
Windmill OpenID Connect (OIDC) allows your scripts to authenticate to other APIs (such as AWS, GCP, or your own API) without having to store sensitive, long-lived credentials. Your Windmill scripts can generate short-lived ID tokens which are passed to a target API to authenticate and "prove" their identity. ID tokens contain subject and claim information specific to the script, flow or worker, allowing for fine-grained access control (e.g. allowing specific scripts to access AWS buckets). This page documents how to generate Windmill tokens and the token format. For guides on how to integrate OIDC with cloud providers like AWS or your own API, see:
Generating OIDC/JWT tokens
OIDC tokens are generated at runtime and are scoped to the script that generated them. Tokens can be generated with Windmill's SDKs or from the REST API directly. Your token must be associated with an audience which identifies the intended recipient of the token. The audience is provided as a parameter when generating the token. If the audience is incorrect, the consumer will reject the token. (For AWS, this audience is sts.amazonaws.com. For your own APIs, you can specify an audience such as auth.yourcompany.com.) If you are using a TypeScript or Python scripts, you can use the Windmill SDK to generate tokens. For other like REST or shell, you should use the REST api directly:
curl -s -X POST -H "Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/oidc/token/MY_AUDIENCE"
Generate the token
Tokens can be generated at runtime with Windmill's SDKs. For example, to generate an OIDC token with audience sts.amazonaws.com to assume an AWS role:
- TypeScript (Deno)
- TypeScript (Bun)
- Python
import { STSClient } from 'npm:@aws-sdk/client-sts';
import { AssumeRoleWithWebIdentityCommand } from 'npm:@aws-sdk/client-sts';
import * as wmill from 'npm:windmill-client';
export async function main() {
const token = await wmill.getIdToken('sts.amazonaws.com');
const command = new AssumeRoleWithWebIdentityCommand({
RoleArn: 'arn:aws:iam::000000000000:role/my_aws_role',
WebIdentityToken: token,
RoleSessionName: 'my_session'
});
const client = new STSClient({ region: 'us-east-1' });
console.log(await client.send(command));
}
import { STSClient } from '@aws-sdk/client-sts';
import { AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
import * as wmill from 'windmill-client';
export async function main() {
const token = await wmill.getIdToken('sts.amazonaws.com');
const command = new AssumeRoleWithWebIdentityCommand({
RoleArn: 'arn:aws:iam::000000000000:role/my_aws_role',
WebIdentityToken: token,
RoleSessionName: 'my_session'
});
const client = new STSClient({ region: 'us-east-1' });
console.log(await client.send(command));
}
import wmill
import boto3
def main():
sts = boto3.client("sts")
token = wmill.get_id_token("sts.amazonaws.com")
credentials = sts.assume_role_with_web_identity(
RoleArn="arn:aws:iam::000000000000:role/my_aws_role",
WebIdentityToken=token,
RoleSessionName="my_session",
)
print(credentials)
Token payload reference
OIDC tokens generated by Windmill's OIDC provider are signed JSON web tokens (JWTs). The ID token is encoded by using RS256 and signed with a dedicated private key. The expiry time for the token is set for 48 hours. The token includes standard claims:
aud
: Intended audience for the token. This field is set by the user when fetching the ID token and is the only customizable field.iss
: The issuer of the token, your base_url set in in your instance settingssub
: The subject claim is validated by your cloud provider. The subject is formatted as{email}::{script_path}::{flow_path}::{workspace}
.exp
: The expiration time of the token.iat
: The time the token was issued.alg
: The algorithm used to sign the token.kid
: The key ID used to sign the token.
The token also includes custom claims provided by Windmill. To see the full list of claims supported by Windmill's OIDC provider, see the claims_supported
entries at <base_url>/api/oidc/.well-known/openid-configuration
. Note that not all cloud providers support all claims.
job_id
: Job idpath
: Job's path (the script path for a script or a virtual path for a step part of a flow)flow_path
: If job is part of a flow, contain the path of the flowgroups
: the groups the requester is part ofusername
: the username the requester has in the given workspaceemail
: the email of the requesterworkspace
: workspace_id the job is executed in
The following example token was requested with the audience sts.amazonaws.com
from the script u/admin/ambitious_script
in the foobar
workspace:
eyJhbGciOiJSUzI1NiIsImtpZCI6IndpbmRtaWxsIn0.eyJpc3MiOiJodHRwczovL215Y29tcGFueS5jb20iLCJhdWQiOlsic3RzLmFtYXpvbi5jb20iXSwiZXhwIjoxNzA1NjYwOTU1LCJpYXQiOjE3MDU0ODgxNTUsInN1YiI6ImFkbWluQHdpbmRtaWxsLmRldjo6dS9hZG1pbi9hbWJpdGlvdXNfc2NyaXB0Ojpub19mbG93Ojpmb29iYXIiLCJlbWFpbCI6ImFkbWluQHdpbmRtaWxsLmRldiIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJqb2JfaWQiOiIwMThkMTcwNC0wMGMxLTA1YmItNDE4My1lNTZhOGVkZWI4MjMiLCJwYXRoIjoidS9hZG1pbi9hbWJpdGlvdXNfc2NyaXB0IiwiZmxvd19wYXRoIjpudWxsLCJncm91cHMiOlsiYWxsIl0sInVzZXJuYW1lIjoiYWRtaW4iLCJlbWFpbCI6ImFkbWluQHdpbmRtaWxsLmRldiIsIndvcmtzcGFjZSI6ImZvb2JhciJ9.FLOAMsT7wTuCMM7hdZBGU5uMna3v-3FVXbouQ5CXJ61Vp7Ew82w9K2IFdb5rE-x-YDra-MdT8F3v1CNcvB1vQ4jSsgPLFm_fDu0aA2fI_B69Kno2KBpd5afXVhMUwSF4zyRiruZdIS1hL8PjeNsMOIfy-c_FnF_BchbarXswpo6_UjjsJg353l2C5wOqny8OT4fNGn4yyvTM5PDPaisxOrBuSQDK0GhuB1zSZ4OAGChnd5-KwopqlYmm0NlNiUUTYa5tORtNfbvDFM-hrQ1GOKCKn18nmSV5QKjzn8Au8lP8qNmoZPXdXhxr4yPx8mxWHge7ckABZfB3xGpufG436g
{
"iss": "https://mycompany.com/api/oidc/",
"aud": [
"sts.amazonaws.com"
],
"exp": 1705660955,
"iat": 1705488155,
"sub": "[email protected]::u/admin/ambitious_script::no_flow::foobar",
"email": "[email protected]",
"email_verified": true,
"job_id": "018d1704-00c1-05bb-4183-e56a8edeb823",
"path": "u/admin/ambitious_script",
"flow_path": null,
"groups": [
"all"
],
"username": "admin",
"workspace": "foobar"
}
Settings discovery
Windmill's OIDC settings are available at the following discovery URL:
<base_url>/api/oidc/.well-known/openid-configuration
Jwks are available directly at:
<base_url>/api/oidc/jwks
Use OIDC with AWS
Create an identity provider with AWS for OIDC
- Search for the
Identity Providers
tab in the console search or IAM. - Add provider
- Select OpenID Connect
- Provider Url:
<base_url>/api/oidc/
, audience:sts.amazonaws.com
Create a role
- IAM -> Roles -> Create Role
- Web Identity
- Pick provider created above, audience:
sts.amazonaws.com
- Pick the permission policy to attach to this role. You can create as many roles as needed so it's best to be specific.
- fill the Trust policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRoleWithWebIdentity",
"Principal": {
"Federated": "arn:aws:iam::976079455550:oidc-provider/<base_url>/api/oidc/"
},
"Condition": {
"StringEquals": {
"<base_url>/api/oidc/:aud": "sts.amazonaws.com",
"<base_url>/api/oidc/:email": "[email protected]",
...
}
}
}
]
}
Note that AWS only supports conditions on the aud
, sub
, and email
claims. You can use StringLike
on the sub
claim to limit by job, flow, or workspace:
"StringLike": {
"<base_url>/api/oidc/:sub": "*::<workspace>",
"<base_url>/api/oidc/:sub": "*::<script_path>::*::*",
"<base_url>/api/oidc/:sub": "*::*::<flow_path>::*"
}
Get AWS credentials
- TypeScript (Deno)
- TypeScript (Bun)
- Python
import { STSClient } from 'npm:@aws-sdk/client-sts';
import { AssumeRoleWithWebIdentityCommand } from 'npm:@aws-sdk/client-sts';
import * as wmill from 'npm:windmill-client';
export async function main() {
const token = await wmill.getIdToken('sts.amazonaws.com');
const command = new AssumeRoleWithWebIdentityCommand({
RoleArn: 'arn:aws:iam::000000000000:role/my_aws_role',
WebIdentityToken: token,
RoleSessionName: 'my_session'
});
const client = new STSClient({ region: 'us-east-1' });
console.log(await client.send(command));
}
import { STSClient } from '@aws-sdk/client-sts';
import { AssumeRoleWithWebIdentityCommand } from '@aws-sdk/client-sts';
import * as wmill from 'windmill-client';
export async function main() {
const token = await wmill.getIdToken('sts.amazonaws.com');
const command = new AssumeRoleWithWebIdentityCommand({
RoleArn: 'arn:aws:iam::000000000000:role/my_aws_role',
WebIdentityToken: token,
RoleSessionName: 'my_session'
});
const client = new STSClient({ region: 'us-east-1' });
console.log(await client.send(command));
}
import wmill
import boto3
def main():
sts = boto3.client("sts")
token = wmill.get_id_token("sts.amazonaws.com")
credentials = sts.assume_role_with_web_identity(
RoleArn="arn:aws:iam::000000000000:role/my_aws_role",
WebIdentityToken=token,
RoleSessionName="my_session",
)
print(credentials)
You can now get the ephemeral access key, secret key, and session token from it to use with any AWS API.
credentials = credentials["Credentials"]
aws_access_key_id=credentials["AccessKeyId"]
aws_secret_access_key=credentials["SecretAccessKey"]
aws_session_token=credentials["SessionToken"]
Use OIDC with Hashicorp Vault
vault auth enable jwt
vault write auth/jwt/config \
bound_issuer="<base_url>/api/oidc/" \
oidc_discovery_url="<base_url>/api/oidc/"
vault write auth/jwt/role/myproject-production -<<EOF
{
"role_type": "jwt",
"bound_audiences": ["MY_AUDIENCE"],
"bound_claims": { "email": "[email protected]"},
"user_claim": "sub",
"policies": ["myproject-production"],
"ttl": "10m"
}
EOF
vault policy write myproject-production - <<EOF
# Read-only permission on 'secret/data/production/*' path
path "secret/data/production/*" {
capabilities = [ "read" ]
}
EOF
Test it
Write a secret at production/foo:
vault kv put -mount=secret production/foo foo=world
- Bash
- Python
- TypeScript (Deno)
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_JWT=$(curl -s -X POST -H "Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/oidc/token/MY_AUDIENCE")
export VAULT_TOKEN=$(vault write -field=token auth/jwt/login role=myproject-production jwt=$VAULT_JWT)
vault kv get -mount=secret production/foo
import wmill
import hvac
def main():
token = wmill.get_id_token("MY_AUDIENCE")
client = hvac.Client()
response = client.auth.jwt.jwt_login(
role="myproject-production",
jwt=token,
)
print('Client token returned: %s' % response['auth']['client_token'])
print(client.secrets.kv.read_secret_version(path='production/foo'))
import * as wmill from 'windmill-client';
export async function main() {
const jwt = await wmill.getIdToken('MY_AUDIENCE');
const res = await fetch('http://127.0.0.1:8200/v1/auth/jwt/login', {
method: 'POST',
body: JSON.stringify({ jwt, role: 'myproject-production' })
});
const token = (await res.json()).auth.client_token;
const password = await fetch('http://127.0.0.1:8200/v1/secret/data/production/foo', {
headers: { 'X-Vault-Token': token }
});
return password.json();
}