tests: Revise OAuth2 tests (#3795)
* tests: OAuth2 - Replace Python `/userinfo` endpoint with Caddy
Better documented, easier flow and separation of concerns via Caddy.
The python code had additional noise related to setting up a basic API which is abstracted away via `Caddyfile` config that's dedicated to this task.
* tests: OAuth2 - Minimize noise + Improve test assertion
Caddyfile can use an Access Token instead of a JWT. Much smaller and correct for this OAuth2 configuration. This new value has been documented inline.
Likewise the `sub` field returned is not important to this test. `email_verified` is kept as it may be helpful for further coverage testing.
The actual test-case has better assertions for success and failure by checking for Dovecot logs we expect instead of netcat response.
`oauth2` to `auth` for the Caddy container hostname is not necessary, just a more generic subdomain choice.
* tests: OAuth2 - Caddyfile `imap/xoauth2` route dynamic via query string
This way is more flexible and doesn't require modifying the `Caddyfile` directly, while still easy to use.
Additionally simplifies understanding the Caddyfile to maintainers by removing the `route` directive that was required to ensure a deterministic order of vars.
* tests: OAuth2 - `/imap/xoauth2` respond with IMAP commands for netcat
Since this is the only intended usage, might as well have it respond with the full file content.
* tests: OAuth2 - Implement coverage for `OAUTHBEARER`
Caddyfile route for `/imap/` now accepts any subpath to support handling both `xoauth2` and `oauthbearer` subpaths.
Both SASL mechanisms represent the same information, with `XOAUTH2` being a common mechanism to encounter defined by Google, whilst `OAUTHBEARER` is the newer variant standardized by RFC 7628 but not yet as widely adopted.
The request to `/userinfo` endpoint will be the same, only the `credentials` value to be encoded differs.
Instead of repeating the block for a similar route, this difference is handled via the Caddyfile `map` directive.
We match the path context (_`/xoauth2` or `/oauthbearer`, the `/imap` prefix was stripped by `handle_path` earlier_), when there is a valid match, `sasl_mechanism` and `credentials` map vars are created and assigned to be referenced by the later `respond` directive.
---
Repeat the same test-case logic, DRY with log asserts extracted to a common function call. This should be fine as the auth method will be sufficient to match against or a common failure caught.
* tests: OAuth2 - Minor revisions
Separate test cases and additional comment on creating the same base64 encoded credentials via CLI as an alternative to running Caddy.
Added a simple `compose.yaml` for troubleshooting or running the container for the `/imap/xoauth2` / `/imap/oauthbearer` endpoints.
* tests: OAuth2 - Route endpoints in Caddyfile with snippets instead
`reverse_proxy` was a bit more convenient, but the additional internal ports weren't really relevant. It also added noise to logging when troubleshooting.
The `import` directive with Snippet blocks instead is a bit cleaner, but when used in a single file snippets must be defined prior to referencing them with the `import` directive.
---
`compose.yaml` inlines the examples, with slight modification to `localhost:80`, since the Caddyfile examples `auth.example.test` is more relevant to the tests which can use it, and not applicable to troubleshooting locally outside of tests.
* chore: Add entry to `CHANGELOG.md`
* chore: Additional context on access token
2024-01-20 22:49:09 +13:00
|
|
|
# Mocked OAuth2 /userinfo endpoint normally provided via an Authorization Server (AS) / Identity Provider (IdP)
|
|
|
|
#
|
|
|
|
# Dovecot will query the mocked `/userinfo` endpoint with the OAuth2 bearer token it was provided during login.
|
|
|
|
# If the session for the token is valid, a response returns an attribute to perform a UserDB lookup on (default: email).
|
|
|
|
|
|
|
|
# `DMS_YWNjZXNzX3Rva2Vu` is the access token our OAuth2 tests expect for an authorization request to be successful.
|
|
|
|
# - The token was created by base64 encoding the string `access_token`, followed by adding `DMS_` as a prefix.
|
|
|
|
# - Normally an access token is a short-lived value associated to a login session. The value does not encode any real data.
|
|
|
|
# It is an opaque token: https://oauth.net/2/bearer-tokens/
|
|
|
|
|
|
|
|
# NOTE: The main server config is at the end within the `:80 { ... }` block.
|
|
|
|
# This is because the endpoints are extracted out into Caddy snippets, which must be defined before they're referenced.
|
|
|
|
|
|
|
|
# /userinfo
|
|
|
|
(route-userinfo) {
|
|
|
|
vars token "DMS_YWNjZXNzX3Rva2Vu"
|
|
|
|
|
|
|
|
# Expects to match an authorization header with a specific bearer token:
|
|
|
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
|
|
|
@auth header Authorization "Bearer {vars.token}"
|
|
|
|
|
|
|
|
# If the provided authorization header has the expected value (bearer token), respond with this JSON payload:
|
|
|
|
handle @auth {
|
|
|
|
# JSON inlined via HereDoc string feature:
|
|
|
|
# Dovecot OAuth2 defaults to `username_attribute = email`, which must be returned in the response to match
|
|
|
|
# with the `user` credentials field that Dovecot received via base64 encoded IMAP `AUTHENTICATE` value.
|
|
|
|
respond <<EOF
|
|
|
|
{
|
|
|
|
"email": "user1@localhost.localdomain",
|
|
|
|
"email_verified": true
|
|
|
|
}
|
|
|
|
EOF
|
|
|
|
}
|
|
|
|
|
|
|
|
# Failed to authorize, close connection and send a 401 status (unauthorized):
|
|
|
|
respond 401 {
|
|
|
|
close
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-24 07:11:05 +13:00
|
|
|
# NOTE: This portion of config is only relevant for understanding what happens seamlesssly,
|
|
|
|
# DMS tests no longer use raw IMAP commands with netcat, thus none of this is relevant beyond reference for troubleshooting.
|
|
|
|
#
|
tests: Revise OAuth2 tests (#3795)
* tests: OAuth2 - Replace Python `/userinfo` endpoint with Caddy
Better documented, easier flow and separation of concerns via Caddy.
The python code had additional noise related to setting up a basic API which is abstracted away via `Caddyfile` config that's dedicated to this task.
* tests: OAuth2 - Minimize noise + Improve test assertion
Caddyfile can use an Access Token instead of a JWT. Much smaller and correct for this OAuth2 configuration. This new value has been documented inline.
Likewise the `sub` field returned is not important to this test. `email_verified` is kept as it may be helpful for further coverage testing.
The actual test-case has better assertions for success and failure by checking for Dovecot logs we expect instead of netcat response.
`oauth2` to `auth` for the Caddy container hostname is not necessary, just a more generic subdomain choice.
* tests: OAuth2 - Caddyfile `imap/xoauth2` route dynamic via query string
This way is more flexible and doesn't require modifying the `Caddyfile` directly, while still easy to use.
Additionally simplifies understanding the Caddyfile to maintainers by removing the `route` directive that was required to ensure a deterministic order of vars.
* tests: OAuth2 - `/imap/xoauth2` respond with IMAP commands for netcat
Since this is the only intended usage, might as well have it respond with the full file content.
* tests: OAuth2 - Implement coverage for `OAUTHBEARER`
Caddyfile route for `/imap/` now accepts any subpath to support handling both `xoauth2` and `oauthbearer` subpaths.
Both SASL mechanisms represent the same information, with `XOAUTH2` being a common mechanism to encounter defined by Google, whilst `OAUTHBEARER` is the newer variant standardized by RFC 7628 but not yet as widely adopted.
The request to `/userinfo` endpoint will be the same, only the `credentials` value to be encoded differs.
Instead of repeating the block for a similar route, this difference is handled via the Caddyfile `map` directive.
We match the path context (_`/xoauth2` or `/oauthbearer`, the `/imap` prefix was stripped by `handle_path` earlier_), when there is a valid match, `sasl_mechanism` and `credentials` map vars are created and assigned to be referenced by the later `respond` directive.
---
Repeat the same test-case logic, DRY with log asserts extracted to a common function call. This should be fine as the auth method will be sufficient to match against or a common failure caught.
* tests: OAuth2 - Minor revisions
Separate test cases and additional comment on creating the same base64 encoded credentials via CLI as an alternative to running Caddy.
Added a simple `compose.yaml` for troubleshooting or running the container for the `/imap/xoauth2` / `/imap/oauthbearer` endpoints.
* tests: OAuth2 - Route endpoints in Caddyfile with snippets instead
`reverse_proxy` was a bit more convenient, but the additional internal ports weren't really relevant. It also added noise to logging when troubleshooting.
The `import` directive with Snippet blocks instead is a bit cleaner, but when used in a single file snippets must be defined prior to referencing them with the `import` directive.
---
`compose.yaml` inlines the examples, with slight modification to `localhost:80`, since the Caddyfile examples `auth.example.test` is more relevant to the tests which can use it, and not applicable to troubleshooting locally outside of tests.
* chore: Add entry to `CHANGELOG.md`
* chore: Additional context on access token
2024-01-20 22:49:09 +13:00
|
|
|
# /imap/xoauth2
|
|
|
|
# Generate IMAP commands for authentication testing
|
|
|
|
# Base64 encoded credentials can alternative be done via CLI with:
|
|
|
|
# echo -en 'user=${USERNAME}\001auth=Bearer ${ACCESS_TOKEN}\001\001' | base64 -w0; echo
|
|
|
|
#
|
|
|
|
# Provide `user` and `access_token` values via query string parameters:
|
|
|
|
# curl 'http://auth.example.test/imap/xoauth2?user=user1@localhost.localdomain&access_token=DMS_YWNjZXNzX3Rva2Vu'
|
|
|
|
# curl 'http://auth.example.test/imap/oauthbearer?user=user1@localhost.localdomain&access_token=DMS_YWNjZXNzX3Rva2Vu'
|
|
|
|
#
|
|
|
|
# Example Response:
|
|
|
|
# a0 AUTHENTICATE XOAUTH2 dXNlcj11c2VyMUBsb2NhbGhvc3QubG9jYWxkb21haW4BYXV0aD1CZWFyZXIgRE1TX1lXTmpaWE56WDNSdmEyVnUBAQ==
|
|
|
|
# a1 EXAMINE INBOX
|
|
|
|
# a2 LOGOUT
|
|
|
|
#
|
|
|
|
# When Dovecot queries /userinfo endpoint, it will be after base64 decoding the IMAP `AUTHENTICATE` value,
|
|
|
|
# and sending the `auth` value from the `credentials` variable as an HTTP Authorization header.
|
|
|
|
(route-imap) {
|
|
|
|
# The login username + OAuth2 access token prior to Base64 encoding, as per the XOAUTH2 spec:
|
|
|
|
# https://developers.google.com/gmail/imap/xoauth2-protocol#the_sasl_xoauth2_mechanism
|
|
|
|
# For OAUTHBEARER `host` and `port` do not appear to affect authentication with Dovecot
|
|
|
|
map {path} {sasl_mechanism} {credentials} {
|
|
|
|
/xoauth2 XOAUTH2 "user={query.user}\001auth=Bearer {query.access_token}\001\001"
|
|
|
|
/oauthbearer OAUTHBEARER "n,a={query.user},\001host=localhost\001port=143\001auth=Bearer {query.access_token}\001\001"
|
|
|
|
}
|
|
|
|
|
|
|
|
# Responds with the raw IMAP commands for testing XOAUTH2 authentication.
|
|
|
|
# Uses the `b64enc` template function to encode credentials as required for `IMAP AUTHENTICATE`:
|
|
|
|
templates
|
|
|
|
respond <<EOF
|
|
|
|
a0 AUTHENTICATE {sasl_mechanism} {{b64enc "{credentials}"}}
|
|
|
|
a1 EXAMINE INBOX
|
|
|
|
a2 LOGOUT
|
|
|
|
EOF
|
|
|
|
}
|
|
|
|
|
|
|
|
# Routes the endpoints to the logical blocks extracted out as snippets above
|
|
|
|
:80 {
|
|
|
|
# This is the `/userinfo` endpoint that Dovecot connects to with the OAuth2 setting (default: `introspection_mode = auth`).
|
|
|
|
# Example: curl http://auth.example.test/userinfo -H 'Authorization: Bearer DMS_YWNjZXNzX3Rva2Vu'
|
|
|
|
handle_path /userinfo {
|
|
|
|
import route-userinfo
|
|
|
|
}
|
|
|
|
|
|
|
|
# An additional endpoint for maintainers to generate `test/files/auth/imap-oauth2-auth.txt`
|
|
|
|
handle_path /imap/* {
|
|
|
|
import route-imap
|
|
|
|
}
|
|
|
|
}
|