Hello! On Tue, May 21, 2024 at 04:16:05AM +0300, Maxim Dounin wrote:
[...] > Additionally, I tend to think that a better approach to > communicate with the auth server might be to parse SASL initial > responses as sent by clients, and provide parsed information, > username and the Bearer token, in the "Auth-User" and "Auth-Pass" > headers, similarly to what we do for other SASL mechanism, such as > PLAIN and CRAM-MD5. > > This will certainly work for XOAUTH2, since the only information > is the username and the Bearer token. In OAUTHBEARER some > additional information can be provided, but I very much doubt it > is used in practice (and, if needed, full initial response > can be sent separately, in a separate header). And parsing > implementation seems to be easy enough. > > Immediate benefits include early correctness checking (e.g., > "n,user=t...@example.com,^A..." as used in tests and in some RFC 7628 > examples is incorrect, it should be "n,a=t...@example.com,^A..." > instead) and simpler auth server implementation. > > I've provided proof-of-concept as a separate patch below. > > What do you think about this approach? Just for the record, below are login decoding fixes for the proof-of-concept patch in question, as well as test improvements and full merged patch to support XOAUTH2/OAUTHBEARER with parsing. I'm going to commit this shortly unless there are objections. [...] > # HG changeset patch > # User Maxim Dounin <mdou...@mdounin.ru> > # Date 1716247681 -10800 > # Tue May 21 02:28:01 2024 +0300 > # Node ID 377c966d4623000c0b9a5b2c0d47f9dd64b4cd9d > # Parent b06a347640e565012aced65e2a694a306ed2db5c > Mail: parsing of XOAUTH2 and OAUTHBEARER. > > 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 the Bearer token itself. > > Additionally, only continuation responses correct for the particular > mechanism are now accepted after errors ("AQ==" for OAUTHBEARER, empty > line for XOAUTH2). > > To be merged with the previous patch. [...] > +ngx_int_t > +ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, > + ngx_uint_t n) > +{ > + u_char *p, *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; > @@ -782,16 +885,14 @@ ngx_mail_auth_oauth(ngx_mail_session_t * > return NGX_OK; > } > > - invalid: > - > s->quit = s->auth_quit; > ngx_str_null(&s->auth_err); > > return NGX_MAIL_PARSE_INVALID_COMMAND; > } > > - ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, > - "mail auth oauth: \"%V\" type %ui", &arg[n], auth_method); > + 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) { > @@ -801,19 +902,113 @@ ngx_mail_auth_oauth(ngx_mail_session_t * > 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/OAUTHBEARER command"); > + "AUTH OAUTHBEARER command"); > + return NGX_MAIL_PARSE_INVALID_COMMAND; > + } > + > + /* > + * RFC 7628 > + * "n,a=u...@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 = oauth.len; > - s->passwd.data = oauth.data; > + s->passwd.len = 0; > + prev = NULL; > > - ngx_str_null(&s->login); > + 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; > + } > > - ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, > - "mail auth oauth: \"%V\"", &s->passwd); > + /* decode =2C =3D in login */ > + > + p = s->login.data; > + last = s->login.data + s->login.len; > + > + while (p < last) { > + if (*p == '=') { > + if (p[1] == '2' && (p[2] == 'C' || p[2] == 'c')) { > + *p = ','; > + > + } else if (p[1] == '3' && (p[2] == 'D' || p[2] == 'd')) { > + *p = '='; > > - s->auth_method = auth_method; > + } 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; > + } > + > + p++; > + } > + > + 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; > } Login decoding is wrong here, here is a fix: diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -865,7 +865,7 @@ ngx_int_t ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n) { - u_char *p, *last, *prev; + u_char *p, *d, *last, *prev; ngx_str_t *arg, oauth; arg = s->args.elts; @@ -980,15 +980,22 @@ ngx_mail_auth_oauthbearer(ngx_mail_sessi /* 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')) { - *p = ','; + *d++ = ','; } else if (p[1] == '3' && (p[2] == 'D' || p[2] == 'd')) { - *p = '='; + *d++ = '='; } else { ngx_log_error(NGX_LOG_INFO, c->log, 0, @@ -1001,9 +1008,11 @@ ngx_mail_auth_oauthbearer(ngx_mail_sessi continue; } - p++; + *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); And patch for tests: diff --git a/mail_oauth.t b/mail_oauth.t --- a/mail_oauth.t +++ b/mail_oauth.t @@ -71,17 +71,18 @@ http { smtp %%PORT_8026%%; } - map $http_auth_pass $reply { - ~secretok OK; + map $http_auth_user:$http_auth_pass $reply { + t...@example.com:secretok OK; + test=,@example.com:secretok OK; default auth-failed; } + map $http_auth_pass $passw { - ~secretok secret; - default ""; + secretok secret; } + map $http_auth_pass $sasl { - ~saslfail "eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0="; - default ""; + saslfail "eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0="; } server { @@ -92,7 +93,6 @@ http { add_header Auth-Status $reply; add_header Auth-Server 127.0.0.1; add_header Auth-Port $proxy_port; - add_header Auth-User t...@example.com; add_header Auth-Pass $passw; add_header Auth-Wait 1; add_header Auth-Error-SASL $sasl; @@ -106,7 +106,7 @@ EOF $t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); $t->run_daemon(\&Test::Nginx::POP3::pop3_test_daemon); $t->run_daemon(\&Test::Nginx::SMTP::smtp_test_daemon); -$t->try_run('no oauth support')->plan(47); +$t->try_run('no oauth support')->plan(48); $t->waitforsocket('127.0.0.1:' . port(8144)); $t->waitforsocket('127.0.0.1:' . port(8111)); @@ -123,6 +123,8 @@ EOF my $s; my $token = encode_base64( "n,a=test\@example.com,\001auth=Bearer secretok\001\001", ''); +my $token_escaped = encode_base64( + "n,a=test=3D=2C\@example.com,\001auth=Bearer secretok\001\001", ''); my $token_saslfail = encode_base64( "n,a=test\@example.com,\001auth=Bearer saslfail\001\001", ''); my $token_bad = encode_base64( @@ -144,6 +146,11 @@ my $token_xoauth2_bad = encode_base64( $s = Test::Nginx::IMAP->new(); $s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . $token_escaped); +$s->ok('imap oauthbearer escaped login'); + +$s = Test::Nginx::IMAP->new(); +$s->read(); $s->send('1 AUTHENTICATE OAUTHBEARER'); $s->check(qr/\+ /, 'imap oauthbearer challenge'); $s->send($token); Full XOAUTH2/OAUTHBEARER patch with parsing: # HG changeset patch # User Maxim Dounin <mdou...@mdounin.ru> # Date 1717195412 -10800 # Sat Jun 01 01:43:32 2024 +0300 # Node ID a87a01815c540faf782e96078ddd303adef2175d # Parent e73875d3d33e07948136a5eec2d313d6ffdfbe72 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. diff --git a/src/mail/ngx_mail.h b/src/mail/ngx_mail.h --- a/src/mail/ngx_mail.h +++ b/src/mail/ngx_mail.h @@ -141,7 +141,9 @@ typedef enum { ngx_pop3_auth_login_password, ngx_pop3_auth_plain, ngx_pop3_auth_cram_md5, - ngx_pop3_auth_external + ngx_pop3_auth_external, + ngx_pop3_auth_xoauth2, + ngx_pop3_auth_oauthbearer } ngx_pop3_state_e; @@ -152,6 +154,8 @@ typedef enum { ngx_imap_auth_plain, ngx_imap_auth_cram_md5, ngx_imap_auth_external, + ngx_imap_auth_xoauth2, + ngx_imap_auth_oauthbearer, ngx_imap_login, ngx_imap_user, ngx_imap_passwd @@ -165,6 +169,8 @@ typedef enum { ngx_smtp_auth_plain, ngx_smtp_auth_cram_md5, ngx_smtp_auth_external, + ngx_smtp_auth_xoauth2, + ngx_smtp_auth_oauthbearer, ngx_smtp_helo, ngx_smtp_helo_xclient, ngx_smtp_helo_auth, @@ -212,8 +218,9 @@ typedef struct { unsigned no_sync_literal:1; unsigned starttls:1; unsigned esmtp:1; - unsigned auth_method:3; + unsigned auth_method:4; unsigned auth_wait:1; + unsigned auth_quit:1; ngx_str_t login; ngx_str_t passwd; @@ -229,6 +236,8 @@ typedef struct { ngx_str_t smtp_from; ngx_str_t smtp_to; + ngx_str_t auth_err; + ngx_str_t cmd; ngx_uint_t command; @@ -303,15 +312,19 @@ typedef struct { #define NGX_MAIL_AUTH_APOP 3 #define NGX_MAIL_AUTH_CRAM_MD5 4 #define NGX_MAIL_AUTH_EXTERNAL 5 -#define NGX_MAIL_AUTH_NONE 6 +#define NGX_MAIL_AUTH_XOAUTH2 6 +#define NGX_MAIL_AUTH_OAUTHBEARER 7 +#define NGX_MAIL_AUTH_NONE 8 -#define NGX_MAIL_AUTH_PLAIN_ENABLED 0x0002 -#define NGX_MAIL_AUTH_LOGIN_ENABLED 0x0004 -#define NGX_MAIL_AUTH_APOP_ENABLED 0x0008 -#define NGX_MAIL_AUTH_CRAM_MD5_ENABLED 0x0010 -#define NGX_MAIL_AUTH_EXTERNAL_ENABLED 0x0020 -#define NGX_MAIL_AUTH_NONE_ENABLED 0x0040 +#define NGX_MAIL_AUTH_PLAIN_ENABLED 0x0002 +#define NGX_MAIL_AUTH_LOGIN_ENABLED 0x0004 +#define NGX_MAIL_AUTH_APOP_ENABLED 0x0008 +#define NGX_MAIL_AUTH_CRAM_MD5_ENABLED 0x0010 +#define NGX_MAIL_AUTH_EXTERNAL_ENABLED 0x0020 +#define NGX_MAIL_AUTH_XOAUTH2_ENABLED 0x0040 +#define NGX_MAIL_AUTH_OAUTHBEARER_ENABLED 0x0080 +#define NGX_MAIL_AUTH_NONE_ENABLED 0x0100 #define NGX_MAIL_PARSE_INVALID_COMMAND 20 @@ -399,6 +412,10 @@ ngx_int_t ngx_mail_auth_cram_md5_salt(ng ngx_int_t ngx_mail_auth_cram_md5(ngx_mail_session_t *s, ngx_connection_t *c); ngx_int_t ngx_mail_auth_external(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n); +ngx_int_t ngx_mail_auth_xoauth2(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n); +ngx_int_t ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n); ngx_int_t ngx_mail_auth_parse(ngx_mail_session_t *s, ngx_connection_t *c); void ngx_mail_send(ngx_event_t *wev); diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c --- a/src/mail/ngx_mail_auth_http_module.c +++ b/src/mail/ngx_mail_auth_http_module.c @@ -53,6 +53,7 @@ struct ngx_mail_auth_http_ctx_s { ngx_str_t err; ngx_str_t errmsg; ngx_str_t errcode; + ngx_str_t errsasl; time_t sleep; @@ -67,6 +68,7 @@ static void ngx_mail_auth_http_ignore_st static void ngx_mail_auth_http_process_headers(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); static void ngx_mail_auth_sleep_handler(ngx_event_t *rev); +static void ngx_mail_auth_send_error(ngx_mail_session_t *s); static ngx_int_t ngx_mail_auth_http_parse_header_line(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); static void ngx_mail_auth_http_block_read(ngx_event_t *rev); @@ -152,6 +154,8 @@ static ngx_str_t ngx_mail_auth_http_me ngx_string("apop"), ngx_string("cram-md5"), ngx_string("external"), + ngx_string("xoauth2"), + ngx_string("oauthbearer"), ngx_string("none") }; @@ -677,6 +681,51 @@ ngx_mail_auth_http_process_headers(ngx_m continue; } + if (len == sizeof("Auth-Error-SASL") - 1 + && ngx_strncasecmp(ctx->header_name_start, + (u_char *) "Auth-Error-SASL", + sizeof("Auth-Error-SASL") - 1) + == 0) + { + if (s->auth_method != NGX_MAIL_AUTH_XOAUTH2 + && s->auth_method != NGX_MAIL_AUTH_OAUTHBEARER) + { + continue; + } + + len = ctx->header_end - ctx->header_start; + + if (s->protocol == NGX_MAIL_SMTP_PROTOCOL) { + size = len + sizeof("334 " CRLF) - 1; + + } else { + size = len + sizeof("+ " CRLF) - 1; + } + + p = ngx_pnalloc(s->connection->pool, size); + if (p == NULL) { + ngx_close_connection(ctx->peer.connection); + ngx_destroy_pool(ctx->pool); + ngx_mail_session_internal_server_error(s); + return; + } + + ctx->errsasl.len = size; + ctx->errsasl.data = p; + + if (s->protocol == NGX_MAIL_SMTP_PROTOCOL) { + *p++ = '3'; *p++ = '3'; *p++ = '4'; *p++ = ' '; + + } else { + *p++ = '+'; *p++ = ' '; + } + + p = ngx_cpymem(p, ctx->header_start, len); + *p++ = CR; *p = LF; + + continue; + } + /* ignore other headers */ continue; @@ -717,14 +766,15 @@ ngx_mail_auth_http_process_headers(ngx_m *p++ = CR; *p = LF; } - s->out = ctx->err; + s->out = ctx->errsasl; + s->auth_err = ctx->err; timer = ctx->sleep; ngx_destroy_pool(ctx->pool); if (timer == 0) { - s->quit = 1; - ngx_mail_send(s->connection->write); + s->auth_quit = 1; + ngx_mail_auth_send_error(s); return; } @@ -858,9 +908,8 @@ ngx_mail_auth_http_process_headers(ngx_m static void ngx_mail_auth_sleep_handler(ngx_event_t *rev) { - ngx_connection_t *c; - ngx_mail_session_t *s; - ngx_mail_core_srv_conf_t *cscf; + ngx_connection_t *c; + ngx_mail_session_t *s; ngx_log_debug0(NGX_LOG_DEBUG_MAIL, rev->log, 0, "mail auth sleep handler"); @@ -877,33 +926,7 @@ ngx_mail_auth_sleep_handler(ngx_event_t return; } - cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); - - rev->handler = cscf->protocol->auth_state; - - s->mail_state = 0; - s->auth_method = NGX_MAIL_AUTH_PLAIN; - s->tag.len = 0; - - c->log->action = "in auth state"; - - ngx_mail_send(c->write); - - if (c->destroyed) { - return; - } - - ngx_add_timer(rev, cscf->timeout); - - if (rev->ready) { - rev->handler(rev); - return; - } - - if (ngx_handle_read_event(rev, 0) != NGX_OK) { - ngx_mail_close_connection(c); - } - + ngx_mail_auth_send_error(s); return; } @@ -915,6 +938,57 @@ ngx_mail_auth_sleep_handler(ngx_event_t } +static void +ngx_mail_auth_send_error(ngx_mail_session_t *s) +{ + ngx_event_t *rev; + ngx_connection_t *c; + ngx_mail_core_srv_conf_t *cscf; + + c = s->connection; + rev = c->read; + + cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); + + rev->handler = cscf->protocol->auth_state; + + s->auth_method = NGX_MAIL_AUTH_PLAIN; + + c->log->action = "in auth state"; + + if (s->out.len == 0) { + s->out = s->auth_err; + s->quit = s->auth_quit; + ngx_str_null(&s->auth_err); + + s->state = 0; + s->mail_state = 0; + s->tag.len = 0; + + } else { + s->auth_err.len -= s->tag.len; + s->auth_err.data += s->tag.len; + } + + ngx_mail_send(c->write); + + if (c->destroyed) { + return; + } + + ngx_add_timer(rev, cscf->timeout); + + if (rev->ready) { + rev->handler(rev); + return; + } + + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_mail_close_connection(c); + } +} + + static ngx_int_t ngx_mail_auth_http_parse_header_line(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx) diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -755,6 +755,274 @@ ngx_mail_auth_external(ngx_mail_session_ } +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=u...@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 @@ ngx_mail_auth(ngx_mail_session_t *s, ngx { 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); } diff --git a/src/mail/ngx_mail_imap_handler.c b/src/mail/ngx_mail_imap_handler.c --- a/src/mail/ngx_mail_imap_handler.c +++ b/src/mail/ngx_mail_imap_handler.c @@ -220,6 +220,14 @@ ngx_mail_imap_auth_state(ngx_event_t *re case ngx_imap_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_imap_auth_xoauth2: + rc = ngx_mail_auth_xoauth2(s, c, 0); + break; + + case ngx_imap_auth_oauthbearer: + rc = ngx_mail_auth_oauthbearer(s, c, 0); + break; } } else if (rc == NGX_IMAP_NEXT) { @@ -432,6 +440,38 @@ ngx_mail_imap_authenticate(ngx_mail_sess s->mail_state = ngx_imap_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(iscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_imap_auth_xoauth2; + return ngx_mail_auth_xoauth2(s, c, 1); + } + + ngx_str_set(&s->out, imap_plain_next); + s->mail_state = ngx_imap_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(iscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_imap_auth_oauthbearer; + return ngx_mail_auth_oauthbearer(s, c, 1); + } + + ngx_str_set(&s->out, imap_plain_next); + s->mail_state = ngx_imap_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_imap_module.c b/src/mail/ngx_mail_imap_module.c --- a/src/mail/ngx_mail_imap_module.c +++ b/src/mail/ngx_mail_imap_module.c @@ -30,6 +30,8 @@ static ngx_conf_bitmask_t ngx_mail_imap { ngx_string("login"), NGX_MAIL_AUTH_LOGIN_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_null_string, 0 } }; @@ -40,6 +42,8 @@ static ngx_str_t ngx_mail_imap_auth_met ngx_null_string, /* APOP */ ngx_string("AUTH=CRAM-MD5"), ngx_string("AUTH=EXTERNAL"), + ngx_string("AUTH=XOAUTH2"), + ngx_string("AUTH=OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -182,7 +186,7 @@ ngx_mail_imap_merge_srv_conf(ngx_conf_t } for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { @@ -208,7 +212,7 @@ ngx_mail_imap_merge_srv_conf(ngx_conf_t auth = p; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { diff --git a/src/mail/ngx_mail_parse.c b/src/mail/ngx_mail_parse.c --- a/src/mail/ngx_mail_parse.c +++ b/src/mail/ngx_mail_parse.c @@ -953,6 +953,20 @@ ngx_mail_auth_parse(ngx_mail_session_t * return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (arg[0].len == 7) { + + if (ngx_strncasecmp(arg[0].data, (u_char *) "XOAUTH2", 7) == 0) { + + if (s->args.nelts == 1 || s->args.nelts == 2) { + return NGX_MAIL_AUTH_XOAUTH2; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + if (arg[0].len == 8) { if (ngx_strncasecmp(arg[0].data, (u_char *) "CRAM-MD5", 8) == 0) { @@ -976,5 +990,19 @@ ngx_mail_auth_parse(ngx_mail_session_t * return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (arg[0].len == 11) { + + if (ngx_strncasecmp(arg[0].data, (u_char *) "OAUTHBEARER", 11) == 0) { + + if (s->args.nelts == 1 || s->args.nelts == 2) { + return NGX_MAIL_AUTH_OAUTHBEARER; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + return NGX_MAIL_PARSE_INVALID_COMMAND; } diff --git a/src/mail/ngx_mail_pop3_handler.c b/src/mail/ngx_mail_pop3_handler.c --- a/src/mail/ngx_mail_pop3_handler.c +++ b/src/mail/ngx_mail_pop3_handler.c @@ -260,6 +260,14 @@ ngx_mail_pop3_auth_state(ngx_event_t *re case ngx_pop3_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_pop3_auth_xoauth2: + rc = ngx_mail_auth_xoauth2(s, c, 0); + break; + + case ngx_pop3_auth_oauthbearer: + rc = ngx_mail_auth_oauthbearer(s, c, 0); + break; } } @@ -553,6 +561,38 @@ ngx_mail_pop3_auth(ngx_mail_session_t *s s->mail_state = ngx_pop3_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(pscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_pop3_auth_xoauth2; + return ngx_mail_auth_xoauth2(s, c, 1); + } + + ngx_str_set(&s->out, pop3_next); + s->mail_state = ngx_pop3_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(pscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_pop3_auth_oauthbearer; + return ngx_mail_auth_oauthbearer(s, c, 1); + } + + ngx_str_set(&s->out, pop3_next); + s->mail_state = ngx_pop3_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_pop3_module.c b/src/mail/ngx_mail_pop3_module.c --- a/src/mail/ngx_mail_pop3_module.c +++ b/src/mail/ngx_mail_pop3_module.c @@ -30,6 +30,8 @@ static ngx_conf_bitmask_t ngx_mail_pop3 { ngx_string("apop"), NGX_MAIL_AUTH_APOP_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_null_string, 0 } }; @@ -40,6 +42,8 @@ static ngx_str_t ngx_mail_pop3_auth_met ngx_null_string, /* APOP */ ngx_string("CRAM-MD5"), ngx_string("EXTERNAL"), + ngx_string("XOAUTH2"), + ngx_string("OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -183,7 +187,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t size += sizeof("SASL") - 1 + sizeof(CRLF) - 1; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -214,7 +218,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t p = ngx_cpymem(p, "SASL", sizeof("SASL") - 1); for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -254,7 +258,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t + sizeof("." CRLF) - 1; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -279,7 +283,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t sizeof("+OK methods supported:" CRLF) - 1); for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { diff --git a/src/mail/ngx_mail_smtp_handler.c b/src/mail/ngx_mail_smtp_handler.c --- a/src/mail/ngx_mail_smtp_handler.c +++ b/src/mail/ngx_mail_smtp_handler.c @@ -548,6 +548,14 @@ ngx_mail_smtp_auth_state(ngx_event_t *re case ngx_smtp_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_smtp_auth_xoauth2: + rc = ngx_mail_auth_xoauth2(s, c, 0); + break; + + case ngx_smtp_auth_oauthbearer: + rc = ngx_mail_auth_oauthbearer(s, c, 0); + break; } } @@ -745,6 +753,38 @@ ngx_mail_smtp_auth(ngx_mail_session_t *s s->mail_state = ngx_smtp_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(sscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_smtp_auth_xoauth2; + return ngx_mail_auth_xoauth2(s, c, 1); + } + + ngx_str_set(&s->out, smtp_next); + s->mail_state = ngx_smtp_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(sscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_smtp_auth_oauthbearer; + return ngx_mail_auth_oauthbearer(s, c, 1); + } + + ngx_str_set(&s->out, smtp_next); + s->mail_state = ngx_smtp_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_smtp_module.c b/src/mail/ngx_mail_smtp_module.c --- a/src/mail/ngx_mail_smtp_module.c +++ b/src/mail/ngx_mail_smtp_module.c @@ -22,6 +22,8 @@ static ngx_conf_bitmask_t ngx_mail_smtp { ngx_string("login"), NGX_MAIL_AUTH_LOGIN_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_string("none"), NGX_MAIL_AUTH_NONE_ENABLED }, { ngx_null_string, 0 } }; @@ -33,6 +35,8 @@ static ngx_str_t ngx_mail_smtp_auth_met ngx_null_string, /* APOP */ ngx_string("CRAM-MD5"), ngx_string("EXTERNAL"), + ngx_string("XOAUTH2"), + ngx_string("OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -210,7 +214,7 @@ ngx_mail_smtp_merge_srv_conf(ngx_conf_t auth_enabled = 0; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { @@ -253,7 +257,7 @@ ngx_mail_smtp_merge_srv_conf(ngx_conf_t *p++ = 'A'; *p++ = 'U'; *p++ = 'T'; *p++ = 'H'; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { -- Maxim Dounin http://mdounin.ru/