Skip to content

Commit fed43d7

Browse files
committed
groupware: add OIDC authentication support between Groupware backend and Stalwart
* re-implement the auth-api service to authenticate Reva tokens following the OIDC Userinfo endpoint specification * pass the context where necessary and add an authenticator interface to the JMAP HTTP driver, in order to select between master authentication (which is used when GROUPWARE_JMAP_MASTER_USERNAME and GROUPWARE_JMAP_MASTER_PASSWORD are both set) and OIDC token forwarding through bearer auth * add Stalwart directory configuration "idmoidc" which uses the OpenCloud auth-api service API (/auth/) to validate the token it received as bearer auth from the Groupware backend's JMAP client, using it as an OIDC Userinfo endpoint * implement optional additional shared secret to secure the Userinfo service, as an additional path parameter
1 parent 28ec9ad commit fed43d7

38 files changed

Lines changed: 953 additions & 646 deletions

File tree

.vscode/launch.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,15 @@
137137
"OC_SERVICE_ACCOUNT_ID": "service-account-id",
138138
"OC_SERVICE_ACCOUNT_SECRET": "service-account-secret",
139139

140-
"OC_ADD_RUN_SERVICES": "groupware",
141-
"GROUPWARE_LOG_LEVEL": "trace"
140+
"OC_ADD_RUN_SERVICES": "auth-api,groupware",
141+
"GROUPWARE_LOG_LEVEL": "trace",
142+
143+
"GROUPWARE_JMAP_MASTER_USERNAME": "",
144+
"GROUPWARE_JMAP_MASTER_PASSWORD": "admin",
145+
146+
"AUTHAPI_HTTP_ADDR": "0.0.0.0:10000",
147+
"AUTHAPI_AUTH_REQUIRE_SHARED_SECRET": "true",
148+
"AUTHAPI_AUTH_SHARED_SECRETS": "stalwart=maethaR9eiXaiph8ahn8ohH6dahPiequ;unused=eeyaigh6hae1zo5ahGeete6oohaiquei",
142149
}
143150
},
144151
{

devtools/deployments/opencloud_full/.env

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,11 +311,11 @@ KEYCLOAK_ADMIN_PASSWORD=
311311
# Domain of Stalwart
312312
# Defaults to "stalwart.opencloud.test"
313313
STALWART_DOMAIN=
314-
315314
# LDAP configuration to use for Stalwart:
316315
# Can either be either
317-
# - idmldap: for the built-in IDP/IDM
318-
# - ldap: when using KeyCloak and OpenLDAP
316+
# - idmldap: for the built-in IDP/IDM, using Master Authentication between Groupware and Stalwart, and LDAP in Stalwart
317+
# - idmoidc: built-in IDP/IDM, using OIDC Userinfo between Groupware and Stalwart
318+
# - ldap: when using KeyCloak and OpenLDAP, with Master Authentication between Groupware and Stalwart, and LDAP in Stalwart
319319
STALWART_AUTH_DIRECTORY=idmldap
320320

321321
## IMPORTANT ##
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
authentication.fallback-admin.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
2+
authentication.fallback-admin.user = "mailadmin"
3+
authentication.master.secret = "$6$4qPYDVhaUHkKcY7s$bB6qhcukb9oFNYRIvaDZgbwxrMa2RvF5dumCjkBFdX19lSNqrgKltf3aPrFMuQQKkZpK2YNuQ83hB1B3NiWzj."
4+
authentication.master.user = "master"
5+
directory.oidc.cache.size = 1048576
6+
directory.oidc.cache.ttl.negative = "10m"
7+
directory.oidc.cache.ttl.positive = "1h"
8+
directory.oidc.endpoint.method = "userinfo"
9+
directory.oidc.endpoint.url = "http://172.17.0.1:10000/auth/maethaR9eiXaiph8ahn8ohH6dahPiequ"
10+
directory.oidc.fields.email = "email"
11+
directory.oidc.fields.full-name = "name"
12+
directory.oidc.fields.username = "preferred_username"
13+
directory.oidc.timeout = "15s"
14+
directory.oidc.type = "oidc"
15+
http.allowed-endpoint = 200
16+
http.hsts = true
17+
http.permissive-cors = false
18+
http.url = "'https://' + config_get('server.hostname')"
19+
http.use-x-forwarded = true
20+
metrics.prometheus.auth.secret = "secret"
21+
metrics.prometheus.auth.username = "metrics"
22+
metrics.prometheus.enable = true
23+
server.listener.http.bind = "0.0.0.0:8080"
24+
server.listener.http.protocol = "http"
25+
server.listener.https.bind = "0.0.0.0:443"
26+
server.listener.https.protocol = "http"
27+
server.listener.https.tls.implicit = true
28+
server.listener.imap.bind = "0.0.0.0:143"
29+
server.listener.imap.protocol = "imap"
30+
server.listener.imaptls.bind = "0.0.0.0:993"
31+
server.listener.imaptls.protocol = "imap"
32+
server.listener.imaptls.tls.implicit = true
33+
server.listener.pop3.bind = "0.0.0.0:110"
34+
server.listener.pop3.protocol = "pop3"
35+
server.listener.pop3s.bind = "0.0.0.0:995"
36+
server.listener.pop3s.protocol = "pop3"
37+
server.listener.pop3s.tls.implicit = true
38+
server.listener.sieve.bind = "0.0.0.0:4190"
39+
server.listener.sieve.protocol = "managesieve"
40+
server.listener.smtp.bind = "0.0.0.0:25"
41+
server.listener.smtp.protocol = "smtp"
42+
server.listener.submission.bind = "0.0.0.0:587"
43+
server.listener.submission.protocol = "smtp"
44+
server.listener.submissions.bind = "0.0.0.0:465"
45+
server.listener.submissions.protocol = "smtp"
46+
server.listener.submissions.tls.implicit = true
47+
server.max-connections = 8192
48+
server.socket.backlog = 1024
49+
server.socket.nodelay = true
50+
server.socket.reuse-addr = true
51+
server.socket.reuse-port = true
52+
sharing.allow-directory-query = false
53+
storage.blob = "rocksdb"
54+
storage.data = "rocksdb"
55+
storage.directory = "oidc"
56+
storage.fts = "rocksdb"
57+
storage.lookup = "rocksdb"
58+
store.rocksdb.compression = "lz4"
59+
store.rocksdb.path = "/opt/stalwart/data"
60+
store.rocksdb.type = "rocksdb"
61+
tracer.console.ansi = true
62+
tracer.console.buffered = true
63+
tracer.console.enable = true
64+
tracer.console.level = "trace"
65+
tracer.console.lossy = false
66+
tracer.console.multiline = false
67+
tracer.console.type = "stdout"

devtools/deployments/opencloud_full/opencloud.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ services:
6464
GROUPS_LDAP_BIND_PASSWORD: "admin"
6565
IDM_LDAPS_ADDR: 0.0.0.0:9235
6666
GROUPWARE_JMAP_BASE_URL: https://${STALWART_DOMAIN:-stalwart.opencloud.test}
67+
GROUPWARE_JMAP_MASTER_USERNAME: "master"
68+
GROUPWARE_JMAP_MASTER_PASSWORD: "admin"
6769
volumes:
6870
- ./config/opencloud/app-registry.yaml:/etc/opencloud/app-registry.yaml
6971
- ./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml

go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ require (
77
github.com/CiscoM31/godata v1.0.11
88
github.com/KimMachineGun/automemlimit v0.7.5
99
github.com/Masterminds/semver v1.5.0
10-
github.com/MicahParks/jwkset v0.8.0
1110
github.com/MicahParks/keyfunc/v2 v2.1.0
12-
github.com/MicahParks/keyfunc/v3 v3.3.11
1311
github.com/Nerzal/gocloak/v13 v13.9.0
1412
github.com/ProtonMail/go-crypto v1.1.5
1513
github.com/bbalet/stopwords v1.0.0
@@ -103,7 +101,6 @@ require (
103101
github.com/tidwall/sjson v1.2.5
104102
github.com/tus/tusd/v2 v2.8.0
105103
github.com/unrolled/secure v1.16.0
106-
github.com/urfave/cli/v2 v2.27.7
107104
github.com/vmihailenco/msgpack/v5 v5.4.1
108105
github.com/wk8/go-ordered-map v1.0.0
109106
github.com/xhit/go-simple-mail/v2 v2.16.0
@@ -386,6 +383,7 @@ require (
386383
github.com/tklauser/numcpus v0.8.0 // indirect
387384
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
388385
github.com/trustelem/zxcvbn v1.0.1 // indirect
386+
github.com/urfave/cli/v2 v2.27.7 // indirect
389387
github.com/valyala/fastjson v1.6.4 // indirect
390388
github.com/vektah/gqlparser/v2 v2.5.31 // indirect
391389
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect

go.sum

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,8 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
8282
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
8383
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
8484
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
85-
github.com/MicahParks/jwkset v0.8.0 h1:jHtclI38Gibmu17XMI6+6/UB59srp58pQVxePHRK5o8=
86-
github.com/MicahParks/jwkset v0.8.0/go.mod h1:fVrj6TmG1aKlJEeceAz7JsXGTXEn72zP1px3us53JrA=
8785
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
8886
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
89-
github.com/MicahParks/keyfunc/v3 v3.3.11 h1:eA6wNltwdSRX2gtpTwZseBCC9nGeBkI9KxHtTyZbDbo=
90-
github.com/MicahParks/keyfunc/v3 v3.3.11/go.mod h1:y6Ed3dMgNKTcpxbaQHD8mmrYDUZWJAxteddA6OQj+ag=
9187
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
9288
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
9389
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=

pkg/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ type Config struct {
8989
AppProvider *appProvider.Config `yaml:"app_provider"`
9090
AppRegistry *appRegistry.Config `yaml:"app_registry"`
9191
Audit *audit.Config `yaml:"audit"`
92+
AuthApi *authapi.Config `yaml:"auth_api"`
9293
AuthApp *authapp.Config `yaml:"auth_app"`
9394
AuthBasic *authbasic.Config `yaml:"auth_basic"`
9495
AuthBearer *authbearer.Config `yaml:"auth_bearer"`
@@ -126,5 +127,4 @@ type Config struct {
126127
WebDAV *webdav.Config `yaml:"webdav"`
127128
Webfinger *webfinger.Config `yaml:"webfinger"`
128129
Search *search.Config `yaml:"search"`
129-
AuthApi *authapi.Config `yaml:"authapi"`
130130
}

pkg/jmap/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ type WsClient interface {
2323
}
2424

2525
type WsClientFactory interface {
26-
EnableNotifications(pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error)
26+
EnableNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error)
2727
io.Closer
2828
}
2929

3030
type SessionClient interface {
31-
GetSession(baseurl *url.URL, username string, logger *log.Logger) (SessionResponse, Error)
31+
GetSession(ctx context.Context, baseurl *url.URL, username string, logger *log.Logger) (SessionResponse, Error)
3232
io.Closer
3333
}
3434

pkg/jmap/api_ws.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package jmap
22

3-
func (j *Client) EnablePushNotifications(pushState State, sessionProvider func() (*Session, error)) (WsClient, error) {
4-
return j.ws.EnableNotifications(pushState, sessionProvider, j)
3+
import "context"
4+
5+
func (j *Client) EnablePushNotifications(ctx context.Context, pushState State, sessionProvider func() (*Session, error)) (WsClient, error) {
6+
return j.ws.EnableNotifications(ctx, pushState, sessionProvider, j)
57
}
68

79
func (j *Client) AddWsPushListener(listener WsPushListener) {

pkg/jmap/client.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package jmap
22

33
import (
4+
"context"
45
"errors"
56
"io"
67
"net/url"
@@ -55,8 +56,8 @@ func (j *Client) OnNotification(username string, stateChange StateChange) {
5556
}
5657

5758
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
58-
func (j *Client) FetchSession(sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
59-
wk, err := j.session.GetSession(sessionUrl, username, logger)
59+
func (j *Client) FetchSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
60+
wk, err := j.session.GetSession(ctx, sessionUrl, username, logger)
6061
if err != nil {
6162
return Session{}, err
6263
}

0 commit comments

Comments
 (0)