Skip to content

Commit b5ba568

Browse files
committed
Initial release: universal Roundcube OIDC mail SSO plugin
0 parents  commit b5ba568

18 files changed

Lines changed: 2950 additions & 0 deletions

.env.example

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Roundcube web
2+
ROUNDCUBE_PORT=8080
3+
4+
# DB
5+
DB_ROOT_PASSWORD=change-root-pass
6+
DB_NAME=roundcube
7+
DB_USER=roundcube
8+
DB_PASSWORD=change-db-pass
9+
10+
# Default mail URIs for initial Roundcube bootstrap (plugin overrides per user)
11+
DEFAULT_IMAP_URI=ssl://imap.example.com
12+
DEFAULT_IMAP_PORT=993
13+
DEFAULT_SMTP_URI=tls://smtp.example.com
14+
DEFAULT_SMTP_PORT=587
15+
16+
# OIDC provider
17+
OIDC_ISSUER=https://oidc.example.com
18+
OIDC_CLIENT_ID=roundcube
19+
OIDC_CLIENT_SECRET=
20+
OIDC_REDIRECT_URI=http://localhost:8080/plugins/universal_oidc_mail_sso/callback_bridge.php
21+
OIDC_POST_LOGOUT_REDIRECT_URI=http://localhost:8080/
22+
ALLOWED_EMAIL_DOMAIN=
23+
24+
# 32 random bytes base64; generate with:
25+
# openssl rand -base64 32
26+
RCUBE_MAILBOX_KEY=REPLACE_ME_BASE64_32_BYTES
27+
28+
# Plugin behavior
29+
FORCE_HTTPS=false
30+
DISABLE_PASSWORD_LOGIN=true
31+
ADMIN_GROUP_NAME=webmail_admin
32+
33+
# Generic defaults shown in connect form
34+
DEFAULT_IMAP_HOST=imap.example.com
35+
DEFAULT_IMAP_PORT=993
36+
DEFAULT_IMAP_SECURITY=ssl
37+
DEFAULT_SMTP_HOST=smtp.example.com
38+
DEFAULT_SMTP_PORT=587
39+
DEFAULT_SMTP_SECURITY=tls
40+
DEFAULT_SMTP_AUTH=1

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.env
2+
.env.local
3+
.DS_Store
4+
*.log
5+
vendor/
6+
node_modules/
7+
tmp/
8+
temp/

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# universal-roundcube-oidc-mail-sso
2+
3+
Universal Roundcube plugin for:
4+
- OIDC SSO (Auth Code + PKCE)
5+
- One-time IMAP/SMTP credential setup
6+
- Encrypted credential storage
7+
- Automatic mailbox login
8+
- Admin dashboard (policy + user mapping controls)
9+
10+
## Repository layout
11+
12+
- `plugins/universal_oidc_mail_sso/` plugin source
13+
- `docker-compose.yml` local Roundcube + MariaDB stack
14+
- `.env.example` example environment variables (safe template)
15+
16+
## Quick start
17+
18+
```sh
19+
cp .env.example .env
20+
# edit .env with your OIDC and key values
21+
docker compose up -d
22+
docker compose exec -T db mariadb -u"$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" < plugins/universal_oidc_mail_sso/SQL/mysql.local.sql
23+
```
24+
25+
Roundcube:
26+
- `http://localhost:8080`
27+
28+
OIDC callback URL:
29+
- `http://localhost:8080/plugins/universal_oidc_mail_sso/callback_bridge.php`
30+
31+
Admin URL:
32+
- `http://localhost:8080/?_task=settings&_action=plugin.universal_oidc_mail_sso_admin`
33+
34+
## Security note
35+
36+
Do not commit `.env` or any generated secret files. This repo is configured to ignore `.env`.

