Skip to content

Commit 480bea9

Browse files
committed
auth/oauth: Implement OAuth Bearer authentication
1 parent f2c8c80 commit 480bea9

23 files changed

Lines changed: 1361 additions & 45 deletions

File tree

.mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ nav:
7272
- reference/auth/plain_separate.md
7373
- reference/auth/netauth.md
7474
- reference/auth/tls.md
75+
- reference/auth/oauth.md
7576
- reference/config-syntax.md
7677
- Integration with software:
7778
- third-party/dovecot.md

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.23-alpine AS build-env
1+
FROM golang:1.25-alpine AS build-env
22

33
ARG ADDITIONAL_BUILD_TAGS=""
44

docs/reference/auth/oauth.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# OAuth Bearer Token Authentication
2+
3+
`auth.oauth` implements OAuth Bearer Token authentication as defined
4+
in [RFC 7628][rfc7628] and [RFC 6750][rfc6750].
5+
6+
It is not compatible with non-standard XOAUTH2 implementations, such as those
7+
used by Google and Microsoft.
8+
9+
The provided token can be validated either by the server directly by decoding
10+
JWT, or by making an introspection request ([RFC 7662][rfc7662]) to the
11+
authorization server to validate the token and retrieve associated metadata.
12+
13+
## Configuration directives
14+
15+
```
16+
auth.oauth [<url>] {
17+
[debug yes | no]
18+
[introspection auth | get | post | local]
19+
[introspection_url <url>]
20+
[http_header <key> <value>]
21+
[http_header <key> <value> ...]
22+
[introspection_timeout 5s]
23+
[scopes <scope...>]
24+
[username_attribute <attribute>]
25+
[active_attribute active]
26+
[active_value true]
27+
[jwt_key_id_template <template>]
28+
[jwt_key_table <table>]
29+
[jwt_valid_methods <method...>]
30+
[jwt_issuers <issuer...>]
31+
[jwt_expiry_leeway <duration>]
32+
[jwt_audience <table>]
33+
}
34+
```
35+
36+
### debug _yes|no_
37+
Default: no
38+
39+
Enables debug logging.
40+
41+
---
42+
43+
### introspection _auth|get|post|local_
44+
Default: auth
45+
46+
Defines the method used to validate the token. The following options are available:
47+
- `auth`: Add token to the `Authorization` header and make a request to the introspection endpoint.
48+
- `get`: Make a GET request to the introspection endpoint with the token appended to the URL.
49+
- `post`: Make a POST request to the introspection endpoint with the token as `token` form-data field.
50+
- `local`: Validate the token locally by decoding it as a JWT.
51+
52+
---
53+
54+
### introspection_url _<url>_
55+
Default: (none)
56+
57+
The URL of the introspection endpoint to validate the token. Required if `introspection_method` is set
58+
to `auth`, `get`, or `post`.
59+
60+
---
61+
62+
### http_header _<key> <value>_
63+
Default: (none)
64+
65+
Additional HTTP headers to include in the introspection request. This can be used to provide server
66+
credentials or other necessary information to the authorization server.
67+
68+
---
69+
70+
### introspection_timeout _<duration>_
71+
Default: 5s
72+
73+
The timeout for the introspection request. If the request takes longer than this duration, authentication
74+
will fail.
75+
76+
---
77+
78+
### scopes _<scope>_
79+
Default: (none)
80+
81+
The required scope(s) for the token. If specified, the token must include all scopes to be
82+
considered valid.
83+
84+
---
85+
86+
### username_attribute _<attribute>_
87+
Default: email
88+
89+
The attribute in the token response that contains the username. This is used to set the username for the
90+
authenticated user.
91+
92+
If attribute is a list of strings, the first non-empty value will be used as the username.
93+
94+
If client requests a specific username (e.g. via SASL authorization identity), it must match
95+
the username extracted from the token for authentication to succeed. If attribute is a
96+
list then the client requested username must match at least one of the values in the list.
97+
98+
---
99+
100+
### active_attribute _<attribute>_
101+
Default: not specified
102+
103+
The attribute in the token response that indicates whether the token is active. This is used to determine if the
104+
token is valid. If not specified, the token is considered active if the introspection request returns
105+
a successful response.
106+
107+
---
108+
109+
### active_value _<value>_
110+
Default: not specified
111+
112+
The value of the `active_attribute` that indicates the token is active. This is used to determine if the token is
113+
valid. If not specified, the token is considered active if the `active_attribute` is present and has a truthy value
114+
(not empty string, non-zero number, or boolean true).
115+
116+
---
117+
118+
### jwt_key_id_template _<template>_
119+
Default: `{kid}`
120+
121+
The template used to determine the key for `jwt_key_table` lookup. The template can include placeholders
122+
for JWT header and body fields: azp, kid, alg. If azp or kid is missing from the token, the placeholder will be
123+
replaced with `default`.
124+
125+
---
126+
127+
### jwt_key_table _<table>_
128+
Default: (none)
129+
130+
The table to use for looking up the key to validate JWT tokens. The lookup key is determined by applying
131+
the `jwt_key_id_template` to the token's header and body fields.
132+
133+
Most `storage.blob` (including file and s3) can be used here as well as they
134+
all also implement the `table` interface.
135+
136+
---
137+
138+
### jwt_valid_methods _<method>_
139+
Default: all supported methods.
140+
141+
The allowed signing algorithms for JWT tokens. If specified, the token's `alg` header field must match
142+
one of these values to be considered valid.
143+
144+
It is not possible to enable `none` algorithm for JWT tokens.
145+
146+
---
147+
148+
### jwt_issuers _<issuer>_
149+
Default: (none)
150+
151+
If specified, the token's `iss` claim must match one of these values to be considered valid.
152+
153+
---
154+
155+
### jwt_expiry_leeway _<duration>_
156+
Default: 30s
157+
158+
The leeway to apply when validating the token's `exp` claim. This allows for some clock skew between the
159+
token issuer and the server. The token is considered valid if the current time is
160+
before `exp` + `jwt_expiry_leeway`.
161+
162+
---
163+
164+
### jwt_audience _<table>_
165+
Default: (none)
166+
167+
If specified, the token's `aud` claim must match one of the values in this table to be considered
168+
valid.
169+
170+
[rfc7628]: https://tools.ietf.org/html/rfc7628
171+
[rfc6750]: https://tools.ietf.org/html/rfc6750
172+
[rfc7662]: https://tools.ietf.org/html/rfc7662

