merchant

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

test_merchant_mfa.sh (21508B)


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