merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

do_solve_mfa_challenge.sql (3785B)


      1 --
      2 -- This file is part of TALER
      3 -- Copyright (C) 2025 Taler Systems SA
      4 --
      5 -- TALER is free software; you can redistribute it and/or modify it under the
      6 -- terms of the GNU General Public License as published by the Free Software
      7 -- Foundation; either version 3, or (at your option) any later version.
      8 --
      9 -- TALER 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 General Public License for more details.
     12 --
     13 -- You should have received a copy of the GNU General Public License along with
     14 -- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 --
     16 
     17 DROP FUNCTION IF EXISTS merchant_do_solve_mfa_challenge;
     18 CREATE FUNCTION merchant_do_solve_mfa_challenge (
     19   IN in_challenge_id INT8,
     20   IN in_h_body BYTEA,
     21   IN in_solution TEXT,
     22   IN in_now INT8,
     23   OUT out_solved BOOLEAN,
     24   OUT out_retry_counter INT4
     25 )
     26 LANGUAGE plpgsql
     27 AS $$
     28 DECLARE
     29   my_confirmation_date INT8;
     30 DECLARE
     31   my_rec RECORD;
     32 BEGIN
     33 
     34   -- Check if challenge exists and matches
     35   SELECT
     36     tc.confirmation_date
     37    ,tc.retry_counter
     38    ,(tc.code = in_solution) AS solved
     39   INTO
     40     my_rec
     41   FROM merchant.tan_challenges tc
     42   WHERE tc.challenge_id = in_challenge_id
     43     AND tc.h_body = in_h_body
     44     AND tc.expiration_date > in_now;
     45 
     46   IF NOT FOUND
     47   THEN
     48     out_solved = FALSE;
     49     RETURN;
     50   END IF;
     51 
     52   my_confirmation_date = my_rec.confirmation_date;
     53   out_retry_counter = my_rec.retry_counter;
     54   out_solved = my_rec.solved;
     55 
     56   -- Check if already solved before.
     57   -- NOTE: this is deliberately idempotent: once the challenge is confirmed,
     58   -- ANY solution (including a wrong one) yields out_solved=TRUE and the
     59   -- retry counter is left alone.  This is intentional and safe:
     60   --  * authorization is not granted here.  It is granted by
     61   --    get_mfa_challenge() reporting a non-NULL confirmation_date, which this
     62   --    branch does not change; re-confirming an already confirmed challenge
     63   --    therefore grants nothing that the caller did not already have.
     64   --  * reaching this branch already requires knowing in_challenge_id AND the
     65   --    matching in_h_body (a hash over the request body with a 32-byte random
     66   --    server-side salt), which together are the capability that authorizes
     67   --    the operation in the first place.
     68   --  * not decrementing the retry counter here keeps a client that retries a
     69   --    confirmation after a lost response from burning its attempts, and
     70   --    avoids turning this endpoint into a TAN-guessing oracle on a challenge
     71   --    whose code has already been used.
     72   IF my_confirmation_date IS NOT NULL
     73   THEN
     74     out_solved = TRUE;
     75     RETURN;
     76   END IF;
     77 
     78   IF (0 = out_retry_counter)
     79   THEN
     80     out_solved = FALSE;
     81     RETURN;
     82   END IF;
     83 
     84   IF out_solved
     85   THEN
     86     -- Newly solved, update DB!
     87     my_confirmation_date = in_now;
     88     UPDATE merchant.tan_challenges
     89        SET confirmation_date = my_confirmation_date
     90      WHERE challenge_id = in_challenge_id;
     91   ELSE
     92     -- Failed to solve, decrement retry counter
     93     out_retry_counter = out_retry_counter - 1;
     94     UPDATE merchant.tan_challenges
     95        SET retry_counter = out_retry_counter
     96      WHERE challenge_id = in_challenge_id;
     97   END IF;
     98 END;
     99 $$;
    100 
    101 COMMENT ON FUNCTION merchant_do_solve_mfa_challenge(INT8, BYTEA, TEXT, INT8)
    102   IS 'Checks in_solution against the code of the challenge identified by'
    103      ' in_challenge_id and in_h_body, and confirms the challenge if it'
    104      ' matches. Returns out_solved=FALSE with a NULL out_retry_counter if no'
    105      ' such (unexpired) challenge exists. Confirming an already confirmed'
    106      ' challenge is idempotent: out_solved=TRUE is returned regardless of'
    107      ' in_solution and the retry counter is not decremented.';