challenger

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

do_challenge_address.sql (7915B)


      1 --
      2 -- This file is part of TALER
      3 -- Copyright (C) 2024 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 
     18 DROP FUNCTION IF EXISTS challenger_do_challenge_set_address_and_pin;
     19 CREATE FUNCTION challenger_do_challenge_set_address_and_pin (
     20   IN in_nonce BYTEA,
     21   IN in_address TEXT,
     22   -- Newest last_tx_time for which a (re)transmission is still due, that is
     23   -- 'now - retransmission_frequency'.  Computed by the caller because
     24   -- last_tx_time is only known here; see do_challenge_address.c.
     25   IN in_retransmit_cutoff INT8,
     26   IN in_now INT8,
     27   IN in_tan INT4,
     28   OUT out_not_found BOOLEAN,
     29   OUT out_last_tx_time INT8,
     30   OUT out_last_pin INT4,
     31   OUT out_state TEXT,
     32   OUT out_pin_transmit BOOLEAN,
     33   OUT out_auth_attempts_left INT4,
     34   -- How many TAN transmissions are left *after* this call?  Note that
     35   -- this is a different budget from out_auth_attempts_left, which counts
     36   -- the guesses the user has on the current TAN.
     37   OUT out_pin_transmissions_left INT4,
     38   OUT out_client_redirect_uri TEXT,
     39   OUT out_address_refused BOOLEAN,
     40   OUT out_solved BOOLEAN,
     41   -- TRUE if the validation failed permanently: no address change, TAN
     42   -- transmission or TAN attempt is left.
     43   OUT out_failed BOOLEAN)
     44 LANGUAGE plpgsql
     45 AS $$
     46 DECLARE
     47   my_status RECORD;
     48   my_do_update BOOL;
     49   my_address_differs BOOL;
     50 BEGIN
     51 
     52 my_do_update = FALSE;
     53 
     54 SELECT address
     55       ,address_attempts_left
     56       ,pin_transmissions_left
     57       ,last_tx_time
     58       ,client_redirect_uri
     59       ,last_pin
     60       ,pending_pin
     61       ,auth_attempts_left
     62       ,client_state
     63       ,failed
     64   INTO my_status
     65   FROM validations
     66  WHERE nonce=in_nonce
     67    AND expiration_time > in_now
     68    FOR UPDATE;
     69 
     70 IF NOT FOUND
     71 THEN
     72   out_not_found=TRUE;
     73   out_last_tx_time=0;
     74   out_last_pin=NULL;
     75   out_pin_transmit=FALSE;
     76   out_auth_attempts_left=0;
     77   out_pin_transmissions_left=0;
     78   out_client_redirect_uri=NULL;
     79   out_address_refused=TRUE;
     80   out_solved=FALSE;
     81   out_failed=FALSE;
     82   out_state=NULL;
     83   RETURN;
     84 END IF;
     85 out_not_found=FALSE;
     86 out_failed=FALSE;
     87 out_last_tx_time=my_status.last_tx_time;
     88 out_last_pin=my_status.last_pin;
     89 out_pin_transmit=FALSE;
     90 out_auth_attempts_left=my_status.auth_attempts_left;
     91 out_pin_transmissions_left=my_status.pin_transmissions_left;
     92 out_state=my_status.client_state;
     93 out_client_redirect_uri=my_status.client_redirect_uri;
     94 
     95 IF ( 0 > my_status.auth_attempts_left ) -- this challenge is solved
     96 THEN
     97   out_address_refused=TRUE;
     98   out_solved=TRUE;
     99   out_auth_attempts_left=0;
    100   RETURN;
    101 END IF;
    102 out_solved=FALSE;
    103 
    104 -- Once out of options, nothing this call could do helps: an address change
    105 -- is refused (address_attempts_left is 0, or the C layer refused to change
    106 -- a read-only address before calling us) and no TAN may be transmitted.
    107 -- A validation can get here without a /solve, e.g. if the helper failed on
    108 -- the last transmission after the guesses on the previous TAN were spent.
    109 IF ( my_status.failed OR
    110      ( (0 = my_status.auth_attempts_left) AND
    111        (0 = my_status.pin_transmissions_left) AND
    112        ( (0 = my_status.address_attempts_left) OR
    113          COALESCE (my_status.address::JSONB->'read_only'
    114                    = 'true'::JSONB, FALSE) ) ) )
    115 THEN
    116   IF NOT my_status.failed
    117   THEN
    118     UPDATE validations
    119        SET failed=TRUE
    120      WHERE nonce=in_nonce;
    121   END IF;
    122   out_failed=TRUE;
    123   out_address_refused=TRUE;
    124   out_last_pin=NULL;
    125   out_auth_attempts_left=0;
    126   RETURN;
    127 END IF;
    128 
    129 -- Two addresses are the same address if they are the same JSON *value*.
    130 -- Comparing the raw text instead would make a purely cosmetic difference --
    131 -- a different field order, redundant whitespace -- count as a different
    132 -- address, with two consequences.  Firstly, the daemon itself re-appends
    133 -- 'read_only' to the address as the *last* field (see
    134 -- challenger-httpd_challenge.c), so an unchanged address can come back here
    135 -- with its fields in a different order and silently cost an honest user one
    136 -- of their address attempts.  Secondly, the budget is deliberately
    137 -- 'address_attempts_left' addresses times 'pin_transmissions_left' TANs
    138 -- each; if cosmetic variants counted as distinct addresses, all of those
    139 -- messages could be aimed at a single recipient.  Comparing as JSONB also
    140 -- makes this agree with the C layer, which compares addresses with
    141 -- json_equal() in addr_equal().
    142 my_address_differs = ( (my_status.address IS NOT NULL) AND
    143                        (in_address::JSONB IS DISTINCT FROM
    144                         my_status.address::JSONB) );
    145 
    146 IF ( (0 = my_status.address_attempts_left) AND
    147      my_address_differs )
    148 THEN
    149   out_address_refused=TRUE;
    150   out_last_pin=NULL;
    151   RETURN;
    152 END IF;
    153 out_address_refused=FALSE;
    154 
    155 IF ( my_address_differs OR
    156      (my_status.address IS NULL) )
    157 THEN
    158   -- We are changing the address, update counters.  Refilling
    159   -- 'pin_transmissions_left' and clearing the retransmission cooldown is
    160   -- intentional: the user gets a fixed number of addresses and, for each
    161   -- address, a fixed number of TAN transmissions, so that a user who
    162   -- mistyped their address does not have to wait out the cooldown of a
    163   -- message that went to somebody else.
    164   my_status.address_attempts_left
    165     = GREATEST(0,my_status.address_attempts_left - 1);
    166   -- Store the canonical (JSONB) rendering, so that what is on file does not
    167   -- depend on the field order the client happened to use.
    168   my_status.address = in_address::JSONB::TEXT;
    169   my_status.pin_transmissions_left = 3;
    170   my_status.last_tx_time = 0;
    171   -- The TAN generated below is now merely 'pending' until the helper
    172   -- confirms the transmission, so -- unlike before this patch -- this call
    173   -- no longer necessarily overwrites 'last_pin'.  The TAN on file went to
    174   -- the *previous* address, so it must be dropped here: if the helper then
    175   -- fails, the user must be left without a usable TAN rather than with one
    176   -- that attests an address it was never sent to.
    177   my_status.last_pin = NULL;
    178   my_status.pending_pin = NULL;
    179   my_status.auth_attempts_left = 0;
    180   out_last_pin = NULL;
    181   out_auth_attempts_left = 0;
    182   my_do_update=TRUE;
    183 END IF;
    184 
    185 IF ( (my_status.pin_transmissions_left > 0) AND
    186      (my_status.last_tx_time <= in_retransmit_cutoff) )
    187 THEN
    188   -- Enough time has passed since the last transmission, so we are changing
    189   -- the TAN, update counters.  The new TAN is only stored as 'pending_pin':
    190   -- it is promoted to 'last_pin' (and 'auth_attempts_left' reset) by
    191   -- CHALLENGERDB_do_challenge_address_confirm_pin() once the AUTH_COMMAND
    192   -- helper confirmed the transmission.  Until then the TAN the user may
    193   -- already hold from an earlier transmission stays valid.
    194   my_status.pin_transmissions_left = my_status.pin_transmissions_left - 1;
    195   my_status.pending_pin = in_tan;
    196   my_status.last_tx_time = in_now;
    197   out_pin_transmissions_left = my_status.pin_transmissions_left;
    198   out_auth_attempts_left = 3;
    199   out_pin_transmit=TRUE;
    200   out_last_pin = in_tan;
    201   out_last_tx_time = in_now;
    202   my_do_update=TRUE;
    203 END IF;
    204 
    205 IF my_do_update
    206 THEN
    207   UPDATE validations SET
    208     address=my_status.address
    209    ,address_attempts_left=my_status.address_attempts_left
    210    ,pin_transmissions_left=my_status.pin_transmissions_left
    211    ,last_tx_time=my_status.last_tx_time
    212    ,last_pin=my_status.last_pin
    213    ,pending_pin=my_status.pending_pin
    214    ,auth_attempts_left=my_status.auth_attempts_left
    215   WHERE nonce=in_nonce;
    216 END IF;
    217 
    218 RETURN;
    219 
    220 END $$;