challenger

OAuth 2.0-based authentication service that validates user can receive messages at a certain address
Log | Files | Refs | Submodules | README | LICENSE

challenger-httpd_token.c (19170B)


      1 /*
      2   This file is part of Challenger
      3   Copyright (C) 2023 Taler Systems SA
      4 
      5   Challenger is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   Challenger is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   Challenger; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file challenger-httpd_token.c
     18  * @brief functions to handle incoming /token requests
     19  * @author Christian Grothoff
     20  */
     21 #include "platform.h"
     22 #include "challenger-httpd.h"
     23 #include <gnunet/gnunet_util_lib.h>
     24 #include <gnunet/gnunet_db_lib.h>
     25 #include "challenger-httpd_token.h"
     26 #include "challenger-httpd_common.h"
     27 #include <taler/taler_json_lib.h>
     28 #include <taler/taler_signatures.h>
     29 #include "challenger_cm_enums.h"
     30 #include "challenger-database/validation_get_pkce.h"
     31 #include "challenger-database/client_check.h"
     32 #include "challenger-database/token_add_token.h"
     33 
     34 
     35 /**
     36  * Context for a /token operation.
     37  */
     38 struct TokenContext
     39 {
     40 
     41   /**
     42    * Nonce of the validation process the request is about.
     43    */
     44   struct CHALLENGER_ValidationNonceP nonce;
     45 
     46   /**
     47    * Handle for processing uploaded data.
     48    */
     49   struct MHD_PostProcessor *pp;
     50 
     51   /**
     52    * Uploaded 'client_id' field from POST data.
     53    */
     54   char *client_id;
     55 
     56   /**
     57    * Uploaded 'client_id' field from POST data.
     58    */
     59   char *redirect_uri;
     60 
     61   /**
     62    * Uploaded 'client_secret' field from POST data.
     63    */
     64   char *client_secret;
     65 
     66   /**
     67    * Uploaded 'code' field from POST data.
     68    */
     69   char *code;
     70 
     71   /**
     72    * Uploaded 'grant_type' field from POST data.
     73    */
     74   char *grant_type;
     75 
     76   /**
     77    * Uploaded 'code_verifier' field from POST data.
     78    */
     79   char *code_verifier;
     80 };
     81 
     82 
     83 /**
     84  * Function called to clean up a backup context.
     85  *
     86  * @param cls a `struct TokenContext`
     87  */
     88 static void
     89 cleanup_ctx (void *cls)
     90 {
     91   struct TokenContext *bc = cls;
     92 
     93   if (NULL != bc->pp)
     94   {
     95     GNUNET_break_op (MHD_YES ==
     96                      MHD_destroy_post_processor (bc->pp));
     97   }
     98   GNUNET_free (bc->client_id);
     99   GNUNET_free (bc->redirect_uri);
    100   GNUNET_free (bc->client_secret);
    101   GNUNET_free (bc->code);
    102   GNUNET_free (bc->grant_type);
    103   GNUNET_free (bc->code_verifier);
    104   GNUNET_free (bc);
    105 }
    106 
    107 
    108 /**
    109  * Iterator over key-value pairs where the value may be made available
    110  * in increments and/or may not be zero-terminated.  Used for
    111  * processing POST data.
    112  *
    113  * @param cls a `struct TokenContext *`
    114  * @param kind type of the value, always #MHD_POSTDATA_KIND when called from MHD
    115  * @param key 0-terminated key for the value
    116  * @param filename name of the uploaded file, NULL if not known
    117  * @param content_type mime-type of the data, NULL if not known
    118  * @param transfer_encoding encoding of the data, NULL if not known
    119  * @param data pointer to @a size bytes of data at the
    120  *              specified offset
    121  * @param off offset of data in the overall value
    122  * @param size number of bytes in @a data available
    123  * @return #MHD_YES to continue iterating,
    124  *         #MHD_NO to abort the iteration
    125  */
    126 static enum MHD_Result
    127 post_iter (void *cls,
    128            enum MHD_ValueKind kind,
    129            const char *key,
    130            const char *filename,
    131            const char *content_type,
    132            const char *transfer_encoding,
    133            const char *data,
    134            uint64_t off,
    135            size_t size)
    136 {
    137   struct TokenContext *bc = cls;
    138   struct Map
    139   {
    140     const char *name;
    141     char **ptr;
    142   } map[] = {
    143     {
    144       .name = "client_id",
    145       .ptr = &bc->client_id
    146     },
    147     {
    148       .name = "redirect_uri",
    149       .ptr = &bc->redirect_uri
    150     },
    151     {
    152       .name = "client_secret",
    153       .ptr = &bc->client_secret
    154     },
    155     {
    156       .name = "code",
    157       .ptr = &bc->code
    158     },
    159     {
    160       .name = "grant_type",
    161       .ptr = &bc->grant_type
    162     },
    163     {
    164       .name = "code_verifier",
    165       .ptr = &bc->code_verifier
    166     },
    167     {
    168       .name = NULL,
    169       .ptr = NULL
    170     },
    171   };
    172   char **ptr = NULL;
    173   size_t slen;
    174 
    175   (void) kind;
    176   (void) filename;
    177   (void) content_type;
    178   (void) transfer_encoding;
    179   (void) off;
    180   for (unsigned int i = 0; NULL != map[i].name; i++)
    181     if (0 == strcmp (key,
    182                      map[i].name))
    183       ptr = map[i].ptr;
    184   if (NULL == ptr)
    185     return MHD_YES; /* ignore */
    186   if (NULL == *ptr)
    187     slen = 0;
    188   else
    189     slen = strlen (*ptr);
    190   if (NULL == *ptr)
    191     *ptr = GNUNET_malloc (size + 1);
    192   else
    193     *ptr = GNUNET_realloc (*ptr,
    194                            slen + size + 1);
    195   memcpy (&(*ptr)[slen],
    196           data,
    197           size);
    198   (*ptr)[slen + size] = '\0';
    199   return MHD_YES;
    200 }
    201 
    202 
    203 enum MHD_Result
    204 CH_handler_token (struct CH_HandlerContext *hc,
    205                   const char *upload_data,
    206                   size_t *upload_data_size)
    207 {
    208   struct TokenContext *bc = hc->ctx;
    209 
    210   if (NULL == bc)
    211   {
    212     /* first call, setup internals */
    213     bc = GNUNET_new (struct TokenContext);
    214     hc->cc = &cleanup_ctx;
    215     hc->ctx = bc;
    216     bc->pp = MHD_create_post_processor (hc->connection,
    217                                         2 * 1024,
    218                                         &post_iter,
    219                                         bc);
    220     TALER_MHD_check_content_length (hc->connection,
    221                                     2 * 1024);
    222     return MHD_YES;
    223   }
    224   /* handle upload */
    225   if (0 != *upload_data_size)
    226   {
    227     enum MHD_Result res;
    228 
    229     res = MHD_post_process (bc->pp,
    230                             upload_data,
    231                             *upload_data_size);
    232     *upload_data_size = 0;
    233     if (MHD_YES == res)
    234       return MHD_YES;
    235     return MHD_NO;
    236   }
    237   if ( (NULL == bc->grant_type) ||
    238        (0 != strcmp (bc->grant_type,
    239                      "authorization_code")) )
    240   {
    241     GNUNET_break_op (0);
    242     return CH_reply_with_oauth_error (
    243       hc->connection,
    244       MHD_HTTP_BAD_REQUEST,
    245       "unsupported_grant_type",
    246       TALER_EC_GENERIC_PARAMETER_MALFORMED,
    247       "authorization_code");
    248   }
    249 
    250   if (NULL == bc->code)
    251   {
    252     GNUNET_break_op (0);
    253     return CH_reply_with_oauth_error (
    254       hc->connection,
    255       MHD_HTTP_BAD_REQUEST,
    256       "invalid_request",
    257       TALER_EC_GENERIC_PARAMETER_MISSING,
    258       "code");
    259   }
    260   if (NULL == bc->client_secret)
    261   {
    262     GNUNET_break_op (0);
    263     return CH_reply_with_oauth_error (
    264       hc->connection,
    265       MHD_HTTP_BAD_REQUEST,
    266       "invalid_client",
    267       TALER_EC_GENERIC_PARAMETER_MISSING,
    268       "client_secret");
    269   }
    270   if (NULL == bc->client_id)
    271   {
    272     GNUNET_break_op (0);
    273     return CH_reply_with_oauth_error (
    274       hc->connection,
    275       MHD_HTTP_BAD_REQUEST,
    276       "invalid_client",
    277       TALER_EC_GENERIC_PARAMETER_MISSING,
    278       "client_id");
    279   }
    280   if (NULL == bc->redirect_uri)
    281   {
    282     GNUNET_break_op (0);
    283     return CH_reply_with_oauth_error (
    284       hc->connection,
    285       MHD_HTTP_BAD_REQUEST,
    286       "invalid_request",
    287       TALER_EC_GENERIC_PARAMETER_MISSING,
    288       "redirect_uri");
    289   }
    290 
    291   /* Check this client is authorized to access the service */
    292   {
    293     enum GNUNET_DB_QueryStatus qs;
    294     char *client_url = NULL;
    295     unsigned long long client_id;
    296     char dummy;
    297 
    298     if (1 != sscanf (bc->client_id,
    299                      "%llu%c",
    300                      &client_id,
    301                      &dummy))
    302     {
    303       GNUNET_break_op (0);
    304       return CH_reply_with_oauth_error (
    305         hc->connection,
    306         MHD_HTTP_BAD_REQUEST,
    307         "invalid_client",
    308         TALER_EC_GENERIC_PARAMETER_MALFORMED,
    309         "client_id");
    310     }
    311 
    312     qs = CHALLENGERDB_client_check (CH_context,
    313                                     client_id,
    314                                     bc->client_secret,
    315                                     0, /* do not increment */
    316                                     &client_url);
    317     switch (qs)
    318     {
    319     case GNUNET_DB_STATUS_HARD_ERROR:
    320       GNUNET_break (0);
    321       return TALER_MHD_reply_with_error (
    322         hc->connection,
    323         MHD_HTTP_INTERNAL_SERVER_ERROR,
    324         TALER_EC_GENERIC_DB_FETCH_FAILED,
    325         "client_check");
    326     case GNUNET_DB_STATUS_SOFT_ERROR:
    327       GNUNET_break (0);
    328       return MHD_NO;
    329     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    330       GNUNET_break_op (0);
    331       return CH_reply_with_oauth_error (
    332         hc->connection,
    333         MHD_HTTP_NOT_FOUND,
    334         "invalid_client",
    335         TALER_EC_CHALLENGER_GENERIC_CLIENT_UNKNOWN,
    336         NULL);
    337     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    338       break;
    339     }
    340     if ( (NULL != client_url) &&
    341          (0 != strcmp (client_url,
    342                        bc->redirect_uri)) )
    343     {
    344       GNUNET_break_op (0);
    345       return CH_reply_with_oauth_error (
    346         hc->connection,
    347         MHD_HTTP_UNAUTHORIZED,
    348         "invalid_client",
    349         TALER_EC_CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI,
    350         NULL);
    351     }
    352     GNUNET_free (client_url);
    353   }
    354 
    355   if (GNUNET_OK !=
    356       CH_code_to_nonce (bc->code,
    357                         &bc->nonce))
    358   {
    359     GNUNET_break_op (0);
    360     return CH_reply_with_oauth_error (
    361       hc->connection,
    362       MHD_HTTP_UNAUTHORIZED,
    363       "invalid_grant",
    364       TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE,
    365       NULL);
    366   }
    367 
    368   /* Check code is valid */
    369   {
    370     char *client_secret;
    371     json_t *address;
    372     char *client_scope;
    373     char *client_state;
    374     char *client_redirect_uri;
    375     char *code_challenge;
    376     uint32_t code_challenge_method;
    377     enum GNUNET_DB_QueryStatus qs;
    378     char *code;
    379     enum CHALLENGER_CM code_challenge_method_enum;
    380 
    381     qs = CHALLENGERDB_validation_get_pkce (CH_context,
    382                                            &bc->nonce,
    383                                            &client_secret,
    384                                            &address,
    385                                            &client_scope,
    386                                            &client_state,
    387                                            &client_redirect_uri,
    388                                            &code_challenge,
    389                                            &code_challenge_method);
    390     switch (qs)
    391     {
    392     case GNUNET_DB_STATUS_HARD_ERROR:
    393       GNUNET_break (0);
    394       return TALER_MHD_reply_with_error (
    395         hc->connection,
    396         MHD_HTTP_INTERNAL_SERVER_ERROR,
    397         TALER_EC_GENERIC_DB_FETCH_FAILED,
    398         "validation_get_pkce");
    399     case GNUNET_DB_STATUS_SOFT_ERROR:
    400       GNUNET_break (0);
    401       return MHD_NO;
    402     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    403       GNUNET_break_op (0);
    404       return CH_reply_with_oauth_error (
    405         hc->connection,
    406         MHD_HTTP_UNAUTHORIZED,
    407         "invalid_grant",
    408         TALER_EC_CHALLENGER_GENERIC_VALIDATION_UNKNOWN,
    409         "validation_get_pkce");
    410     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    411       break;
    412     }
    413 
    414     code_challenge_method_enum = CHALLENGER_cm_from_int (
    415       code_challenge_method);
    416     if (CHALLENGER_CM_UNKNOWN == code_challenge_method_enum)
    417     {
    418       GNUNET_break (0);
    419       json_decref (address);
    420       GNUNET_free (client_scope);
    421       GNUNET_free (client_secret);
    422       GNUNET_free (client_redirect_uri);
    423       GNUNET_free (client_state);
    424       GNUNET_free (code_challenge);
    425       return TALER_MHD_reply_with_error (
    426         hc->connection,
    427         MHD_HTTP_INTERNAL_SERVER_ERROR,
    428         TALER_EC_GENERIC_PARAMETER_MALFORMED,
    429         "Invalid code_challenge_method");
    430     }
    431 
    432     /* Verify the code_challenge if present*/
    433     if (NULL != code_challenge)
    434     {
    435       if (NULL == bc->code_verifier)
    436       {
    437         GNUNET_break_op (0);
    438         json_decref (address);
    439         GNUNET_free (client_scope);
    440         GNUNET_free (client_secret);
    441         GNUNET_free (client_redirect_uri);
    442         GNUNET_free (client_state);
    443         GNUNET_free (code_challenge);
    444         return CH_reply_with_oauth_error (
    445           hc->connection,
    446           MHD_HTTP_UNAUTHORIZED,
    447           "invalid_grant",
    448           TALER_EC_GENERIC_PARAMETER_MISSING,
    449           "code_verifier is missing");
    450       }
    451 
    452       switch (code_challenge_method_enum)
    453       {
    454       case CHALLENGER_CM_S256:
    455         {
    456           gcry_md_hd_t hd;
    457           unsigned char hash[32];
    458           char *encoded_hash = NULL;
    459           size_t encoded_len;
    460           const void *md;
    461 
    462           if (GPG_ERR_NO_ERROR !=
    463               gcry_md_open (&hd,
    464                             GCRY_MD_SHA256,
    465                             0))
    466           {
    467             GNUNET_break (0);
    468             json_decref (address);
    469             GNUNET_free (client_scope);
    470             GNUNET_free (client_secret);
    471             GNUNET_free (client_redirect_uri);
    472             GNUNET_free (client_state);
    473             GNUNET_free (code_challenge);
    474             return CH_reply_with_oauth_error (
    475               hc->connection,
    476               MHD_HTTP_INTERNAL_SERVER_ERROR,
    477               "server_error",
    478               TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    479               "Failed to initialize SHA256 hash function");
    480           }
    481           gcry_md_write (hd,
    482                          bc->code_verifier,
    483                          strlen (bc->code_verifier));
    484           md = gcry_md_read (hd,
    485                              0);
    486           GNUNET_assert (NULL != md);
    487           memcpy (hash,
    488                   md,
    489                   sizeof (hash));
    490           gcry_md_close (hd);
    491 
    492           encoded_len
    493             = GNUNET_STRINGS_base64url_encode (hash,
    494                                                sizeof (hash),
    495                                                &encoded_hash);
    496 
    497           if ( (0 == encoded_len) ||
    498                (NULL == encoded_hash) )
    499           {
    500             GNUNET_break (0);
    501             json_decref (address);
    502             GNUNET_free (client_scope);
    503             GNUNET_free (client_secret);
    504             GNUNET_free (client_redirect_uri);
    505             GNUNET_free (client_state);
    506             GNUNET_free (code_challenge);
    507             GNUNET_free (encoded_hash);
    508             return CH_reply_with_oauth_error (
    509               hc->connection,
    510               MHD_HTTP_INTERNAL_SERVER_ERROR,
    511               "server_error",
    512               TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
    513               "Failed to encode hash to Base64 URL");
    514           }
    515 
    516           if (0 != strcmp (encoded_hash,
    517                            code_challenge))
    518           {
    519             GNUNET_break_op (0);
    520             json_decref (address);
    521             GNUNET_free (client_scope);
    522             GNUNET_free (client_secret);
    523             GNUNET_free (client_redirect_uri);
    524             GNUNET_free (client_state);
    525             GNUNET_free (code_challenge);
    526             GNUNET_free (encoded_hash);
    527             return CH_reply_with_oauth_error (
    528               hc->connection,
    529               MHD_HTTP_UNAUTHORIZED,
    530               "invalid_grant",
    531               TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE,
    532               "code_verifier does not match code_challenge (SHA256)");
    533           }
    534           GNUNET_free (encoded_hash);
    535         }
    536         break;
    537       case CHALLENGER_CM_PLAIN:
    538         {
    539           if (0 != strcmp (bc->code_verifier,
    540                            code_challenge))
    541           {
    542             GNUNET_break_op (0);
    543             json_decref (address);
    544             GNUNET_free (client_scope);
    545             GNUNET_free (client_secret);
    546             GNUNET_free (client_redirect_uri);
    547             GNUNET_free (client_state);
    548             GNUNET_free (code_challenge);
    549             return CH_reply_with_oauth_error (
    550               hc->connection,
    551               MHD_HTTP_UNAUTHORIZED,
    552               "invalid_grant",
    553               TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE,
    554               "code_verifier does not match code_challenge (PLAIN)");
    555           }
    556         }
    557         break;
    558       case CHALLENGER_CM_UNKNOWN:
    559       case CHALLENGER_CM_EMPTY:
    560         GNUNET_break (0);
    561         json_decref (address);
    562         GNUNET_free (client_scope);
    563         GNUNET_free (client_secret);
    564         GNUNET_free (client_redirect_uri);
    565         GNUNET_free (client_state);
    566         GNUNET_free (code_challenge);
    567         return CH_reply_with_oauth_error (
    568           hc->connection,
    569           MHD_HTTP_INTERNAL_SERVER_ERROR,
    570           "server_error",
    571           TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
    572           "Database has empty or unknown challenge mode but with code_challenge");
    573       }
    574     }
    575 
    576     if (NULL == address)
    577     {
    578       GNUNET_break_op (0);
    579       GNUNET_free (client_scope);
    580       GNUNET_free (client_secret);
    581       GNUNET_free (client_redirect_uri);
    582       GNUNET_free (client_state);
    583       GNUNET_free (code_challenge);
    584       return CH_reply_with_oauth_error (
    585         hc->connection,
    586         MHD_HTTP_CONFLICT,
    587         "invalid_request",
    588         TALER_EC_CHALLENGER_MISSING_ADDRESS,
    589         "code");
    590     }
    591     code = CH_compute_code (&bc->nonce,
    592                             client_secret,
    593                             client_scope,
    594                             address,
    595                             client_redirect_uri);
    596     json_decref (address);
    597     GNUNET_free (client_scope);
    598     GNUNET_free (client_secret);
    599     GNUNET_free (client_redirect_uri);
    600     GNUNET_free (client_state);
    601     GNUNET_free (code_challenge);
    602     if (0 != strcmp (code,
    603                      bc->code))
    604     {
    605       GNUNET_break_op (0);
    606       GNUNET_free (code);
    607       return CH_reply_with_oauth_error (
    608         hc->connection,
    609         MHD_HTTP_UNAUTHORIZED,
    610         "invalid_grant",
    611         TALER_EC_CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE,
    612         "code");
    613     }
    614     GNUNET_free (code);
    615   }
    616 
    617   {
    618     struct CHALLENGER_AccessTokenP token;
    619     enum GNUNET_DB_QueryStatus qs;
    620 
    621     GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
    622                                 &token,
    623                                 sizeof (token));
    624     qs = CHALLENGERDB_token_add_token (CH_context,
    625                                        &bc->nonce,
    626                                        &token,
    627                                        CH_token_expiration,
    628                                        CH_validation_expiration);
    629     switch (qs)
    630     {
    631     case GNUNET_DB_STATUS_HARD_ERROR:
    632       GNUNET_break (0);
    633       return TALER_MHD_reply_with_error (
    634         hc->connection,
    635         MHD_HTTP_INTERNAL_SERVER_ERROR,
    636         TALER_EC_GENERIC_DB_STORE_FAILED,
    637         "token_add_token");
    638     case GNUNET_DB_STATUS_SOFT_ERROR:
    639       GNUNET_break (0);
    640       return MHD_NO;
    641     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    642       GNUNET_break (0);
    643       return CH_reply_with_oauth_error (
    644         hc->connection,
    645         MHD_HTTP_UNAUTHORIZED,
    646         "invalid_grant",
    647         TALER_EC_CHALLENGER_GRANT_UNKNOWN,
    648         "token_add_token");
    649     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    650       break;
    651     }
    652 
    653     return TALER_MHD_REPLY_JSON_PACK (
    654       hc->connection,
    655       MHD_HTTP_OK,
    656       GNUNET_JSON_pack_data_auto ("access_token",
    657                                   &token),
    658       GNUNET_JSON_pack_string ("token_type",
    659                                "Bearer"),
    660       GNUNET_JSON_pack_uint64 ("expires_in",
    661                                CH_token_expiration.rel_value_us
    662                                / GNUNET_TIME_UNIT_SECONDS.rel_value_us));
    663   }
    664 }