External Identity Providers
Use an ExternalIdentityProvider when your own application calls Mission Control's API on behalf of its users, for example when you embed Mission Control in your product. It isn't for signing people in to the Mission Control UI. For that, see Single Sign-On.
Your application signs a JWT for each of its users with its own issuer, and sends it with each request. Mission Control treats the request as coming from that user: a person named <provider>:<username>, who belongs to the teams the provider maps them into.
A provider answers three questions, in this order:
| Question | Answered by | Result |
|---|---|---|
| Is this token real, and is it for us? | Verification | The token is accepted or rejected |
| Who is this person? | identity | A person |
| Which teams are they in? | identity.teams | Team memberships, limited to an allowlist |
What the person may do is never decided by the provider. It comes from RoleBindings that select the person or their teams, and RoleBindings never read claims.
ExternalIdentityProviders are managed through Kubernetes only.
Fields
| Field | Required | Default | Description |
|---|---|---|---|
spec.issuer | Yes | Must exactly match the token's iss claim. Can't be changed | |
spec.audience | Yes | Must be one of the token's aud claims, usually the Mission Control URL | |
spec.jwksURL | No | jwks_uri of the issuer's OpenID configuration | Where the issuer's signing keys are fetched from |
spec.algorithms | No | RS256, ES256 | Signature algorithms accepted |
spec.maxTokenAge | No | 5m | Longest lifetime (exp - iat) a token may have |
spec.identity.username | Yes | CEL expression returning the person's username | |
spec.identity.name | No | The username | CEL expression returning the person's display name |
spec.identity.email | No | CEL expression returning an email, shown on the person only | |
spec.identity.teams.mapper | If teams is set | CEL expression returning a list of team names | |
spec.identity.teams.allowed | If teams is set | The only teams this provider may add anyone to. At least one | |
spec.disabled | No | false | Rejects every token from this provider, without removing it |
For example, a provider named oipa, with issuer https://auth.oipa.example.com, identity.username set to claims.user.id, and a team mapper with allowed: [acme-operator, acme-viewer], signs in a token whose user.id claim is u-4821 as the person oipa:u-4821.
How a request is signed in
Your application sends the user's token in the Authorization: Bearer <token> header of every request. There's no session or cookie.
Mission Control looks at the token's iss claim. If it matches a provider's issuer, the token must pass that provider's checks, or the request is refused with 401 Unauthorized. Tokens from other issuers are handled by Mission Control's normal sign-in, as if no provider existed.
Don't set issuer to the issuer Mission Control's own sign-in uses, such as its Clerk issuer. Such a provider shows Ready=False and is ignored.
Verification
A token is accepted only when these checks pass:
| Check | Rule |
|---|---|
| Provider | The provider isn't disabled |
| Signature | Signed with one of algorithms, by a key from the provider's signing keys |
iss | Exactly issuer |
aud | Contains audience |
exp | Present, and not in the past |
iat | Present, and not in the future |
nbf | If present, not in the future |
| Lifetime | exp - iat is at most maxTokenAge |
| Username | identity.username returns a non-empty string without * |
Time checks tolerate 30 seconds of clock skew. Mission Control can't revoke these tokens, so maxTokenAge keeps them short-lived. An application that needs longer sessions issues new tokens, not longer ones.
Every token that passes these checks is trusted, so the issuer must be dedicated to your application. Shared issuers that sign tokens for many unrelated applications, such as Google, GitHub Actions or multi-tenant Microsoft Entra, aren't supported.
Algorithms and keys
- Only asymmetric algorithms such as
RS256,ES256andEdDSAare accepted. Shared-secret algorithms such asHS256are rejected. issuerandjwksURLmust usehttps, except forlocalhostduring development.- Mission Control fetches the signing keys from
jwksURL, or from your issuer's/.well-known/openid-configuration, and picks up rotated keys automatically.
All accepted algorithms and key fetching details
- Accepted algorithms:
RS256,RS384,RS512,PS256,PS384,PS512,ES256,ES384,ES512andEdDSA.noneis never accepted. - A discovered
jwks_urimust usehttpstoo. Redirects are followed at most 5 times, and never from https to http. - Keys are fetched on the provider's first token, cached, and refreshed in the background. If they can't be fetched, the request fails with
500 Internal Server Error, and the next token retries.
Identity
Each user of a provider is a person, created on their first request. Their key is <provider name>:<username>, such as oipa:u-4821, and that's how RoleBindings name them.
username, name and email are CEL expressions over claims, which holds every claim of the verified token:
identity.name[claims.user.first_name,
has(claims.user.middle_name) ? claims.user.middle_name : "",
claims.user.last_name].filter(s, s != "").join(" ")
- If
usernamefails, or doesn't return a non-empty string, the token is rejected with401. Convert a numeric user ID withstring(). - A username containing
*is rejected, so it can't be confused with<provider>:*. - If
nameoremailfails, the person keeps the stored value. - The username must be unique and stable in your application. A username that changes makes a new person, without the old one's teams or bindings.
- These people are separate from Mission Control's own users. An external user whose email is
alice@example.comgets none of the Mission Control useralice@example.com's access. Mission Control never verifies, matches on, or sends anything to that email. - Wherever a person is shown, such as in approvals and team members, a provider's person is shown with their key.
Teams
The person's teams from the provider become exactly the teams the mapper returns that are also in allowed:
identity.teams.mapperhas(claims.org.roles) ? claims.org.roles.map(r, "acme-" + r) : []
- Allowed teams only. A returned team that isn't in
allowedis skipped and logged. Including the shippedadminsteam inallowedmakes the provider report a warning, since your application's claims then decide who is a Mission Control admin. - Existing teams only. The provider never creates teams. An
allowedentry naming a team that doesn't exist is reported in the provider's status. - Kept in sync both ways. The person joins returned teams and leaves teams this provider added them to that it no longer returns.
- Only its own memberships. The provider never removes a membership an admin added. A membership an admin removes comes back on the next sync if the mapper still returns it. To keep a user out of a team, change
allowedor the claims. - On every request. The mapper runs before each request is checked, and memberships are written only when they change.
- A failing mapper refuses the request with
401and a body saying that team mapping failed. The stored memberships stay as they are. - Removing a team from
allowed, or removingidentity.teams, removes this provider's memberships of those teams at once. - Without
teams, the provider adds no one to any team. Its users get only what RoleBindings grant them by name.
These memberships work like any other team membership, including for playbook approvers and on the Subjects page, until the person's next request changes them. Team Mapping covers team mappers in general.
How changes take effect
| Change | Effect |
|---|---|
| A valid provider is applied | Takes effect at once |
| A provider becomes invalid | Ready=False. Its tokens get 401, and its people keep their memberships |
disabled: true | Its tokens get 401, and everything else is kept |
issuer changes | Rejected. A provider for another issuer is a new provider |
Teams are added to allowed, or the mapper changes | Apply on each person's next request |
| The provider is deleted | Its people are retired: kept for audit, removed from every team, and never signed in as again. See the warning below |
issuer must be unique across all providers, and name across all namespaces. Of providers that share either, the one with the earliest creation timestamp wins and the others are Ready=False.
A provider is rejected when its issuer uses http, it uses HS256, maxTokenAge isn't a positive duration, audience or identity.username is missing, or a team mapper has no allowed. A provider whose CEL expression doesn't compile is stored as Ready=False with reason Invalid.
Deleting a provider doesn't delete the RoleBindings that name its users, such as <provider>:u-4821 or <provider>:*. A new provider created later with the same name gets those bindings. Delete them along with the provider.
Limitations
- Only JWT bearer tokens are supported: no opaque tokens, introspection or userinfo lookups.
- Tokens can't be revoked before they expire. Disable or delete the provider to cut off all its users at once.
- Access belongs to the person, not the token. Your application can't give the same user different access per request, such as by switching a tenant claim between tokens.
- Users of a provider can't create personal access tokens.