merchant

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

test_merchant_mfa.sh (21787B)


      1 #!/usr/bin/env bash
      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
      6 # it under the terms of the GNU General Public License as
      7 # published by the Free Software Foundation; either version 3, or
      8 # (at your option) any later version.
      9 #
     10 # TALER is distributed in the hope that it will be useful, but
     11 # WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU General Public
     16 # License along with TALER; see the file COPYING.  If not, see
     17 # <http://www.gnu.org/licenses/>
     18 #
     19 set -eu
     20 
     21 . setup.sh
     22 
     23 
     24 echo -n "Reject password-change MFA without mandatory TAN channels "
     25 INVALID_CONFIG=$(mktemp -p "${TMPDIR:-/tmp}" test_merchant_mfa.conf-XXXXXX)
     26 INVALID_LOG=$(mktemp -p "${TMPDIR:-/tmp}" test_merchant_mfa.log-XXXXXX)
     27 cp test_merchant_mfa.conf "$INVALID_CONFIG"
     28 sed -i '/^MANDATORY_TAN_CHANNELS[[:space:]]*=/d' "$INVALID_CONFIG"
     29 STATUS=0
     30 timeout 10 taler-merchant-httpd -L INFO -c "$INVALID_CONFIG" \
     31     > "$INVALID_LOG" 2>&1 || STATUS=$?
     32 if [ "$STATUS" = "0" ] ||
     33    [ "$STATUS" = "124" ] ||
     34    ! grep -q \
     35        'requires at least one MANDATORY_TAN_CHANNELS entry' \
     36        "$INVALID_LOG"
     37 then
     38     cat "$INVALID_LOG"
     39     rm -f "$INVALID_CONFIG" "$INVALID_LOG"
     40     exit_fail "Expected invalid password-change MFA configuration to fail"
     41 fi
     42 rm -f "$INVALID_CONFIG" "$INVALID_LOG"
     43 echo "OK"
     44 
     45 
     46 # Launch system.
     47 setup \
     48     -c "test_merchant_mfa.conf" \
     49     -m
     50 LAST_RESPONSE=$(mktemp -p "${TMPDIR:-/tmp}" test_response.conf-XXXXXX)
     51 
     52 solve_challenge()
     53 {
     54     CHALLENGE_ID=$1
     55     CHANNEL=$2
     56     EXPECTED_ADDRESS=$3
     57 
     58     rm -f "/tmp/test-merchant-$CHANNEL-tan.txt" \
     59         "/tmp/test-merchant-$CHANNEL-address.txt"
     60     STATUS=$(curl -H "Content-Type: application/json" -X POST \
     61         "http://localhost:9966/challenge/$CHALLENGE_ID" \
     62         -d '{}' \
     63         -w "%{http_code}" -s \
     64         -o "$LAST_RESPONSE")
     65     if [ "$STATUS" != "200" ]
     66     then
     67         jq < "$LAST_RESPONSE"
     68         exit_fail "Expected challenge transmission to return 200 OK. Got: $STATUS"
     69     fi
     70 
     71     TAN=$(head -n1 "/tmp/test-merchant-$CHANNEL-tan.txt" | awk '{print $1}')
     72     ADDRESS=$(cat "/tmp/test-merchant-$CHANNEL-address.txt")
     73     if [ "$ADDRESS" != "$EXPECTED_ADDRESS" ]
     74     then
     75         exit_fail "Expected $CHANNEL address '$EXPECTED_ADDRESS'. Got: $ADDRESS"
     76     fi
     77 
     78     STATUS=$(curl -H "Content-Type: application/json" -X POST \
     79         "http://localhost:9966/challenge/$CHALLENGE_ID/confirm" \
     80         -d '{"tan":"'"$TAN"'"}' \
     81         -w "%{http_code}" -s \
     82         -o "$LAST_RESPONSE")
     83     if [ "$STATUS" != "204" ]
     84     then
     85         jq < "$LAST_RESPONSE"
     86         exit_fail "Expected challenge confirmation to return 204 No Content. Got: $STATUS"
     87     fi
     88 }
     89 
     90 
     91 solve_response_challenges()
     92 {
     93     RESPONSE=$1
     94     while IFS=$'\t' read -r CHALLENGE_ID CHANNEL
     95     do
     96         case "$CHANNEL" in
     97         email)
     98             solve_challenge "$CHALLENGE_ID" "$CHANNEL" "self@example.com"
     99             ;;
    100         sms)
    101             solve_challenge "$CHALLENGE_ID" "$CHANNEL" "+4171234"
    102             ;;
    103         *)
    104             exit_fail "Unexpected TAN channel: $CHANNEL"
    105             ;;
    106         esac
    107     done < <(jq -r '.challenges[] | [.challenge_id, .tan_channel] | @tsv' "$RESPONSE")
    108 }
    109 
    110 echo -n "Configuring a merchant admin instance ..."
    111 
    112 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    113     -H 'Authorization: Bearer secret-token:super_secret' \
    114     http://localhost:9966/management/instances \
    115     -d '{"auth":{"method":"external"},"id":"admin","name":"default","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \
    116     -w "%{http_code}" -s \
    117     -o "$LAST_RESPONSE")
    118 
    119 if [ "$STATUS" != "204" ]
    120 then
    121     jq < "$LAST_RESPONSE"
    122     exit_fail "Expected 204 ok, instance created. got: $STATUS"
    123 fi
    124 
    125 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    126     -H 'Authorization: Bearer secret-token:super_secret' \
    127     http://localhost:9966/private/accounts \
    128     -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/43?receiver-name=user43"}' \
    129     -w "%{http_code}" -s \
    130     -o "$LAST_RESPONSE")
    131 
    132 
    133 if [ "$STATUS" != "200" ]
    134 then
    135     jq < "$LAST_RESPONSE"
    136     exit_fail "Expected 200 OK. Got: $STATUS"
    137 fi
    138 
    139 echo " OK"
    140 echo -n "Self-provision instance ..."
    141 
    142 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    143     http://localhost:9966/instances \
    144     -d '{"auth":{"method":"token", "password":"pass1234"},"id":"self","name":"default","phone_number":"+4171234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \
    145     -w "%{http_code}" -s \
    146     -o "$LAST_RESPONSE")
    147 
    148 if [ "$STATUS" != "202" ]
    149 then
    150     jq < "$LAST_RESPONSE"
    151     exit_fail "Expected 202 Accepted. Got: $STATUS"
    152 fi
    153 echo " OK"
    154 
    155 C1=$(jq -r .challenges[0].challenge_id < "$LAST_RESPONSE")
    156 C2=$(jq -r .challenges[1].challenge_id < "$LAST_RESPONSE")
    157 
    158 echo -n "Requesting challenge $C1 "
    159 
    160 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    161     "http://localhost:9966/challenge/$C1" \
    162     -d '{}' \
    163     -w "%{http_code}" -s \
    164     -o "$LAST_RESPONSE")
    165 
    166 if [ "$STATUS" != "200" ]
    167 then
    168     jq < "$LAST_RESPONSE"
    169     exit_fail "Expected 200 OK. Got: $STATUS"
    170 fi
    171 echo "OK"
    172 
    173 TAN=$(cat /tmp/test-merchant-email-tan.txt | head -n1 | awk '{print $1}')
    174 ADDR=$(cat /tmp/test-merchant-email-address.txt)
    175 
    176 if [ "$ADDR" != "self@example.com" ]
    177 then
    178     exit_fail "Expected address 'self@example.com'. Got: $ADDR"
    179 fi
    180 
    181 echo -n "Sending challenge $C1 solution "
    182 
    183 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    184     "http://localhost:9966/challenge/$C1/confirm" \
    185     -d '{"tan":"'"$TAN"'"}' \
    186     -w "%{http_code}" -s \
    187     -o "$LAST_RESPONSE")
    188 
    189 if [ "$STATUS" != "204" ]
    190 then
    191     jq < "$LAST_RESPONSE"
    192     exit_fail "Expected 204 OK. Got: $STATUS"
    193 fi
    194 echo "OK"
    195 
    196 echo -n "Requesting challenge $C2 "
    197 
    198 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    199     "http://localhost:9966/challenge/$C2" \
    200     -d '{}' \
    201     -w "%{http_code}" -s \
    202     -o "$LAST_RESPONSE")
    203 
    204 if [ "$STATUS" != "200" ]
    205 then
    206     jq < "$LAST_RESPONSE"
    207     exit_fail "Expected 200 OK. Got: $STATUS"
    208 fi
    209 echo "OK"
    210 
    211 TAN=$(cat /tmp/test-merchant-sms-tan.txt | head -n1 | awk '{print $1}')
    212 
    213 echo -n "Sending challenge $C2 solution "
    214 
    215 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    216     "http://localhost:9966/challenge/$C2/confirm" \
    217     -d '{"tan":"'"$TAN"'"}' \
    218     -w "%{http_code}" -s \
    219     -o "$LAST_RESPONSE")
    220 
    221 if [ "$STATUS" != "204" ]
    222 then
    223     jq < "$LAST_RESPONSE"
    224     exit_fail "Expected 204 OK. Got: $STATUS"
    225 fi
    226 echo "OK"
    227 
    228 
    229 echo -n "Retrying instance creation with other body "
    230 
    231 STATUS=$(curl \
    232     -H "Content-Type: application/json" \
    233     -H "Taler-Challenge-Ids: $C1,$C2" \
    234     -X POST \
    235     http://localhost:9966/instances \
    236     -d '{"auth":{"method":"external"},"id":"self","name":"change","phone_number":"+4171234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \
    237     -w "%{http_code}" -s \
    238     -o "$LAST_RESPONSE")
    239 
    240 if [ "$STATUS" != "202" ]
    241 then
    242     jq < "$LAST_RESPONSE"
    243     exit_fail "Expected 202 Accepted. Got: $STATUS"
    244 fi
    245 echo "OK"
    246 
    247 
    248 
    249 echo -n "Retrying instance creation with original body "
    250 
    251 STATUS=$(curl \
    252     -H "Content-Type: application/json" \
    253     -H "Taler-Challenge-Ids: $C1,$C2" \
    254     -X POST \
    255     http://localhost:9966/instances \
    256     -d '{"auth":{"method":"token", "password":"pass1234"},"id":"self","name":"default","phone_number":"+4171234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \
    257     -w "%{http_code}" -s \
    258     -o "$LAST_RESPONSE")
    259 
    260 if [ "$STATUS" != "204" ]
    261 then
    262     jq < "$LAST_RESPONSE"
    263     exit_fail "Expected 204 OK. Got: $STATUS"
    264 fi
    265 
    266 echo "OK"
    267 
    268 
    269 echo -n "Unauthorized trigger MFA to add bank account "
    270 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    271     -H 'Authorization: Bearer secret-token:bad_password' \
    272     http://localhost:9966/instances/self/private/accounts \
    273     -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/44?receiver-name=user44"}' \
    274     -w "%{http_code}" -s \
    275     -o "$LAST_RESPONSE")
    276 
    277 if [ "$STATUS" != "401" ]
    278 then
    279     jq < "$LAST_RESPONSE"
    280     exit_fail "Expected 401 Forbidden. Got: $STATUS"
    281 fi
    282 
    283 echo " OK"
    284 
    285 
    286 echo -n "Do NOT Trigger MFA to add first bank account "
    287 STATUS=$(curl \
    288     -H "Content-Type: application/json" \
    289     -X POST \
    290     -H 'Authorization: Bearer secret-token:pass1234' \
    291     http://localhost:9966/instances/self/private/accounts \
    292     -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/44?receiver-name=user44"}' \
    293     -w "%{http_code}" -s \
    294     -o "$LAST_RESPONSE")
    295 
    296 if [ "$STATUS" != "200" ]
    297 then
    298     jq < "$LAST_RESPONSE"
    299     exit_fail "Expected 200 OK. Got: $STATUS"
    300 fi
    301 
    302 echo " OK"
    303 
    304 
    305 echo -n "Trigger MFA to add 2nd bank account with 2-FA authorization "
    306 STATUS=$(curl \
    307     -H "Content-Type: application/json" \
    308     -X POST \
    309     -H 'Authorization: Bearer secret-token:pass1234' \
    310     http://localhost:9966/instances/self/private/accounts \
    311     -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/45?receiver-name=user45"}' \
    312     -w "%{http_code}" -s \
    313     -o "$LAST_RESPONSE")
    314 
    315 if [ "$STATUS" != "202" ]
    316 then
    317     jq < "$LAST_RESPONSE"
    318     exit_fail "Expected 202 Accepted. Got: $STATUS"
    319 fi
    320 
    321 echo " OK"
    322 
    323 
    324 C1=$(jq -r .challenges[0].challenge_id < "$LAST_RESPONSE")
    325 
    326 # Delete old TANs.
    327 rm /tmp/test-merchant-*-tan.txt
    328 echo -n "Requesting challenge $C1 "
    329 
    330 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    331     "http://localhost:9966/challenge/$C1" \
    332     -d '{}' \
    333     -w "%{http_code}" -s \
    334     -o "$LAST_RESPONSE")
    335 
    336 if [ "$STATUS" != "200" ]
    337 then
    338     jq < "$LAST_RESPONSE"
    339     exit_fail "Expected 200 OK. Got: $STATUS"
    340 fi
    341 echo "OK"
    342 
    343 TAN=$(cat /tmp/test-merchant-email-tan.txt | head -n1 | awk '{print $1}')
    344 ADDR=$(cat /tmp/test-merchant-email-address.txt)
    345 
    346 if [ "$ADDR" != "self@example.com" ]
    347 then
    348     exit_fail "Expected address 'self@example.com'. Got: $ADDR"
    349 fi
    350 
    351 echo -n "Sending challenge $C1 solution "
    352 
    353 STATUS=$(curl \
    354     -H "Content-Type: application/json" \
    355     -X POST \
    356     "http://localhost:9966/challenge/$C1/confirm" \
    357     -d '{"tan":"'"$TAN"'"}' \
    358     -w "%{http_code}" -s \
    359     -o "$LAST_RESPONSE")
    360 
    361 if [ "$STATUS" != "204" ]
    362 then
    363     jq < "$LAST_RESPONSE"
    364     exit_fail "Expected 204 OK. Got: $STATUS"
    365 fi
    366 echo "OK"
    367 
    368 echo -n "Finally, add 2nd bank account "
    369 STATUS=$(curl \
    370     -X POST \
    371     -H "Content-Type: application/json" \
    372     -H "Taler-Challenge-Ids: $C1" \
    373     -H 'Authorization: Bearer secret-token:pass1234' \
    374     http://localhost:9966/instances/self/private/accounts \
    375     -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/45?receiver-name=user45"}' \
    376     -w "%{http_code}" -s \
    377     -o "$LAST_RESPONSE")
    378 
    379 if [ "$STATUS" != "200" ]
    380 then
    381     jq < "$LAST_RESPONSE"
    382     exit_fail "Expected 200 OK. Got: $STATUS"
    383 fi
    384 
    385 echo " OK"
    386 
    387 
    388 echo -n "Begin forgotten password reset "
    389 STATUS=$(curl \
    390     -X POST \
    391     -H "Content-Type: application/json" \
    392     http://localhost:9966/instances/self/forgot-password \
    393     -d '{"method":"token","password":"amnesia"}' \
    394     -w "%{http_code}" -s \
    395     -o "$LAST_RESPONSE")
    396 
    397 if [ "$STATUS" != "202" ]
    398 then
    399     jq < "$LAST_RESPONSE"
    400     exit_fail "Expected 202 Accepted. Got: $STATUS"
    401 fi
    402 
    403 echo " OK"
    404 
    405 C1=$(jq -r .challenges[0].challenge_id < "$LAST_RESPONSE")
    406 C2=$(jq -r .challenges[1].challenge_id < "$LAST_RESPONSE")
    407 
    408 echo -n "Requesting challenge $C1 "
    409 
    410 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    411     "http://localhost:9966/challenge/$C1" \
    412     -d '{}' \
    413     -w "%{http_code}" -s \
    414     -o "$LAST_RESPONSE")
    415 
    416 if [ "$STATUS" != "200" ]
    417 then
    418     jq < "$LAST_RESPONSE"
    419     exit_fail "Expected 200 OK. Got: $STATUS"
    420 fi
    421 echo "OK"
    422 
    423 TAN=$(cat /tmp/test-merchant-email-tan.txt | head -n1 | awk '{print $1}')
    424 ADDR=$(cat /tmp/test-merchant-email-address.txt)
    425 
    426 if [ "$ADDR" != "self@example.com" ]
    427 then
    428     exit_fail "Expected address 'self@example.com'. Got: $ADDR"
    429 fi
    430 
    431 echo -n "Sending challenge $C1 solution "
    432 
    433 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    434     "http://localhost:9966/challenge/$C1/confirm" \
    435     -d '{"tan":"'"$TAN"'"}' \
    436     -w "%{http_code}" -s \
    437     -o "$LAST_RESPONSE")
    438 
    439 if [ "$STATUS" != "204" ]
    440 then
    441     jq < "$LAST_RESPONSE"
    442     exit_fail "Expected 204 OK. Got: $STATUS"
    443 fi
    444 echo "OK"
    445 
    446 echo -n "Reject password reset with only one mandatory challenge solved "
    447 STATUS=$(curl \
    448     -X POST \
    449     -H "Content-Type: application/json" \
    450     -H "Taler-Challenge-Ids: $C1" \
    451     http://localhost:9966/instances/self/forgot-password \
    452     -d '{"method":"token","password":"amnesia"}' \
    453     -w "%{http_code}" -s \
    454     -o "$LAST_RESPONSE")
    455 
    456 if [ "$STATUS" != "202" ] ||
    457    [ "$(jq -r .combi_and < "$LAST_RESPONSE")" != "true" ]
    458 then
    459     jq < "$LAST_RESPONSE"
    460     exit_fail "Expected both mandatory channels to remain required. Got: $STATUS"
    461 fi
    462 echo "OK"
    463 
    464 
    465 echo -n "Requesting challenge $C2 "
    466 
    467 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    468     "http://localhost:9966/challenge/$C2" \
    469     -d '{}' \
    470     -w "%{http_code}" -s \
    471     -o "$LAST_RESPONSE")
    472 
    473 if [ "$STATUS" != "200" ]
    474 then
    475     jq < "$LAST_RESPONSE"
    476     exit_fail "Expected 200 OK. Got: $STATUS"
    477 fi
    478 echo "OK"
    479 
    480 TAN=$(cat /tmp/test-merchant-sms-tan.txt | head -n1 | awk '{print $1}')
    481 
    482 echo -n "Sending challenge $C2 solution "
    483 
    484 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    485     "http://localhost:9966/challenge/$C2/confirm" \
    486     -d '{"tan":"'"$TAN"'"}' \
    487     -w "%{http_code}" -s \
    488     -o "$LAST_RESPONSE")
    489 
    490 if [ "$STATUS" != "204" ]
    491 then
    492     jq < "$LAST_RESPONSE"
    493     exit_fail "Expected 204 OK. Got: $STATUS"
    494 fi
    495 echo "OK"
    496 
    497 echo -n "Complete password reset "
    498 STATUS=$(curl \
    499     -X POST \
    500     -H "Content-Type: application/json" \
    501     -H "Taler-Challenge-Ids: $C1,$C2" \
    502     http://localhost:9966/instances/self/forgot-password \
    503     -d '{"method":"token","password":"amnesia"}' \
    504     -w "%{http_code}" -s \
    505     -o "$LAST_RESPONSE")
    506 
    507 if [ "$STATUS" != "204" ]
    508 then
    509     jq < "$LAST_RESPONSE"
    510     exit_fail "Expected 204 No content. Got: $STATUS"
    511 fi
    512 
    513 echo " OK"
    514 
    515 
    516 
    517 
    518 
    519 echo -n "Begin password reset with login token issuance "
    520 TOKEN_RESET_BODY='{"method":"token","password":"recovered","token_duration":{"d_us":600000000}}'
    521 STATUS=$(curl \
    522     -X POST \
    523     -H "Content-Type: application/json" \
    524     http://localhost:9966/instances/self/forgot-password \
    525     -d "$TOKEN_RESET_BODY" \
    526     -w "%{http_code}" -s \
    527     -o "$LAST_RESPONSE")
    528 
    529 if [ "$STATUS" != "202" ]
    530 then
    531     jq < "$LAST_RESPONSE"
    532     exit_fail "Expected 202 Accepted. Got: $STATUS"
    533 fi
    534 cp "$LAST_RESPONSE" "$LAST_RESPONSE.challenges"
    535 RESET_CHALLENGE_IDS=$(jq -r '[.challenges[].challenge_id] | join(",")' \
    536     < "$LAST_RESPONSE")
    537 solve_response_challenges "$LAST_RESPONSE.challenges"
    538 echo "OK"
    539 
    540 
    541 echo -n "Reject solved challenges for a modified token duration "
    542 STATUS=$(curl \
    543     -X POST \
    544     -H "Content-Type: application/json" \
    545     -H "Taler-Challenge-Ids: $RESET_CHALLENGE_IDS" \
    546     http://localhost:9966/instances/self/forgot-password \
    547     -d '{"method":"token","password":"recovered","token_duration":{"d_us":300000000}}' \
    548     -w "%{http_code}" -s \
    549     -o "$LAST_RESPONSE")
    550 
    551 if [ "$STATUS" != "202" ]
    552 then
    553     jq < "$LAST_RESPONSE"
    554     exit_fail "Expected changed request body to require new challenges. Got: $STATUS"
    555 fi
    556 echo "OK"
    557 
    558 
    559 echo -n "Complete password reset and receive login token "
    560 STATUS=$(curl \
    561     -X POST \
    562     -H "Content-Type: application/json" \
    563     -H "Taler-Challenge-Ids: $RESET_CHALLENGE_IDS" \
    564     http://localhost:9966/instances/self/forgot-password \
    565     -d "$TOKEN_RESET_BODY" \
    566     -w "%{http_code}" -s \
    567     -o "$LAST_RESPONSE")
    568 
    569 if [ "$STATUS" != "200" ]
    570 then
    571     jq < "$LAST_RESPONSE"
    572     exit_fail "Expected 200 OK with login token. Got: $STATUS"
    573 fi
    574 RESET_TOKEN=$(jq -er \
    575     'select(.scope == "spa" and .refreshable == true) | .access_token' \
    576     < "$LAST_RESPONSE")
    577 if [ "$(jq -r .token < "$LAST_RESPONSE")" != "$RESET_TOKEN" ]
    578 then
    579     exit_fail "Expected token and access_token response fields to match"
    580 fi
    581 echo "OK"
    582 
    583 
    584 echo -n "Use login token returned by password reset "
    585 STATUS=$(curl \
    586     -X GET \
    587     -H "Authorization: Bearer $RESET_TOKEN" \
    588     http://localhost:9966/instances/self/private/products \
    589     -w "%{http_code}" -s \
    590     -o "$LAST_RESPONSE")
    591 
    592 if [ "$STATUS" != "200" ]
    593 then
    594     jq < "$LAST_RESPONSE"
    595     exit_fail "Expected reset login token to authorize SPA access. Got: $STATUS"
    596 fi
    597 echo "OK"
    598 
    599 
    600 echo -n "Require one additional factor for a password change "
    601 PASSWORD_CHANGE_BODY='{"method":"token","password":"recovered","old_password":"recovered"}'
    602 STATUS=$(curl \
    603     -X POST \
    604     -H "Content-Type: application/json" \
    605     -H "Authorization: Bearer $RESET_TOKEN" \
    606     http://localhost:9966/instances/self/private/auth \
    607     -d "$PASSWORD_CHANGE_BODY" \
    608     -w "%{http_code}" -s \
    609     -o "$LAST_RESPONSE")
    610 
    611 if [ "$STATUS" != "202" ] ||
    612    [ "$(jq -r .combi_and < "$LAST_RESPONSE")" != "false" ] ||
    613    [ "$(jq -r '.challenges | length' < "$LAST_RESPONSE")" != "2" ]
    614 then
    615     jq < "$LAST_RESPONSE"
    616     exit_fail "Expected password change to offer either MFA channel. Got: $STATUS"
    617 fi
    618 
    619 C1=$(jq -r .challenges[0].challenge_id < "$LAST_RESPONSE")
    620 CHANNEL=$(jq -r .challenges[0].tan_channel < "$LAST_RESPONSE")
    621 case "$CHANNEL" in
    622 email)
    623     EXPECTED_ADDRESS="self@example.com"
    624     ;;
    625 sms)
    626     EXPECTED_ADDRESS="+4171234"
    627     ;;
    628 *)
    629     exit_fail "Unexpected TAN channel: $CHANNEL"
    630     ;;
    631 esac
    632 solve_challenge "$C1" "$CHANNEL" "$EXPECTED_ADDRESS"
    633 
    634 STATUS=$(curl \
    635     -X POST \
    636     -H "Content-Type: application/json" \
    637     -H "Taler-Challenge-Ids: $C1" \
    638     -H "Authorization: Bearer $RESET_TOKEN" \
    639     http://localhost:9966/instances/self/private/auth \
    640     -d "$PASSWORD_CHANGE_BODY" \
    641     -w "%{http_code}" -s \
    642     -o "$LAST_RESPONSE")
    643 
    644 if [ "$STATUS" != "204" ]
    645 then
    646     jq < "$LAST_RESPONSE"
    647     exit_fail "Expected one solved challenge to authorize password change. Got: $STATUS"
    648 fi
    649 echo "OK"
    650 
    651 
    652 echo -n "Self-provision second instance for instance-binding test "
    653 OTHER_INSTANCE_BODY='{"auth":{"method":"token","password":"recovered"},"id":"other","name":"other","phone_number":"+4171234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us":50000000},"default_pay_delay":{"d_us":60000000}}'
    654 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    655     http://localhost:9966/instances \
    656     -d "$OTHER_INSTANCE_BODY" \
    657     -w "%{http_code}" -s \
    658     -o "$LAST_RESPONSE")
    659 
    660 if [ "$STATUS" != "202" ]
    661 then
    662     jq < "$LAST_RESPONSE"
    663     exit_fail "Expected 202 Accepted. Got: $STATUS"
    664 fi
    665 cp "$LAST_RESPONSE" "$LAST_RESPONSE.challenges"
    666 OTHER_CREATE_CHALLENGE_IDS=$(jq -r \
    667     '[.challenges[].challenge_id] | join(",")' < "$LAST_RESPONSE")
    668 solve_response_challenges "$LAST_RESPONSE.challenges"
    669 
    670 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    671     -H "Taler-Challenge-Ids: $OTHER_CREATE_CHALLENGE_IDS" \
    672     http://localhost:9966/instances \
    673     -d "$OTHER_INSTANCE_BODY" \
    674     -w "%{http_code}" -s \
    675     -o "$LAST_RESPONSE")
    676 if [ "$STATUS" != "204" ]
    677 then
    678     jq < "$LAST_RESPONSE"
    679     exit_fail "Expected 204 No Content. Got: $STATUS"
    680 fi
    681 echo "OK"
    682 
    683 
    684 echo -n "Solve token-creation challenge for first instance "
    685 TOKEN_BODY='{"scope":"spa","duration":{"d_us":600000000},"refreshable":true}'
    686 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    687     -H 'Authorization: Bearer secret-token:recovered' \
    688     http://localhost:9966/instances/self/private/token \
    689     -d "$TOKEN_BODY" \
    690     -w "%{http_code}" -s \
    691     -o "$LAST_RESPONSE")
    692 if [ "$STATUS" != "202" ]
    693 then
    694     jq < "$LAST_RESPONSE"
    695     exit_fail "Expected 202 Accepted. Got: $STATUS"
    696 fi
    697 INSTANCE_CHALLENGE_ID=$(jq -r '.challenges[0].challenge_id' < "$LAST_RESPONSE")
    698 INSTANCE_CHALLENGE_CHANNEL=$(jq -r '.challenges[0].tan_channel' < "$LAST_RESPONSE")
    699 case "$INSTANCE_CHALLENGE_CHANNEL" in
    700 email)
    701     INSTANCE_CHALLENGE_ADDRESS=self@example.com
    702     ;;
    703 sms)
    704     INSTANCE_CHALLENGE_ADDRESS=+4171234
    705     ;;
    706 *)
    707     exit_fail "Unexpected TAN channel: $INSTANCE_CHALLENGE_CHANNEL"
    708     ;;
    709 esac
    710 solve_challenge "$INSTANCE_CHALLENGE_ID" \
    711     "$INSTANCE_CHALLENGE_CHANNEL" \
    712     "$INSTANCE_CHALLENGE_ADDRESS"
    713 echo "OK"
    714 
    715 
    716 echo -n "Reject solved challenge at a different instance "
    717 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    718     -H 'Authorization: Bearer secret-token:recovered' \
    719     -H "Taler-Challenge-Ids: $INSTANCE_CHALLENGE_ID" \
    720     http://localhost:9966/instances/other/private/token \
    721     -d "$TOKEN_BODY" \
    722     -w "%{http_code}" -s \
    723     -o "$LAST_RESPONSE")
    724 if [ "$STATUS" != "202" ]
    725 then
    726     jq < "$LAST_RESPONSE"
    727     exit_fail "Expected foreign challenge to be rejected with 202. Got: $STATUS"
    728 fi
    729 if jq -e --arg cid "$INSTANCE_CHALLENGE_ID" \
    730     '.challenges[] | select(.challenge_id == $cid)' "$LAST_RESPONSE" > /dev/null
    731 then
    732     exit_fail "Foreign challenge was returned as applicable to the other instance"
    733 fi
    734 echo "OK"
    735 
    736 
    737 echo -n "Accept solved challenge at its original instance "
    738 STATUS=$(curl -H "Content-Type: application/json" -X POST \
    739     -H 'Authorization: Bearer secret-token:recovered' \
    740     -H "Taler-Challenge-Ids: $INSTANCE_CHALLENGE_ID" \
    741     http://localhost:9966/instances/self/private/token \
    742     -d "$TOKEN_BODY" \
    743     -w "%{http_code}" -s \
    744     -o "$LAST_RESPONSE")
    745 if [ "$STATUS" != "200" ]
    746 then
    747     jq < "$LAST_RESPONSE"
    748     exit_fail "Expected same-instance challenge to authorize request. Got: $STATUS"
    749 fi
    750 echo "OK"
    751 
    752 
    753 echo "TEST PASSED"
    754 
    755 exit 0