Authorization
As soon as a user is authenticated it is allowed to use the application, secured by traefik-oidc-auth.
But you may have multiple applications and you may want to specify a more granular definition of who is allowed to access which application.
This can be achieved using the plugin's ClaimAssertions.
When a user is authenticated, an access_token and an id_token is returned by the identity provider, a session is created and these tokens are stored within that session. By default the access_token is used but you can use the other one by setting TokenValidation to IdToken.
Both tokens are Json Web Tokens (JWT) which can be decoded and made human-readable using online tools. When being decoded they're represented as a JSON-object where every property is called a Claim.
Here is an example of a decoded token:
{
"exp": 1749314471,
"iat": 1749314411,
"auth_time": 1749314411,
"jti": "onrtac:3b5c0b13-8471-4e98-a329-226d6c1d0b78",
"iss": "http://127-0-0-1.sslip.io:8000/realms/master",
"aud": [
"master-realm",
"account"
],
"sub": "c0d6f5c3-d05f-474d-bf06-f590b0a397ad",
"typ": "Bearer",
"azp": "traefik",
"sid": "758810d0-6be1-43a3-8c2b-7ae69e5eba00",
"acr": "1",
"realm_access": {
"roles": [
"create-realm",
"default-roles-master",
"offline_access",
"admin",
"uma_authorization"
]
},
"resource_access": {
"master-realm": {
"roles": [
"view-realm",
"view-identity-providers",
"query-groups"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid email profile",
"email_verified": false,
"preferred_username": "admin"
}
Every identity provider returns tokens with a default set of claims. Some of them are standardized while some are not.
Some identity providers also allow you to map different properties into your token but you need to refer to your IDP's documentation for how to do that.
You can also check out the Identity Providers section for more information and some config examples.
How it works
Every authorization rule is expressed by a claim-assertion using the AssertClaims-property.
These rules need to contain a name-selector and an optional AllOf or AnyOf quantifier.
If only the name is set without a quantifier, the rule only checks for presence of the claim without further validating it's value.
It is also possible to combine the AnyOf and AllOf quantifiers in one assertion.
Because the name is being interpreted as json path, you may need to escape some names, if they contain special characters like a colon or minus.
So instead of Name: "my:zitadel:grants", use Name: "['my:zitadel:grants']".
If the user is not authorized, all claims, contained in the token, are printed in the console if the LogLevel is set to DEBUG. This may help you to know which claims exist in your token.
When is authorization checked?
By default, AssertClaims is only evaluated once, when the user logs in and the session is created. For every subsequent request on that session, the previously computed result is simply reused - it is not re-evaluated, even if the underlying claims would now produce a different result (e.g. after a silent token refresh). Set CheckOnEveryRequest to true if you need the assertion to be re-evaluated on every request - for example when you're checking an acr/amr claim to enforce step-up authentication for specific routes, or when the required claims could change without the user going through a full login again. When using AuthorizationHeader or AuthorizationCookie, this is always treated as true, since there is no persistent session to cache the result in.
If the initial check at login fails, that unauthorized result is cached in the session, unless CheckOnEveryRequest is enabled. While the cached result is in effect, and depending on UnauthorizedBehavior, every following request to a protected route will either get a 403 again, or - with Challenge explicitly configured - bounce through the IDP once before landing on the 403 page, until the session is replaced by a new one that actually passes the check. With CheckOnEveryRequest enabled the result isn't cached but re-evaluated on every request, so the same session can start passing without re-login once its (e.g. refreshed) claims satisfy the rules.
Here is a commonly used example configuration on how to only allow admin or media users, based on the roles claim.
As you can see, the name selects the roles claim from the token and checks if the value matches AnyOf the given values.
- ⚙ Configuration
- ✅ Authorized Token
- ❌ Unauthorized Token
YAML
http:
middlewares:
oidc-auth:
plugin:
traefik-oidc-auth:
Provider:
Url: "https://your-idp.com"
ClientId: "<YourClientId>"
UsePkce: true
Scopes: ["openid", "profile", "email"]
Authorization:
AssertClaims:
- Name: roles
AnyOf: ["admin", "media"]
Docker Labels
traefik.http.middlewares.oidc-auth.traefik-oidc-auth.authorization.assertClaims[0].name=roles"
traefik.http.middlewares.oidc-auth.traefik-oidc-auth.authorization.assertClaims[0].anyOf=admin,media"
{
"exp": 1749314471,
"iat": 1749314411,
"auth_time": 1749314411,
"jti": "onrtac:3b5c0b13-8471-4e98-a329-226d6c1d0b78",
"iss": "http://127-0-0-1.sslip.io:8000/realms/master",
"aud": [
"master-realm",
"account"
],
"sub": "c0d6f5c3-d05f-474d-bf06-f590b0a397ad",
"typ": "Bearer",
"azp": "traefik",
"sid": "758810d0-6be1-43a3-8c2b-7ae69e5eba00",
"acr": "1",
"scope": "openid email profile",
"email_verified": false,
"preferred_username": "admin",
"roles": ["admin"]
}
{
"exp": 1749314471,
"iat": 1749314411,
"auth_time": 1749314411,
"jti": "onrtac:3b5c0b13-8471-4e98-a329-226d6c1d0b78",
"iss": "http://127-0-0-1.sslip.io:8000/realms/master",
"aud": [
"master-realm",
"account"
],
"sub": "c0d6f5c3-d05f-474d-bf06-f590b0a397ad",
"typ": "Bearer",
"azp": "traefik",
"sid": "758810d0-6be1-43a3-8c2b-7ae69e5eba00",
"acr": "1",
"scope": "openid email profile",
"email_verified": false,
"preferred_username": "admin",
"roles": ["user"]
}
More complicated examples
Here are some more complex examples based on the following json structure. This json doesn't represent an actual JWT but I hope you get the idea.
{
"store": {
"bicycle": {
"color": "red",
"price": 19.95
},
"book": [
{
"author": "Herman Melville",
"category": "fiction",
"isbn": "0-553-21311-3",
"price": 8.99,
"title": "Moby Dick"
},
{
"author": "J. R. R. Tolkien",
"category": "fiction",
"isbn": "0-395-19395-8",
"price": 22.99,
"title": "The Lord of the Rings"
}
],
}
}
Example: Expect array to contain a set of values
Name: store.book[*].price
AllOf: [ 22.99, 8.99 ]
This assertion would succeed as the book array contains all values specified by the AllOf quantifier
Name: store.book[*].price
AllOf: [ 22.99, 8.99, 1 ]
This assertion would fail as the book array contains no entry for which the price is 1
Example: Expect object key to be any value of a set of values
Name: store.bicycle.color
AnyOf: [ "red", "blue", "green" ]
This assertion would succeed as the store object contains a bicycle object whose color is red
Custom Error Page
If a user is authenticated but unauthorized, the response depends on the UnauthorizedBehavior setting. By default (Unauthorized), a default error page is shown and a status code 403 - Forbidden is returned. If UnauthorizedBehavior is explicitly set to Challenge, HTML requests are first redirected back to the IDP once (e.g. to allow the user to switch accounts or satisfy a step-up authentication requirement); if the user is still unauthorized afterwards, or for non-HTML requests, the same 403 error page is shown instead.
You can customize this page by providing your own HTML-file as shown below:
http:
middlewares:
oidc-auth:
plugin:
traefik-oidc-auth:
Provider:
Url: "https://your-idp.com"
ClientId: "<YourClientId>"
UsePkce: true
Scopes: ["openid", "profile", "email"]
ErrorPages:
Unauthorized:
FilePath: "/opt/traefik/error-pages/unauthorized.html"
If you're running traefik in a docker container, make sure you copy or mount this file into the container.
This html file needs to be self-contained which means all CSS and JS must be inlined or loaded from a CDN.