jetmore / swaks

Swaks - Swiss Army Knife for SMTP
http://jetmore.org/john/code/swaks/
GNU General Public License v2.0
848 stars 86 forks source link

Added support for XOAUTH2 authorization, and documented in base.pod #44

Open desirider opened 2 years ago

desirider commented 2 years ago

Hi John,

I've added and tested support for XOAUTH2 authorization protocol. Also added documentation, with Gmail as an example. The XOAUTH2 protocol requires an access token, and I am passing it via the -ap argument.

Tested it several times on my local machine.

Thanks, Desirider.

polarathene commented 5 months ago

OAUTHBEARER is intended to replace XOAUTH2, but may take a while for that transition to be more widespread. Dovecot supports both.


EDIT: Presently curl will work as shown below, but only when used within the v13.3 DMS container. Newer versions of curl have a bug where only OAUTHBEARER is used.

DMS recently added support for these in Dovecot with the v13.3 release.


Offline test environment with Docker Compose + DMS

If you need a popular production instance to test against, DMS makes this simple (it already bundles the latest swaks from Github Releases):

# Normally this would provide TLS configured for both services, that's been omitted to keep the reproduction simple
services:
  # Quick and easy mailserver setup with Postfix + Dovecot for testing OAuth2 support
  dms:
    image: docker.io/mailserver/docker-mailserver:13.3
    container_name: dms-mail
    hostname: mail.example.test
    environment:
      # Enable the OAuth2 support in Dovecot and configure it for our mocked service (caddy):
      ENABLE_OAUTH2: 1
      OAUTH2_INTROSPECTION_URL: http://auth.example.test/userinfo/
    # Test authentication against these ports:
    ports:
      - "143:143" # IMAP STARTTLS (Dovecot)
      - "587:587" # SMTP STARTTLS (Postfix)
    configs:
      - source: dms-accounts
        target: /tmp/docker-mailserver/postfix-accounts.cf

  # This would normally be a proper auth service, this is sufficient to mock out the required behaviour for testing
  caddy-oauth2:
    image: caddy:2.7
    container_name: dms-oauth2
    # Leverage Docker's internal DNS for the private network bridge it creates between services:
    hostname: auth.example.test
    ports:
      - "80:80"
    configs:
      - source: mock-auth-service
        target: /etc/caddy/Caddyfile

# Using the Docker Compose `configs.content` feature instead of volume mounting separate files.
# NOTE: This feature requires Docker Compose v2.23.1 (Nov 2023) or newer:
# https://github.com/compose-spec/compose-spec/pull/446
configs:
  # Basic Caddyfile example, see a better documented equivalent at:
  # https://github.com/docker-mailserver/docker-mailserver/blob/v13.3.0/test/config/oauth2/Caddyfile
  mock-auth-service:
    content: |
      :80 {
        @auth header Authorization "Bearer DMS_YWNjZXNzX3Rva2Vu"

        handle @auth {
          respond `{ "email": "john.doe@example.test", "email_verified": true }`
        }
        # Otherwise fail when expected auth header and value were not matched:
        respond 401 {
          close
        }
      }
  # DMS expects an account to be configured to run, this config provides one
  # You can add new accounts with `docker compose exec dms setup email add user@example.test bad-password`
  # Login credentials:
  # user: "john.doe@example.test" password: "secret"
  # user: "jane.doe@example.test" password: "secret"
  dms-accounts:
    # NOTE: `$` needed to be repeated to escape it,
    # which opts out of the `compose.yaml` variable interpolation feature.
    content: |
      john.doe@example.test|{SHA512-CRYPT}$$6$$sbgFRCmQ.KWS5ryb$$EsWrlYosiadgdUOxCBHY0DQ3qFbeudDhNMqHs6jZt.8gmxUwiLVy738knqkHD4zj4amkb296HFqQ3yDq4UXt8.
      jane.doe@example.test|{SHA512-CRYPT}$$6$$o65y1ZXC4ooOPLwZ$$7TF1nYowEtNJpH6BwJBgdj2pPAxaCvhIKQA6ww5zdHm/AA7aemY9eoHC91DOgYNaKj1HLxSeWNDdvrp6mbtUY.

Commands

Generate the auth strings with base64 encoding via CLI if necessary:

