# External auth with JWT

> How do I authenticate users with external JWT tokens instead of Windmill built-in auth?

Instead of using the built-in authentication and authorization mechanisms, you can generate your own JWT tokens with the desired permissions for your already authenticated users and pass them to Windmill.
This way, you control what permissions your users have without having to create them in Windmill.

This feature is [Enterprise Edition](/pricing) only.

For [billing](/pricing), a unique external JWT user is counted per (username/email, scope, instance) tuple used in the last 30 days. The same email used with two different scopes, or on two different instances, counts as two JWT users. Each unique JWT user counts as half a [Seat](../../core_concepts/16_roles_and_permissions/index.mdx#users) - the same as an [operator](../../core_concepts/16_roles_and_permissions/index.mdx#operator). Operators and external JWT users are interchangeable in seat counting (e.g. 1 operator + 1 external JWT user = 1 Seat).

For JWT verification, the first option is to pass as an environment variable called `JWT_EXT_PUBLIC_KEY` the public key in PEM format (RS256).
The second option is to pass as `JWT_EXT_JWKS_URL` the url of the JWKs endpoint to retrieve the public keys from which the JWTs will be matched on the `kid` field.
In the latter case, the instance will refresh its cache of public keys every 15 minutes.
The environment variable in both cases has to be passed to all servers and workers.

The JWT token should be prefixed with `jwt_ext_` to let Windmill know that this is an external token and passed in the `Authorization` header as a bearer token.
The JWT payload has to contain the following fields:
- `username`: the username of the user (for logs)
- `email`: the email of the user (for logs)
- `is_admin`: a boolean indicating if the user is an admin
- `is_operator`: a boolean indicating if the user is an operator (run-only)
- `workspace_id`: the workspace id. The token will be granted the configured permissions in the specified workspace.
- `workspace_ids`: an array of workspace ids. When provided, the token will be granted the configured permissions in all specified workspaces. In that case, the `workspace_id` field is not required.
- `folders`: an array of arrays containing 3 values: the name of the folder, a boolean indicating whether the user can write to the folder and a boolean indicating whether the user is the owner of the folder (can manage folder permissions)
- `groups`: an array of strings containing the groups to which the user belongs. When using JWT external authentication, **group permissions on folders are not checked** because the user's permissions depend directly on the folder permissions specified in the JWT.
- `scopes`: an optional array of strings containing the scopes of the user/token. To limit the user's job listing permissions by specific tags, add a scope with the value `if_jobs:filter_tags:tag1,tag2`.

While you don't need to create the user on Windmill, you should create the workspaces and folders.

Here's an example TypeScript code that generates the JWT token with the required payload and token prefix:

```typescript

const YOUR_PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----`

async function generateJWT(
  kid: string,
  username: string,
  email: string,
  is_admin= false,
  is_operator= false,
  workspace_id?: string,
  workspace_ids?: string[],
  folders: [string, boolean, boolean][] = [],
  groups: string[] = [],
  scopes: string[] = []
) {
  const signer = createSigner({
    kid,
    algorithm: 'RS256',
    key: YOUR_PRIVATE_KEY,
    expiresIn: '1h'
  })

  const token = await signer({
    username,
    email,
    is_admin,
    is_operator,
    folders,
    groups,
    workspace_id,
    workspace_ids,
    scopes
  })

  return "jwt_ext_" + token
}
```

## Embed public apps using your own authentication

On the [Enterprise Edition](/pricing), it is possible for one to embed [public apps](../../apps/8_public_apps.mdx) and reuse the embedding app auth and userbase. To do so, use an iframe as usual with the public app url as `src`.

For instance: `https://your_instance.com/public/foo/a7f83d827f8bbb3a332c730659f4cf39`, but in addition, append to the url the jwt token, separated by a '/'.

`https://your_instance.com/public/foo/a7f83d827f8bbb3a332c730659f4cf39/eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IiJ9.ey1c2VybmFtZSI6ImZvbyIsImVtYWlsIjoiZm9vQHdpbmRtaWxsLmRldiIsImlzX2FkbWluIjp0cnVlLCJpc19vcGVyYXRvciI6ZmFsc2UsImZvbGRlcnMiOltdLCJncm91cHMiOltdLCJ3b3Jrc3BhY2VfaWQiOiJmb28iLCJpYXQiOjE3MjM0OTc0ODksImV4cCI6MTcyMzUwMTA4OX0.nnhAGjR_PbuHhLzVCDrTZmRRU9zQd_gpona8bSuvIWlK6Taaxgojn-8tQk1IDykw_WHdnLPgWrOt9uGPAPFD0SsLqK_P-aM1Q8KU-X-Ve3qMQ-Sru5IpE-BgCnb5n0_s2mR6-Ebl6exPYQWJFFNWQ_cj6lDF2fYS71hv2IeQqwssHU4YD1ujUl0rm1rCzRKGuK-iVr9mdFywB2K95iBnhJ0-XLnysjyM4UtutGqLqVZgIHabm2HhgFNNLfHtpfgNYtrk3l3lxCYsr9jmD5Z3lnLcWNhBDWxBlneyIp7yt73hLt7v5QKVoKzFgU_Ikf_FVx0TC7dmUvEKNhqfjfKNA`

This will make the app act such that the user authenticated is the one that corresponds to the jwt payload. Windmill uses just-in-time provisioning by default and the user doesn't need to have been pre-provisioned for it to work.
