merchant

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

do_solve_mfa_challenge.sql (4168B)


      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 IS NULL) AS not_sent
     39    ,(tc.code = in_solution) AS solved
     40   INTO
     41     my_rec
     42   FROM merchant.tan_challenges tc
     43   WHERE tc.challenge_id = in_challenge_id
     44     AND tc.h_body = in_h_body
     45     AND tc.expiration_date > in_now;
     46 
     47   IF NOT FOUND
     48   THEN
     49     out_solved = FALSE;
     50     RETURN;
     51   END IF;
     52 
     53   my_confirmation_date = my_rec.confirmation_date;
     54   out_retry_counter = my_rec.retry_counter;
     55   out_solved = my_rec.solved;
     56 
     57   -- Check if already solved before.
     58   -- NOTE: this is deliberately idempotent: once the challenge is confirmed,
     59   -- ANY solution (including a wrong one) yields out_solved=TRUE and the
     60   -- retry counter is left alone.  This is intentional and safe:
     61   --  * authorization is not granted here.  It is granted by
     62   --    get_mfa_challenge() reporting a non-NULL confirmation_date, which this
     63   --    branch does not change; re-confirming an already confirmed challenge
     64   --    therefore grants nothing that the caller did not already have.
     65   --  * reaching this branch already requires knowing in_challenge_id AND the
     66   --    matching in_h_body (a hash over the request body with a 32-byte random
     67   --    server-side salt), which together are the capability that authorizes
     68   --    the operation in the first place.
     69   --  * not decrementing the retry counter here keeps a client that retries a
     70   --    confirmation after a lost response from burning its attempts, and
     71   --    avoids turning this endpoint into a TAN-guessing oracle on a challenge
     72   --    whose code has already been used.
     73   IF my_confirmation_date IS NOT NULL
     74   THEN
     75     out_solved = TRUE;
     76     RETURN;
     77   END IF;
     78 
     79   -- No TAN was sent for this challenge yet, so there is no code that
     80   -- in_solution could match.  Nothing was guessed, so the retry counter
     81   -- is left alone.
     82   IF my_rec.not_sent
     83   THEN
     84     out_solved = FALSE;
     85     RETURN;
     86   END IF;
     87 
     88   IF (0 = out_retry_counter)
     89   THEN
     90     out_solved = FALSE;
     91     RETURN;
     92   END IF;
     93 
     94   IF out_solved
     95   THEN
     96     -- Newly solved, update DB!
     97     my_confirmation_date = in_now;
     98     UPDATE merchant.tan_challenges
     99        SET confirmation_date = my_confirmation_date
    100      WHERE challenge_id = in_challenge_id;
    101   ELSE
    102     -- Failed to solve, decrement retry counter
    103     out_retry_counter = out_retry_counter - 1;
    104     UPDATE merchant.tan_challenges
    105        SET retry_counter = out_retry_counter
    106      WHERE challenge_id = in_challenge_id;
    107   END IF;
    108 END;
    109 $$;
    110 
    111 COMMENT ON FUNCTION merchant_do_solve_mfa_challenge(INT8, BYTEA, TEXT, INT8)
    112   IS 'Checks in_solution against the code of the challenge identified by'
    113      ' in_challenge_id and in_h_body, and confirms the challenge if it'
    114      ' matches. Returns out_solved=FALSE with a NULL out_retry_counter if no'
    115      ' such (unexpired) challenge exists. A challenge for which no TAN was'
    116      ' sent yet (code is NULL) is never solved and costs no attempt.'
    117      ' Confirming an already confirmed'
    118      ' challenge is idempotent: out_solved=TRUE is returned regardless of'
    119      ' in_solution and the retry counter is not decremented.';