These base64 encoded values will appear in the SMTP protocol exchange as part of SMTP `AUTH` / IMAP `AUTHENTICATE` commands ```console # In DMS Postfix delegates SMTP AUTH to Dovecot via SASL # Dovecot expects to receive the XOAUTH2 / OAUTHBEARER auth string encoded as base64, # which it decodes and verifies the bearer token at the configured OAuth2 service endpoint (eg: `/userinfo`) # via an HTTP request with an Authorization header (of Bearer type). # Encoding the XOAUTH2 auth string as base64: $ echo -en 'user=john.doe@example.test\001auth=Bearer DMS_YWNjZXNzX3Rva2Vu\001\001' | base64 -w0; echo dXNlcj1qb2huLmRvZUBleGFtcGxlLnRlc3QBYXV0aD1CZWFyZXIgRE1TX1lXTmpaWE56WDNSdmEyVnUBAQ== # OAUTHBEARER equivalent: $ echo -en 'n,a=john.doe@example.test,\001host=localhost\001port=143\001auth=Bearer DMS_YWNjZXNzX3Rva2Vu\001\001' | base64 -w0; echo bixhPWpvaG4uZG9lQGV4YW1wbGUudGVzdCwBaG9zdD1sb2NhbGhvc3QBcG9ydD0xNDMBYXV0aD1CZWFyZXIgRE1TX1lXTmpaWE56WDNSdmEyVnUBAQ== # Equivalent values for jane.doe@example.test with the same Access Token # XOAUTH: dXNlcj1qYW5lLmRvZUBleGFtcGxlLnRlc3QBYXV0aD1CZWFyZXIgRE1TX1lXTmpaWE56WDNSdmEyVnUBAQ== # OAUTHBEARER: bixhPWphbmUuZG9lQGV4YW1wbGUudGVzdCwBaG9zdD1sb2NhbGhvc3QBcG9ydD0xNDMBYXV0aD1CZWFyZXIgRE1TX1lXTmpaWE56WDNSdmEyVnUBAQ== ```

Test commands (run on the host system after a docker compose up --force-recreate):

# Testing against the mocked endpoint directly (which is what Dovecot is responsible for handling):
# - Within the DMS container this would be to http://auth.example.test/userinfo
# - Otherwise from the host system reach the caddy container via the published port on localhost
# In this case Dovecot is configured by default to validate successful auth by matching the
# returned `email` field  against the `user` field (provided via the auth string).
curl http://localhost:80/userinfo -H 'Authorization: Bearer DMS_YWNjZXNzX3Rva2Vu' -w '\n'
# Response: { "email": "john.doe@example.test", "email_verified": true }

# Testing through Postfix SMTP AUTH via the proposed swaks options of this PR:
# `swaks` is also available within the container (at `/usr/local/bin/swaks`),
# use it via `docker compose exec dms swaks ...`
swaks --server localhost:587 \
  --from john.doe@example.test \
  --to jane.doe@example.test \
  --auth XOAUTH2 \
  -au john.doe@example.test \
  -ap DMS_YWNjZXNzX3Rva2Vu

For comparison here is the equivalent with curl:

# Set `--login-options` to either 'AUTH=XOAUTH2' or 'AUTH=OAUTHBEARER'.
# Both are valid and the DMS container logs from Postfix will indicate the correct method like:
# `sasl_method=OAUTHBEARER, sasl_username=john.doe@example.test`
# The curl output itself also shows authentication success during the SMTP protocol.
#
# NOTE: If running on the host replace `mail.example.test` with `localhost`.
# NOTE: Technically `--upload-file` expects a proper input with RFC 5322 headers:
# https://everything.curl.dev/usingcurl/smtp
curl --silent --verbose \
  --url 'smtp://mail.example.test:587' \
  --user 'john.doe@example.test' \
  --login-options 'AUTH=XOAUTH2' \
  --oauth2-bearer 'DMS_YWNjZXNzX3Rva2Vu' \
  --mail-from 'john.doe@example.test' \
  --mail-rcpt 'jane.doe@example.test' \
  --upload-file <<< 'Hello Jane!'
Output ``` * Trying 127.0.0.1:587... * Connected to localhost (127.0.0.1) port 587 (#0) < 220 mail.example.test ESMTP > EHLO polarathene-laptop < 250-mail.example.test < 250-PIPELINING < 250-SIZE 10240000 < 250-ETRN < 250-AUTH PLAIN LOGIN OAUTHBEARER XOAUTH2 < 250-AUTH=PLAIN LOGIN OAUTHBEARER XOAUTH2 < 250-ENHANCEDSTATUSCODES < 250-8BITMIME < 250-DSN < 250 CHUNKING > AUTH OAUTHBEARER < 334 > bixhPWpvaG4uZG9lQGV4YW1wbGUudGVzdCwBaG9zdD1sb2NhbGhvc3QBcG9ydD0xNDMBYXV0aD1CZWFyZXIgRE1TX1lXTmpaWE56WDNSdmEyVnUBAQ== < 235 2.7.0 Authentication successful > MAIL FROM: < 250 2.1.0 Ok > RCPT TO: < 250 2.1.5 Ok > DATA < 354 End data with . } [12 bytes data] < 250 2.0.0 Ok: queued as 11E3418A56 * Connection #0 to host localhost left intact ``` **NOTE:** Instead of `< 250 2.0.0 Ok: queued as 11E3418A56` you may get `< 521 5.5.2 mail.example.test Error: bare received` which is due to a recent security feature that expects CRLF for SMTP and curl is not properly handling. This security check can be ignored when performed within the DMS container and trust is given to mail clients running within that container by adding the ENV `PERMIT_DOCKER=container`. However that is not relevant to the auth test reproduction demonstrated