diff src/mail/ngx_mail_handler.c @ 9290:4538c1ffb0f8

Mail: added support for XOAUTH2 and OAUTHBEARER authentication. This patch adds support for the OAUTHBEARER SASL mechanism as defined by RFC 7628, as well as pre-RFC XOAUTH2 SASL mechanism. For both mechanisms, the "Auth-User" header is set to the client identity obtained from the initial SASL response sent by the client, and the "Auth-Pass" header is set to the Bearer token itself. The auth server may return the "Auth-Error-SASL" header, which is passed to the client as an additional SASL challenge. It is expected to contain mechanism-specific error details, base64-encoded. After the client responds (with an empty SASL response for XAUTH2, or with "AQ==" dummy response for OAUTHBEARER), the error message from the "Auth-Status" header is sent. Based on a patch by Rob Mueller.
author Maxim Dounin <mdounin@mdounin.ru>
date Mon, 03 Jun 2024 18:03:11 +0300
parents f83cb031a4a4
children
line wrap: on
line diff
--- a/src/mail/ngx_mail_handler.c	Mon Jun 03 18:03:09 2024 +0300
+++ b/src/mail/ngx_mail_handler.c	Mon Jun 03 18:03:11 2024 +0300
@@ -755,6 +755,274 @@
 }
 
 
