A Caddy plugin for OIDC authentication and authorization.
Inspired by oauth2-proxy but instead of requiring each application to be configured individually, perform authentication and authorization at the Caddy level.
- Avoids the need to configure each application individually, with N+1 oauth2 proxies per application
- Centralized access logging that includes user ID
- Easier integration with security tools like fail2ban, etc
- Anonymous access and client ip-based authorization rules
- Support for RFC9728 (OAuth 2.0 Protected Resource Metadata)
Installation can be done either via the provided Docker image (Caddy with only caddy-oidc installed)
ghcr.io/relvacode/caddy-oidc:latest
Or by building caddy with this plugin via xcaddy
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/relvacode/caddy-oidccaddy-oidc has a global and per-route oidc directive.
The global directive is used to configure the OIDC provider. An example minimum configuration is shown below.
{
oidc example {
issuer https://accounts.google.com
client_id < client_id >
}
}Each route then uses the oidc directive to configure the route using the named provider
example.com {
oidc example {
allow {
user *
}
}
reverse_proxy localhost:8080
}| Option | Description | Default |
|---|---|---|
issuer |
The OIDC issuer URL | |
client_id |
The OIDC client ID | |
tls_insecure_skip_verify |
(optional) Skip TLS certificate verification with the OIDC provider. | |
scope |
(optional) The scope to request from the OIDC provider. The openid scope is required for browser-based login to work. |
openid |
username |
(optional) The claim to use as the username. Defaults to sub. |
sub |
protected_resource_metadata |
(optional) Configure or disable RFC9728 support. | |
authenticate |
(optional) Configure authentication methods |
This module uses a plugin architecture to allow different authentication methods to be configured under the Caddy plugin
namespace http.oidc.authenticator.
When a request requires authentication, authentication methods are tried in the order they are configured. The first authenticator to return a valid session from the request is used. An expired session is ignored, and the next authenticator is tried.
The default configuration is shown below. These authenticators are not used if any authenticator is configured.
authenticate bearer
authenticate cookie {
# Inherits the default cookie configuration
}
authenticate noneBy default, any authentication information from any configured authenticator
is stripped from the request before passing it upstream.
This behavior can be disabled by adding the preserve_request option.
authenticate preserve_requestThe bearer authenticator is used to authenticate requests using a JWT bearer token.
The bearer JWT must be signed by the OIDC provider.
The cookie authenticator is used to authenticate requests using a self-signed session cookie.
Enabling session cookie authentication also enables interactive authentication through the browser via the OAuth 2.0 Authorization Code Flow.
When enabled, if the request is not authenticated, the request is made by a browser,
and there is no matching explicit allow or deny rule,
then the browser will be redirected to the OIDC provider for login.
| Option | Description |
|---|---|
name |
The name of the cookie. |
secret |
The 32 or 64 byte secret key to encrypt session cookies |
domain |
(optional) The domain of the cookie. |
path |
(optional) The path of the cookie. |
insecure |
(optional) Disable secure cookies. |
same_site |
(optional) The samesite mode of the cookie. One of lax, strict or none |
claims |
(optional) Claims to copy into the session cookie. |
redirect_url |
(optional) The URL to redirect to after authentication. If the URL is relative, the fully qualified URL is constructed using the request host and protocol. |
The default configuration is shown below. Any individual option can be overridden whilst preserving the default values for options not specified.
authenticate cookie {
name caddy
same_site lax
path /
secret {env.COOKIE_SECRET}
redirect_url /oauth2/callback
}To minimize the size of the cookie, no claims are copied into the session cookie by default.
Claims can be copied by specifying the claims option if needed for access policy rules or placeholder variables (e.g.,
for logging).
The none authenticator always passes authentication by returning an anonymous session.
Note
A none authenticator should always be the last configured authenticator.
The header authenticator authenticates a JWT token passed via an incoming HTTP request header (without any prefix).
authenticate header X-Api-KeyThe query authenticator authenticates a JWT token passed via an incoming HTTP request query parameter.
Caution
There are several security implications to using query parameters for authentication. See RFC6750 for more information.
authenticate query access_tokenRFC9728 Support (protected_resource_metadata)
Caddy OIDC supports RFC9728 (OAuth 2.0 Protected Resource Metadata) to discover the OIDC provider metadata via the
well-known URL /.well-known/oauth-protected-resource.
If the request is unauthenticated, passes at least one allow rule, and the request is not made by a browser,
then a 401 Unauthorized response is returned with a WWW-Authenticate header conforming
to WWW-Authenticate Response.
Settings can be controlled via the oidc directive protected_resource_metadata. The default behavior is to enable.
# Disable RFC9728 support.
# This makes /.well-known/oauth-protected-resource return a 404 Not Found.
protected_resource_metadata offAs a custom extension to the standard,
resource metadata can be configured to include the expected token audience (aud) claim.
If enabled, the metadata response will contain an additional audience field containing the configured client ID of the
OIDC provider configuration.
This is designed as an alternative to dynamic client registration to let another client (e.g. a CLI) use JWT Exchange with its own token with the OIDC provider and make requests to this server without prior knowledge of this server's OAuth configuration.
# Include the expected audience field in the metadata
protected_resource_metadata {
audience
}The handler directive is placed on routes to provide authentication and authorization for that route. Requests are authenticated according to the configured OIDC provider and then authorized according to access policy rules configured in the directive.
The handler directive must contain at least one allow rule.
# Allow any valid authenticated user
example.com {
oidc example {
allow {
user *
}
}
reverse_proxy localhost:8080
}If the request is unauthenticated, and there is not an explicit allow or deny rule that matches the request,
and the request is made by a browser, then the browser will be automatically redirected to the OIDC provider for
authentication.
Each access rule can be either allow or deny. Inspired by AWS IAM policies, each request must match at least one
allow rule to be authorized.
Access rules match using Caddy's regular request matchers. Additional HTTP matchers are provided for authentication-specific request matching.
Caution
Without an explicit user match in an allow policy rule, all requests will be allowed, even anonymous
requests.
If a request matches any deny rule then the request is denied, even if another allow rule matches.
# Allow any authenticated user from example.com except from steve
oidc example {
allow {
user *@example.com
}
deny {
user steve@example.com
}
}Access rules can be optionally named for logging, if matched then the rule ID will be available as the placeholder
variable {http.auth.rule}.
oidc example {
deny "DenyAnonymousAccess" {
anonymous
}
allow "AllowAnyUserReadAccess" {
method GET HEAD
user *
claim role read
}
allow "AllowAdminWriteAccess" {
method POST PUT PATCH DELETE
user *
claim role write
}
}In addition to the standard Caddy request matchers, the following matchers are provided. These matchers are only compatible with HTTP requests handled by the handler directive.
Matches the username of the authenticated user. A user match will never match an anonymous user.
# Allow any authenticated user
allow {
user *
}# Allow any authenticated user from example.com
allow {
user *@example.com
}# Allow multiple users
allow {
user steve
user bob
user john
}Matches request sessions that are anonymous. Anonymous sessions are sessions that have not been authenticated by the OIDC provider.
# Allow anonymous requests to /healthcheck
allow {
anonymous
path /healthcheck
}Matches claims in the request session.
If the session claim is an array, then the request must match at least one value in the array. Any non-string claim values are ignored and will not match.
Multiple values for a single claim directive are treated as a logical OR. If no values are specified, the matcher only checks for the claim's existence.
Note
Any claims used here must be configured in the cookie authenticator if used.
# Allow requests containing role = write
allow {
claim role write
}# Allow requests containing role = read OR role = write
allow {
claim role read write
}# Allow requests containing role = read AND role = write
allow {
claim role read
claim role write
}# Allow requests containing sub = steve@example.com AND role = read
allow {
claim sub steve@example.com
claim role read
}# Deny all requests missing the 'role' claim
deny {
not {
claim role
}
}Replacer variables are supported in both claim name and claim value.
# Allow requests containing host = {http.host}
allow {
claim host {http.host}
}Wildcard matching is also supported in claim values.
# Allow requests where the role claim starts with "read:"
allow {
claim role read:*
}Matches the authentication method used to authenticate the request.
Possible values are
none- The request is not authenticatedcookie- The request was authenticated using a session cookiebearer- The request was authenticated using a bearer JWT token
# Deny all requests using JWT bearer authentication
deny {
auth_method bearer
}When a request passes through the oidc handler, the
following placeholder variables are available:
| Placeholder | Description |
|---|---|
http.auth.user.id |
The username extracted from the username option of the global directive |
http.auth.user.anonymous |
true if the session is not authenticated otherwise false |
http.auth.method |
The authentication method of the request. One of none, cookie or bearer |
http.auth.user.claim.* |
Set for each claim provided by the matched authenticator |
http.auth.rule |
The named access policy rule that matched the request |
http.auth.result |
The acccess rule evaluation result. One of allow, implicit deny or explicit deny |
Because the oidc handler is ordered after the header handler, to set these variables in response headers, you must
use the defer option
header X-User-Claim-Email {http.auth.user.claim.email} {
defer
}- Simple values like strings, booleans, and numbers are formatted as plain values
- Null values are empty
- Objects are formatted as JSON
- Arrays are formatted using the above rules for each element, joined by commas. Nested arrays are formatted as JSON