docs/reference/blob/fs.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,29 @@
22

33
This module stores message bodies in a file system directory.
44

5+
Module does not escape path separators so "a/b" will be stored in "a" subdirectory
6+
that must exist already. ".." is allowed as long as it does not escape the
7+
configured root directory.
8+
9+
Module supports both `storage.blob` and `table` interfaces and can be used
10+
as a metadata store this way.
11+
12+
## Configuration directives
13+
14+
```
15+
storage.blob.fs <directory>
16+
table.fs <directory>
17+
```
18+
519
```
620
storage.blob.fs {
721
root <directory>
822
}
9-
```
1023
24+
table.fs {
25+
root <directory>
26+
}
1127
```
12-
storage.blob.fs <directory>
13-
```
14-
15-
## Configuration directives
1628

1729
### root _path_
1830
Default: not set

docs/reference/blob/s3.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
storage.blob.s3 module stores messages bodies in a bucket on S3-compatible storage.
44

5+
Module supports both `storage.blob` and `table` interfaces and can be used
6+
as a metadata store this way.
7+
58
```
69
storage.blob.s3 {
710
endpoint play.min.io
@@ -90,7 +93,7 @@ Credentials to use for accessing the S3 Bucket.
9093

9194
Credential Types:
9295

93-
- `access_key`: use AWS access key and secret access key
96+
- `access_key`: use AWS access key and secret access key
9497
- `file_minio`: use credentials for Minio present at ~/.mc/config.json
9598
- `file_aws`: use credentials for AWS S3 present at ~/.aws/credentials
9699
- `iam`: use AWS IAM instance profile for credentials.

docs/tutorials/building-from-source.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ You need C toolchain, Go toolchain and Make:
66

77
On Debian-based system this should work:
88
```
9-
apt-get install golang-1.23 gcc libc6-dev make
9+
apt-get install golang-1.25 gcc libc6-dev make
1010
```
1111

1212
Additionally, if you want manual pages, you should also have scdoc installed.
@@ -23,8 +23,8 @@ toolchain to build maddy. It is necessary to run commands below only
2323
if you have `go` command version older than 1.21.
2424

2525
```
26-
wget "https://go.dev/dl/go1.23.5.linux-amd64.tar.gz"
27-
tar xf "go1.23.5.linux-amd64.tar.gz"
26+
wget "https://go.dev/dl/go1.25.10linux-amd64.tar.gz"
27+
tar xf "go1.25.10.linux-amd64.tar.gz"
2828
export GOROOT="$PWD/go"
2929
export PATH="$PWD/go/bin:$PATH"
3030
```
@@ -39,7 +39,7 @@ $ cd maddy
3939

4040
2. Select the appropriate version to build:
4141
```
42-
$ git checkout v0.8.0 # a specific release
42+
$ git checkout v0.9.4 # a specific release
4343
$ git checkout master # next bugfix release
4444
$ git checkout dev # next feature release
4545
```

framework/module/auth.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ type AuthContext struct {
4646
ProxiedTLS *ProxiedTLSContext // populated instead of TLS if TLS is terminated by upstream and TLS info is available
4747
}
4848

49+
type BearerTokenError struct {
50+
Err error
51+
Status string
52+
Schemes string // probably, "bearer"
53+
Scope string
54+
OIDCConfigURL string
55+
}
56+
57+
func (e BearerTokenError) Error() string { return e.Err.Error() }
58+
func (e BearerTokenError) Unwrap() error { return e.Err }
59+
60+
type BearerTokenAuth interface {
61+
AuthBearerToken(ctx *AuthContext, username, token string) (identity string, err error)
62+
}
63+
4964
type ExternalAuth interface {
5065
AuthExternal(ctx *AuthContext, requestedIdentity string) (finalIdentity string, err error)
5166
}

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
module github.com/foxcpp/maddy
22

3-
go 1.23.1
4-
5-
toolchain go1.23.5
3+
go 1.25
64

75
require (
86
blitiri.com.ar/go/spf v1.5.1
@@ -27,8 +25,11 @@ require (
2725
github.com/foxcpp/go-mtasts v0.0.0-20240130093538-1438da2e5932
2826
github.com/go-ldap/ldap/v3 v3.4.10
2927
github.com/go-sql-driver/mysql v1.8.1
28+
github.com/golang-jwt/jwt/v5 v5.3.1
3029
github.com/google/uuid v1.6.0
3130
github.com/hashicorp/go-hclog v1.6.3
31+
github.com/hashicorp/golang-lru/v2 v2.0.7
32+
github.com/jimlambrt/gldap v0.1.14
3233
github.com/johannesboyne/gofakes3 v0.0.0-20210704111953-6a9f95c2941c
3334
github.com/lib/pq v1.10.9
3435
github.com/libdns/acmedns v0.2.0
@@ -107,7 +108,6 @@ require (
107108
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
108109
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
109110
github.com/hashicorp/hcl v1.0.0 // indirect
110-
github.com/jimlambrt/gldap v0.1.14 // indirect
111111
github.com/jmespath/go-jmespath v0.4.0 // indirect
112112
github.com/josharian/intern v1.0.0 // indirect
113113
github.com/klauspost/compress v1.17.11 // indirect

go.sum

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuh
237237
github.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI=
238238
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
239239
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
240+
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
241+
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
240242
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
241243
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
242244
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -304,12 +306,6 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
304306
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
305307
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
306308
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
307-
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf h1:rmBPY5fryjp9zLQYsUmQqqgsYq7qeVfrjtr96Tf9vD8=
308-
github.com/foxcpp/go-dovecot-sasl v0.0.0-20200522223722-c4699d7a24bf/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=
309-
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260303144336-f7632c6ec0ba h1:yxQhqX9RQCvECZKBtqwCZoKy/6CLaozDZeWH9Lvndy0=
310-
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260303144336-f7632c6ec0ba/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=
311-
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260511115826-4abd5f9faccb h1:EZIXlFawFxTweKz1sYPtDpto0g1h8bfep/U0xGlkxF0=
312-
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260511115826-4abd5f9faccb/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=
313309
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260511123641-a448d7c72dc6 h1:urtEnd//IMEws/prECei6cmUJiynEkUs5q6flZ0Py4g=
314310
github.com/foxcpp/go-dovecot-sasl v0.0.0-20260511123641-a448d7c72dc6/go.mod h1:5yZUmwr851vgjyAfN7OEfnrmKOh/qLA5dbGelXYsu1E=
315311
github.com/foxcpp/go-imap v1.0.0-beta.1.0.20220623182312-df940c324887 h1:qUoaaHyrRpQw85ru6VQcC6JowdhrWl7lSbI1zRX1FTM=
@@ -322,10 +318,6 @@ github.com/foxcpp/go-imap-mess v0.0.0-20230108134257-b7ec3a649613 h1:fw9OWfPxP1C
322318
github.com/foxcpp/go-imap-mess v0.0.0-20230108134257-b7ec3a649613/go.mod h1:P/O/qz4gaVkefzJ40BUtN/ZzBnaEg0YYe1no/SMp7Aw=
323319
github.com/foxcpp/go-imap-namespace v0.0.0-20200802091432-08496dd8e0ed h1:1Jo7geyvunrPSjL6F6D9EcXoNApS5v3LQaro7aUNPnE=
324320
github.com/foxcpp/go-imap-namespace v0.0.0-20200802091432-08496dd8e0ed/go.mod h1:Shows1vmkBWO40ChOClaUe6DUnZrsP1UPAuoWzIUdgQ=
325-
github.com/foxcpp/go-imap-sql v0.5.1-0.20250124140007-8da5567429d5 h1:jMxhw9qmwqg70qfMDWq0ImRHAduQjkTZOC9vBs5t2ug=
326-
github.com/foxcpp/go-imap-sql v0.5.1-0.20250124140007-8da5567429d5/go.mod h1:LMlfyNkVs7v2zE6OVeGe9qWPmKFdXDmLNddPLodPVIw=
327-
github.com/foxcpp/go-imap-sql v0.5.1-0.20260412133145-20097edd35ec h1:Jm71K60qrrnyISeLXMYKzSZe0RVco+aO/RJugJvafIM=
328-
github.com/foxcpp/go-imap-sql v0.5.1-0.20260412133145-20097edd35ec/go.mod h1:LMlfyNkVs7v2zE6OVeGe9qWPmKFdXDmLNddPLodPVIw=
329321
github.com/foxcpp/go-imap-sql v0.5.1-0.20260412184517-b5e85e90f14d h1:oiq5MLSSqd3sl4VNHKTlrwszWTHIx8+x8y/olInMJRo=
330322
github.com/foxcpp/go-imap-sql v0.5.1-0.20260412184517-b5e85e90f14d/go.mod h1:LMlfyNkVs7v2zE6OVeGe9qWPmKFdXDmLNddPLodPVIw=
331323
github.com/foxcpp/go-mockdns v0.0.0-20191216195825-5eabd8dbfe1f/go.mod h1:tPg4cp4nseejPd+UKxtCVQ2hUxNTZ7qQZJa7CLriIeo=
@@ -362,6 +354,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
362354
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
363355
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
364356
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
357+
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
358+
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
365359
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
366360
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
367361
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -480,6 +474,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
480474
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
481475
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
482476
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
477+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
478+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
483479
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
484480
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
485481
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=

0 commit comments

Comments
 (0)