docker-compose.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
services:
2+
db:
3+
image: mariadb:11
4+
container_name: rc_oidc_db
5+
restart: unless-stopped
6+
environment:
7+
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
8+
MYSQL_DATABASE: ${DB_NAME}
9+
MYSQL_USER: ${DB_USER}
10+
MYSQL_PASSWORD: ${DB_PASSWORD}
11+
volumes:
12+
- rc_db_data:/var/lib/mysql
13+
healthcheck:
14+
test: ["CMD", "mariadb-admin", "ping", "-h", "127.0.0.1", "-u${DB_USER}", "-p${DB_PASSWORD}"]
15+
interval: 10s
16+
timeout: 5s
17+
retries: 10
18+
19+
roundcube:
20+
image: roundcube/roundcubemail:latest
21+
container_name: rc_oidc_app
22+
restart: unless-stopped
23+
depends_on:
24+
db:
25+
condition: service_healthy
26+
ports:
27+
- "${ROUNDCUBE_PORT}:80"
28+
environment:
29+
ROUNDCUBEMAIL_DEFAULT_HOST: ${DEFAULT_IMAP_URI}
30+
ROUNDCUBEMAIL_DEFAULT_PORT: ${DEFAULT_IMAP_PORT}
31+
ROUNDCUBEMAIL_SMTP_SERVER: ${DEFAULT_SMTP_URI}
32+
ROUNDCUBEMAIL_SMTP_PORT: ${DEFAULT_SMTP_PORT}
33+
ROUNDCUBEMAIL_PLUGINS: "universal_oidc_mail_sso"
34+
ROUNDCUBEMAIL_DB_TYPE: mysql
35+
ROUNDCUBEMAIL_DB_HOST: db
36+
ROUNDCUBEMAIL_DB_PORT: 3306
37+
ROUNDCUBEMAIL_DB_NAME: ${DB_NAME}
38+
ROUNDCUBEMAIL_DB_USER: ${DB_USER}
39+
ROUNDCUBEMAIL_DB_PASSWORD: ${DB_PASSWORD}
40+
OIDC_ISSUER: ${OIDC_ISSUER}
41+
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
42+
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
43+
OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI}
44+
OIDC_POST_LOGOUT_REDIRECT_URI: ${OIDC_POST_LOGOUT_REDIRECT_URI}
45+
OIDC_SCOPES: ${OIDC_SCOPES}
46+
ALLOWED_EMAIL_DOMAIN: ${ALLOWED_EMAIL_DOMAIN}
47+
ADMIN_GROUP_NAME: ${ADMIN_GROUP_NAME}
48+
ADMIN_EMAILS: ${ADMIN_EMAILS}
49+
RCUBE_MAILBOX_KEY: ${RCUBE_MAILBOX_KEY}
50+
FORCE_HTTPS: ${FORCE_HTTPS}
51+
DISABLE_PASSWORD_LOGIN: ${DISABLE_PASSWORD_LOGIN}
52+
DEFAULT_IMAP_HOST: ${DEFAULT_IMAP_HOST}
53+
DEFAULT_IMAP_PORT: ${DEFAULT_IMAP_PORT}
54+
DEFAULT_IMAP_SECURITY: ${DEFAULT_IMAP_SECURITY}
55+
DEFAULT_SMTP_HOST: ${DEFAULT_SMTP_HOST}
56+
DEFAULT_SMTP_PORT: ${DEFAULT_SMTP_PORT}
57+
DEFAULT_SMTP_SECURITY: ${DEFAULT_SMTP_SECURITY}
58+
DEFAULT_SMTP_AUTH: ${DEFAULT_SMTP_AUTH}
59+
volumes:
60+
- ./plugins/universal_oidc_mail_sso:/var/www/html/plugins/universal_oidc_mail_sso
61+
62+
volumes:
63+
rc_db_data:
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# universal_oidc_mail_sso
2+
3+
Roundcube plugin for:
4+
- OIDC Provider OIDC SSO (Authorization Code + PKCE)
5+
- One-time mailbox credential setup
6+
- Encrypted mailbox credentials at rest
7+
- Automatic IMAP/SMTP authentication after OIDC login
8+
9+
## Features
10+
11+
- OIDC discovery from `OIDC_ISSUER`
12+
- PKCE (`S256`) + `state` + `nonce`
13+
- ID token validation via JWKS (`RS256`)
14+
- Required claim mapping:
15+
- stable user id: `sub`
16+
- email/login id: `email` (required)
17+
- Optional domain allow-list check (`ALLOWED_EMAIL_DOMAIN`; empty or `*` allows all)
18+
- One-time connect screen storing mailbox password/app-password encrypted in DB
19+
- Encryption backends:
20+
- preferred: `libsodium` (`sodium_secretbox`)
21+
- fallback: OpenSSL `aes-256-gcm`
22+
- Auto-injection of IMAP/SMTP credentials in Roundcube hooks (`authenticate`, `storage_connect`, `smtp_connect`)
23+
- Optional SMTP auth toggle for servers that allow relay without SMTP AUTH
24+
- Supports `ssl`, `tls`, `starttls`, and `none` security modes
25+
- Built-in IMAP/SMTP credential test before saving profile
26+
- Audit log table for authentication/provisioning events
27+
- Admin dashboard for mapped accounts + audit history (restricted by OIDC group)
28+
- Session-based setup form throttle + CSRF protection
29+
- Reverse proxy aware redirect URI construction (`X-Forwarded-Proto`, `X-Forwarded-Host`, `FORCE_HTTPS`)
30+
31+
## Plugin layout
32+
33+
- `universal_oidc_mail_sso.php` main plugin
34+
- `lib/OidcClient.php` OIDC discovery/token flow
35+
- `lib/JwksValidator.php` JWT + JWKS signature/claim validation
36+
- `lib/Crypto.php` encryption wrapper and key validation
37+
- `lib/Storage.php` DB access for identity + mailbox records
38+
- `skins/elastic/templates/connect_mailbox.html` one-time mailbox setup view
39+
- `SQL/mysql.initial.sql` schema
40+
- `misc/crypto_tool.php` CLI helper for key/encryption checks
41+
- `misc/run_tests.php` minimal tests
42+
43+
## Database schema
44+
45+
Apply `SQL/mysql.initial.sql` to your Roundcube DB (it uses `{$prefix}` placeholders for Roundcube table prefix handling).
46+
47+
Tables:
48+
- `oidc_mail_sso_oidc_user` minimal mapping (`oidc_sub`, `email`, `last_login_at`, optional `user_id`)
49+
- `oidc_mail_sso_mailbox` encrypted mailbox config and app-password
50+
- `oidc_mail_sso_audit_log` structured event/audit history
51+
52+
## Required env vars
53+
54+
- `OIDC_ISSUER` (e.g. `https://oidc.example.com`)
55+
- `OIDC_CLIENT_ID`
56+
- `RCUBE_MAILBOX_KEY` (base64 of exactly 32 random bytes)
57+
58+
Optional:
59+
- `OIDC_CLIENT_SECRET` (for confidential clients)
60+
- `OIDC_REDIRECT_URI` (if omitted, plugin computes from request/proxy headers)
61+
- `OIDC_POST_LOGOUT_REDIRECT_URI`
62+
- `ALLOWED_EMAIL_DOMAIN` (optional; empty or `*` allows all domains)
63+
- `FORCE_HTTPS=true`
64+
- `DISABLE_PASSWORD_LOGIN=true`
65+
- `ADMIN_GROUP_NAME=webmail_admin`
66+
67+
Generic defaults (override as needed):
68+
- `DEFAULT_IMAP_HOST=imap.example.com`
69+
- `DEFAULT_IMAP_PORT=993`
70+
- `DEFAULT_IMAP_SECURITY=ssl`
71+
- `DEFAULT_SMTP_HOST=smtp.example.com`
72+
- `DEFAULT_SMTP_PORT=587`
73+
- `DEFAULT_SMTP_SECURITY=tls`
74+
- `DEFAULT_SMTP_AUTH=1`
75+
76+
Generate key:
77+
78+
```sh
79+
php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'
80+
```
81+
82+
## Docker (roundcube/roundcubemail) setup
83+
84+
Example `docker-compose.yml` fragment:
85+
86+
```yaml
87+
services:
88+
roundcube:
89+
image: roundcube/roundcubemail:latest
90+
ports:
91+
- "8080:80"
92+
volumes:
93+
- ./plugins/universal_oidc_mail_sso:/var/www/html/plugins/universal_oidc_mail_sso
94+
environment:
95+
ROUNDCUBEMAIL_PLUGINS: "universal_oidc_mail_sso"
96+
OIDC_ISSUER: "https://oidc.example.com"
97+
OIDC_CLIENT_ID: "roundcube"
98+
OIDC_CLIENT_SECRET: ""
99+
ALLOWED_EMAIL_DOMAIN: ""
100+
RCUBE_MAILBOX_KEY: "<base64-32-byte-key>"
101+
FORCE_HTTPS: "true"
102+
DISABLE_PASSWORD_LOGIN: "true"
103+
```
104+
105+
Roundcube plugin config can be copied from `config.inc.php.dist` into `config.inc.php` if needed.
106+
107+
## Local test quickstart (this repository)
108+
109+
Files provided:
110+
- `docker-compose.yml`
111+
- `.env.example`
112+
- `plugins/universal_oidc_mail_sso/SQL/mysql.local.sql`
113+
114+
Steps:
115+
116+
```sh
117+
cd <repo-dir>
118+
cp .env.example .env
119+
# edit .env: set OIDC Provider client values and RCUBE_MAILBOX_KEY
120+
docker compose up -d
121+
docker compose exec -T db mariadb -u"$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" < plugins/universal_oidc_mail_sso/SQL/mysql.local.sql
122+
```
123+
124+
Then open `http://localhost:8080`.
125+
126+
## Where secrets/config go
127+
128+
Primary method (recommended): container environment variables in `.env` (consumed by `docker-compose.yml`):
129+
- `OIDC_CLIENT_ID`
130+
- `OIDC_CLIENT_SECRET`
131+
- `OIDC_ISSUER`
132+
- `OIDC_REDIRECT_URI`
133+
- `RCUBE_MAILBOX_KEY`
134+
- `ADMIN_GROUP_NAME`
135+
136+
## Admin dashboard
137+
138+
URL:
139+
140+
`/?_task=settings&_action=plugin.universal_oidc_mail_sso_admin`
141+
142+
Access control:
143+
- only users with OIDC `groups` claim containing `webmail_admin` (or `ADMIN_GROUP_NAME` override)
144+
145+
Alternative method: Roundcube plugin config file:
146+
- copy `plugins/universal_oidc_mail_sso/config.inc.php.dist`
147+
- to `plugins/universal_oidc_mail_sso/config.inc.php`
148+
- and set `$config['universal_oidc_mail_sso_*']` values there.
149+
150+
## CLI utility
151+
152+
Run key and crypto self-test:
153+
154+
```sh
155+
php plugins/universal_oidc_mail_sso/misc/crypto_tool.php self-test
156+
```
157+
158+
Encrypt demo value:
159+
160+
```sh
161+
php plugins/universal_oidc_mail_sso/misc/crypto_tool.php encrypt "secret"
162+
```
163+
164+
Decrypt:
165+
166+
```sh
167+
php plugins/universal_oidc_mail_sso/misc/crypto_tool.php decrypt <alg> <password_enc_b64> <nonce_b64>
168+
```
169+
170+
## Tests
171+
172+
```sh
173+
php plugins/universal_oidc_mail_sso/misc/run_tests.php
174+
```
175+
176+
Covers:
177+
- encryption/decryption round-trip
178+
- OIDC discovery retrieval and JWKS-backed ID token validation (mocked endpoints)
179+
180+
## Security notes / threat model
181+
182+
- App-password is never stored plaintext in DB.
183+
- Master key is external (`RCUBE_MAILBOX_KEY`), not persisted by plugin.
184+
- Decrypted credentials are used only in memory at auth/connect time.
185+
- Logs intentionally avoid secrets/tokens/passwords.
186+
- Missing claim/domain mismatch/missing mailbox/decrypt failures fail closed.
187+
- CSRF token enforced on connect form submission.
188+
- Basic session-based rate limit applied to setup submissions.
189+
190+
## Operational notes
191+
192+
- Ensure Roundcube runs behind HTTPS and trusted reverse proxy headers are set correctly.
193+
- Keep `RCUBE_MAILBOX_KEY` in secret manager/runtime env, rotate carefully.
194+
- If key changes, previously stored mailbox passwords cannot be decrypted and must be re-provisioned.

0 commit comments

Comments
 (0)