]> dgit.raspbian.org Git - dovecot.git/commitdiff
[PATCH 3/4] auth: db-oauth2: Add oauth2_audience setting, deprecate aud-as-scope...
authorAki Tuomi <aki.tuomi@open-xchange.com>
Fri, 29 May 2026 07:31:50 +0000 (07:31 +0000)
committerNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
Add a dedicated oauth2_audience setting checked against the token's aud
claim (RFC 7519 section 4.1.3) using the same tab-split AND-semantics as
oauth2_scope. Emit a deprecation warning when the existing aud fallback
in db_oauth2_token_in_scope() is triggered so operators know to migrate.

Gbp-Pq: Name 0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch

src/auth/db-oauth2.c
src/auth/db-oauth2.h

index 159344f9ad6b0518863c077d1f7d80494ec1825b..8bf2618bc012e9f011bbb24555bdc8456863bcd4 100644 (file)
 #include "db-oauth2.h"
 #include "dcrypt.h"
 #include "dict.h"
+#include <time.h>
 
 #undef DEF
 #define DEF(type, name) \
        SETTING_DEFINE_STRUCT_##type("oauth2_"#name, name, struct auth_oauth2_settings)
 
+#define WARN_AUD_FALLBACK_PERIOD 600
+static time_t last_warned_aud_fallback = 0;
+
 static const struct setting_define auth_oauth2_setting_defines[] = {
        DEF(STR, tokeninfo_url),
        DEF(STR, grant_url),
        DEF(STR, introspection_url),
        DEF(BOOLLIST, scope),
+       DEF(BOOLLIST, audience),
        DEF(ENUM, introspection_mode),
        DEF(STR_NOVARS, username_validation_format),
        DEF(STR, username_attribute),
@@ -50,6 +55,7 @@ static const struct auth_oauth2_settings auth_oauth2_default_settings = {
        .grant_url = "",
        .introspection_url = "",
        .scope = ARRAY_INIT,
+       .audience = ARRAY_INIT,
        .force_introspection = FALSE,
        .introspection_mode = ":auth:get:post:local",
        .username_validation_format = "%{user}",
@@ -558,8 +564,19 @@ db_oauth2_token_in_scope(struct db_oauth2_request *req,
 
        const char *value = auth_fields_find(req->fields, "scope");
        bool has_scope = value != NULL;
-       if (!has_scope)
+       if (!has_scope && array_is_empty(&req->db->set->audience)) {
                value = auth_fields_find(req->fields, "aud");
+               if (value != NULL) {
+                       time_t t0 = time(NULL);
+                       if (t0 - last_warned_aud_fallback > WARN_AUD_FALLBACK_PERIOD) {
+                               e_warning(authdb_event(req->auth_request),
+                                         "Token has no 'scope' claim; falling back to "
+                                         "'aud' for scope check is deprecated - "
+                                         "use oauth2_audience instead");
+                               last_warned_aud_fallback = t0;
+                       }
+               }
+       }
        e_debug(authdb_event(req->auth_request),
                "Token scope(s): %s", value);
 
@@ -585,6 +602,37 @@ db_oauth2_token_in_scope(struct db_oauth2_request *req,
        return found;
 }
 
+static bool
+db_oauth2_token_in_audience(struct db_oauth2_request *req,
+                           enum passdb_result *result_r, const char **error_r)
+{
+       if (array_is_empty(&req->db->set->audience))
+               return TRUE;
+
+       const char *value = auth_fields_find(req->fields, "aud");
+       e_debug(authdb_event(req->auth_request),
+               "Token audience(s): %s", value != NULL ? value : "(none)");
+
+       bool found = FALSE;
+       if (value != NULL && *value != '\0') {
+               const char *const *entries = t_strsplit_tabescaped(value);
+               const char *wanted;
+               found = TRUE;
+               array_foreach_elem(&req->db->set->audience, wanted) {
+                       if (!str_array_find(entries, wanted)) {
+                               found = FALSE;
+                               break;
+                       }
+               }
+       }
+       if (!found) {
+               *error_r = t_strdup_printf("Token audience does not include '%s'",
+                       t_array_const_string_join(&req->db->set->audience, " "));
+               *result_r = PASSDB_RESULT_USER_DISABLED;
+       }
+       return found;
+}
+
 static void db_oauth2_process_fields(struct db_oauth2_request *req,
                                     enum passdb_result *result_r,
                                     const char **error_r)
@@ -593,7 +641,8 @@ static void db_oauth2_process_fields(struct db_oauth2_request *req,
 
        if (db_oauth2_user_is_enabled(req, result_r, error_r) &&
            db_oauth2_validate_username(req, result_r, error_r) &&
-           db_oauth2_token_in_scope(req, result_r, error_r)) {
+           db_oauth2_token_in_scope(req, result_r, error_r) &&
+           db_oauth2_token_in_audience(req, result_r, error_r)) {
                /* The user has now been successfully authenticated,
                   mark the request as such. This allows having no
                   passdb in config. */
index 3f362c61ddb21629c9f6e32fb0393cb16d31a8f4..fe503da647affa1dedee36aa89096bcb115e4ac7 100644 (file)
@@ -15,6 +15,8 @@ struct auth_oauth2_settings {
        const char *introspection_url;
        /* expected scope(s), optional */
        ARRAY_TYPE(const_string) scope;
+       /* expected audience(s) (aud claim), optional */
+       ARRAY_TYPE(const_string) audience;
        /* mode of introspection, one of auth, get, post, local
           - auth: send token with header Authorization: Bearer token
           - get: append token to url