+ngx_int_t
+ngx_mail_auth_xoauth2(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n)
+{
+    u_char     *p, *last;
+    ngx_str_t  *arg, oauth;
+
+    arg = s->args.elts;
+
+    if (s->auth_err.len) {
+        ngx_log_debug0(NGX_LOG_DEBUG_MAIL, c->log, 0,
+                       "mail auth xoauth2 cancel");
+
+        if (s->args.nelts == 1 && arg[0].len == 0) {
+            s->out = s->auth_err;
+            s->quit = s->auth_quit;
+            s->state = 0;
+            s->mail_state = 0;
+            ngx_str_null(&s->auth_err);
+            return NGX_OK;
+        }
+
+        s->quit = s->auth_quit;
+        ngx_str_null(&s->auth_err);
+
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0,
+                   "mail auth xoauth2: \"%V\"", &arg[n]);
+
+    oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len));
+    if (oauth.data == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client sent invalid base64 encoding in "
+                      "AUTH XOAUTH2 command");
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    /*
+     * https://developers.google.com/gmail/imap/xoauth2-protocol
+     * "user=" {User} "^Aauth=Bearer " {token} "^A^A"
+     */
+
+    p = oauth.data;
+    last = p + oauth.len;
+
+    while (p < last) {
+        if (*p++ == '\1') {
+            s->login.len = p - oauth.data - 1;
+            s->login.data = oauth.data;
+            s->passwd.len = last - p;
+            s->passwd.data = p;
+            break;
+        }
+    }
+
+    if (s->login.len < sizeof("user=") - 1
+        || ngx_strncasecmp(s->login.data, (u_char *) "user=",
+                           sizeof("user=") - 1)
+           != 0)
+    {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client sent invalid login in AUTH XOAUTH2 command");
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    s->login.len -= sizeof("user=") - 1;
+    s->login.data += sizeof("user=") - 1;
+
+    if (s->passwd.len < sizeof("auth=Bearer ") - 1
+        || ngx_strncasecmp(s->passwd.data, (u_char *) "auth=Bearer ",
+                           sizeof("auth=Bearer ") - 1)
+           != 0)
+    {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client sent invalid token in AUTH XOAUTH2 command");
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    s->passwd.len -= sizeof("auth=Bearer ") - 1;
+    s->passwd.data += sizeof("auth=Bearer ") - 1;
+
+    if (s->passwd.len < 2
+        || s->passwd.data[s->passwd.len - 2] != '\1'
+        || s->passwd.data[s->passwd.len - 1] != '\1')
+    {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client sent invalid token in AUTH XOAUTH2 command");
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    s->passwd.len -= 2;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0,
+                   "mail auth xoauth2: \"%V\" \"%V\"", &s->login, &s->passwd);
+
+    s->auth_method = NGX_MAIL_AUTH_XOAUTH2;
+
+    return NGX_DONE;
+}
+
+
+ngx_int_t
+ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c,
+    ngx_uint_t n)
+{
+    u_char     *p, *d, *last, *prev;
+    ngx_str_t  *arg, oauth;
+
+    arg = s->args.elts;
+
+    if (s->auth_err.len) {
+        ngx_log_debug0(NGX_LOG_DEBUG_MAIL, c->log, 0,
+                       "mail auth oauthbearer cancel");
+
+        if (s->args.nelts == 1
+            && ngx_strncmp(arg[0].data, (u_char *) "AQ==", 4) == 0)
+        {
+            s->out = s->auth_err;
+            s->quit = s->auth_quit;
+            s->state = 0;
+            s->mail_state = 0;
+            ngx_str_null(&s->auth_err);
+            return NGX_OK;
+        }
+
+        s->quit = s->auth_quit;
+        ngx_str_null(&s->auth_err);
+
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0,
+                   "mail auth oauthbearer: \"%V\"", &arg[n]);
+
+    oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len));
+    if (oauth.data == NULL) {
+        return NGX_ERROR;
+    }
+
+    if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client sent invalid base64 encoding in "
+                      "AUTH OAUTHBEARER command");
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    /*
+     * RFC 7628
+     * "n,a=user@example.com,^A...^Aauth=Bearer <token>^A^A"
+     */
+
+    p = oauth.data;
+    last = p + oauth.len;
+
+    s->login.len = 0;
+    prev = NULL;
+
+    while (p < last) {
+        if (*p == ',') {
+            if (prev
+                && (size_t) (p - prev) > sizeof("a=") - 1
+                && ngx_strncasecmp(prev, (u_char *) "a=", sizeof("a=") - 1)
+                   == 0)
+            {
+                s->login.len = p - prev - (sizeof("a=") - 1);
+                s->login.data = prev + sizeof("a=") - 1;
+                break;
+            }
+
+            p++;
+            prev = p;
+            continue;
+        }
+
+        if (*p == '\1') {
+            break;
+        }
+
+        p++;
+    }
+
+    if (s->login.len == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client sent invalid login in AUTH OAUTHBEARER command");
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    s->passwd.len = 0;
+    prev = NULL;
+
+    while (p < last) {
+        if (*p == '\1') {
+            if (prev
+                && (size_t) (p - prev) > sizeof("auth=Bearer ") - 1
+                && ngx_strncasecmp(prev, (u_char *) "auth=Bearer ",
+                                   sizeof("auth=Bearer ") - 1)
+                   == 0)
+            {
+                s->passwd.len = p - prev - (sizeof("auth=Bearer ") - 1);
+                s->passwd.data = prev + sizeof("auth=Bearer ") - 1;
+                break;
+            }
+
+            p++;
+            prev = p;
+            continue;
+        }
+
+        p++;
+    }
+
+    if (s->passwd.len == 0) {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client sent invalid token in AUTH OAUTHBEARER command");
+        return NGX_MAIL_PARSE_INVALID_COMMAND;
+    }
+
+    /* decode =2C =3D in login */
+
+    p = s->login.data;
+    d = s->login.data;
+    last = s->login.data + s->login.len;
+
+    while (p < last) {
+        if (*p == '=') {
+
+            /*
+             * login is always followed by other data,
+             * so p[1] and p[2] can be checked directly
+             */
+
+            if (p[1] == '2' && (p[2] == 'C' || p[2] == 'c')) {
+                *d++ = ',';
+
+            } else if (p[1] == '3' && (p[2] == 'D' || p[2] == 'd')) {
+                *d++ = '=';
+
+            } else {
+                ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                              "client sent invalid login in "
+                              "AUTH OAUTHBEARER command");
+                return NGX_MAIL_PARSE_INVALID_COMMAND;
+            }
+
+            p += 3;
+            continue;
+        }
+
+        *d++ = *p++;
+    }
+
+    s->login.len = d - s->login.data;
+
+    ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0,
+                   "mail auth oauthbearer: \"%V\" \"%V\"",
+                   &s->login, &s->passwd);
+
+    s->auth_method = NGX_MAIL_AUTH_OAUTHBEARER;
+
+    return NGX_DONE;
+}
+
+
 void
 ngx_mail_send(ngx_event_t *wev)
 {
@@ -919,13 +1187,17 @@
 {
     s->args.nelts = 0;
 
-    if (s->buffer->pos == s->buffer->last) {
-        s->buffer->pos = s->buffer->start;
-        s->buffer->last = s->buffer->start;
+    if (s->state) {
+        /* preserve tag */
+        s->arg_start = s->buffer->pos;
+
+    } else {
+        if (s->buffer->pos == s->buffer->last) {
+            s->buffer->pos = s->buffer->start;
+            s->buffer->last = s->buffer->start;
+        }
     }
 
-    s->state = 0;
-
     if (c->read->timer_set) {
         ngx_del_timer(c->read);
     }