Skip to content

Commit ade5323

Browse files
nocajarCopilot
andcommitted
Fix 4: Handle TAN response attached to command segment instead of HKTAN
Some banks (e.g. Consorsbank) attach the 0030/3955 response code to the original command segment (HKCCS) rather than to the HKTAN segment. This caused _send_pay_with_possible_retry() to miss the TAN challenge and return a plain TransactionResponse instead of NeedTANResponse. Added fallback: after checking tan_seg responses, also check command_seg responses for 0030/3955 codes. Also: - Add photoTAN QR code handling to transfers.rst full example - Fix typo (result.decoupled → res.decoupled) in transfers.rst - Add Consorsbank to tested.rst (Transactions + Transfer) - Add security function 900 (photoTAN / SecurePlus) - Add sample_consorsbank.py showing photoTAN transfer flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 99b40a7 commit ade5323

4 files changed

Lines changed: 193 additions & 1 deletion

File tree

docs/tested.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ Postbank Yes
1111
BBBank eG Yes Yes
1212
Sparkasse Heidelberg Yes
1313
comdirect Yes Yes
14+
Consorsbank Yes Yes
1415
======================================== ============ ======== ======== ======
1516

1617
Tested security functions
1718
-------------------------
1819

20+
* ``900`` "photoTAN" / "Secure Plus" (QR code)
1921
* ``902`` "photoTAN"
2022
* ``921`` "pushTAN"
2123
* ``930`` "mobile TAN"

docs/transfers.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,22 @@ Full example
6767
if isinstance(res, NeedTANResponse):
6868
print("A TAN is required", res.challenge)
6969
70+
# photoTAN / QR code: save and display the image
71+
if getattr(res, 'challenge_matrix', None):
72+
mime_type, image_data = res.challenge_matrix
73+
with open('tan_challenge.png', 'wb') as f:
74+
f.write(image_data)
75+
print(f"QR code saved to tan_challenge.png ({len(image_data)} bytes)")
76+
# Optionally open the image automatically:
77+
# import subprocess; subprocess.Popen(['open', 'tan_challenge.png'])
78+
7079
if getattr(res, 'challenge_hhduc', None):
7180
try:
7281
terminal_flicker_unix(res.challenge_hhduc)
7382
except KeyboardInterrupt:
7483
pass
7584
76-
if result.decoupled:
85+
if res.decoupled:
7786
tan = input('Please press enter after confirming the transaction in your app:')
7887
else:
7988
tan = input('Please enter TAN:')

fints/client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,20 @@ def _send_pay_with_possible_retry(self, dialog, command_seg, resume_func):
14871487
)
14881488
if resp.code.startswith('9'):
14891489
raise Exception("Error response: {!r}".format(response))
1490+
1491+
# Some banks (e.g. Consorsbank) attach the 0030 TAN-required
1492+
# response to the command segment (HKCCS) rather than the
1493+
# HKTAN segment. Check command_seg responses as fallback.
1494+
for resp in response.responses(command_seg):
1495+
if resp.code in ('0030', '3955'):
1496+
return NeedTANResponse(
1497+
command_seg,
1498+
response.find_segment_first('HITAN'),
1499+
resume_func,
1500+
self.is_challenge_structured(),
1501+
resp.code == '3955',
1502+
hivpp,
1503+
)
14901504
else:
14911505
response = dialog.send(command_seg)
14921506

sample_consorsbank.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Sample: Consorsbank (BLZ 76030080) with python-fints.
4+
5+
Demonstrates fetching transactions and making SEPA transfers with
6+
photoTAN (QR code) authentication.
7+
8+
Consorsbank requires three compatibility fixes (see PR #209):
9+
1. security_method_version=2 for two-step TAN
10+
2. Full account details in KTI1.from_sepa_account
11+
3. force_twostep_tan for segments the bank requires TAN on
12+
despite HIPINS reporting otherwise
13+
14+
Additionally, Consorsbank attaches the TAN-required response (0030)
15+
to the command segment (HKCCS) rather than the HKTAN segment, which
16+
is handled by Fix 4 in this branch.
17+
18+
Usage:
19+
pip install python-fints python-dotenv
20+
python sample_consorsbank.py
21+
22+
Environment variables (or .env file):
23+
FINTS_BLZ=76030080
24+
FINTS_USER=<your user id>
25+
FINTS_PIN=<your PIN>
26+
FINTS_SERVER=https://brokerage-hbci.consorsbank.de/hbci
27+
FINTS_PRODUCT_ID=<your registered product id>
28+
MY_IBAN=<IBAN of the account to use>
29+
"""
30+
31+
import os
32+
import sys
33+
import logging
34+
import subprocess
35+
from datetime import date, timedelta
36+
from decimal import Decimal
37+
38+
from fints.client import FinTS3PinTanClient, NeedTANResponse
39+
40+
logging.basicConfig(level=logging.WARNING)
41+
42+
try:
43+
from dotenv import load_dotenv
44+
load_dotenv()
45+
except ImportError:
46+
pass
47+
48+
49+
def handle_tan(response, client):
50+
"""Handle TAN challenges including photoTAN with QR code image."""
51+
while isinstance(response, NeedTANResponse):
52+
print(f"\nTAN required: {response.challenge}")
53+
54+
# photoTAN / QR code image
55+
if response.challenge_matrix:
56+
mime_type, image_data = response.challenge_matrix
57+
ext = ".png" if "png" in mime_type else ".jpg"
58+
img_path = f"tan_challenge{ext}"
59+
with open(img_path, "wb") as f:
60+
f.write(image_data)
61+
print(f" QR code saved to {img_path} ({len(image_data)} bytes)")
62+
# On macOS: subprocess.Popen(["open", img_path])
63+
# On Linux: subprocess.Popen(["xdg-open", img_path])
64+
tan = input("Scan the QR code and enter TAN: ")
65+
66+
# Flicker / HHD UC challenge
67+
elif response.challenge_hhduc:
68+
print(f" HHD UC data available")
69+
tan = input("Enter TAN: ")
70+
71+
# Decoupled (app confirmation)
72+
elif response.decoupled:
73+
input("Confirm in your banking app, then press ENTER: ")
74+
tan = ""
75+
76+
# Manual TAN entry
77+
else:
78+
tan = input("Enter TAN: ")
79+
80+
response = client.send_tan(response, tan)
81+
return response
82+
83+
84+
def main():
85+
blz = os.environ.get("FINTS_BLZ", "76030080")
86+
user = os.environ["FINTS_USER"]
87+
pin = os.environ["FINTS_PIN"]
88+
server = os.environ.get("FINTS_SERVER", "https://brokerage-hbci.consorsbank.de/hbci")
89+
product_id = os.environ.get("FINTS_PRODUCT_ID")
90+
my_iban = os.environ.get("MY_IBAN")
91+
92+
client = FinTS3PinTanClient(
93+
bank_identifier=blz,
94+
user_id=user,
95+
pin=pin,
96+
server=server,
97+
product_id=product_id,
98+
# Consorsbank reports HKKAZ:N and HKSAL:N in HIPINS but actually
99+
# requires TAN for these operations. HKCCS always requires TAN.
100+
force_twostep_tan={"HKKAZ", "HKSAL"},
101+
)
102+
103+
# Select photoTAN mechanism (Consorsbank uses 900)
104+
if not client.get_current_tan_mechanism():
105+
client.fetch_tan_mechanisms()
106+
client.set_tan_mechanism("900")
107+
108+
with client:
109+
if client.init_tan_response:
110+
handle_tan(client.init_tan_response, client)
111+
112+
# --- Fetch accounts ---
113+
accounts = client.get_sepa_accounts()
114+
if isinstance(accounts, NeedTANResponse):
115+
accounts = handle_tan(accounts, client)
116+
117+
print("Accounts:")
118+
for a in accounts:
119+
print(f" {a.iban} (BIC: {a.bic})")
120+
121+
# Select account
122+
if my_iban:
123+
account = next((a for a in accounts if a.iban == my_iban), None)
124+
if not account:
125+
print(f"Account {my_iban} not found")
126+
return
127+
else:
128+
account = accounts[0]
129+
130+
print(f"\nUsing account: {account.iban}")
131+
132+
# --- Fetch transactions ---
133+
print("\nFetching transactions (last 30 days)...")
134+
start_date = date.today() - timedelta(days=30)
135+
res = client.get_transactions(account, start_date=start_date)
136+
if isinstance(res, NeedTANResponse):
137+
res = handle_tan(res, client)
138+
139+
if res:
140+
print(f"Found {len(res)} transactions:")
141+
for t in res[-5:]: # show last 5
142+
d = t.data
143+
amt = d.get("amount")
144+
amount_str = f"{amt.amount:>10.2f} {amt.currency}" if amt else ""
145+
print(f" {d.get('date')} {amount_str} {d.get('applicant_name', '')}")
146+
else:
147+
print("No transactions found.")
148+
149+
# --- SEPA Transfer (uncomment to use) ---
150+
# res = client.simple_sepa_transfer(
151+
# account=account,
152+
# iban="DE89370400440532013000",
153+
# bic="COBADEFFXXX",
154+
# recipient_name="Max Mustermann",
155+
# amount=Decimal("1.00"),
156+
# account_name="Your Name",
157+
# reason="Test transfer",
158+
# )
159+
# if isinstance(res, NeedTANResponse):
160+
# res = handle_tan(res, client)
161+
# print(f"Transfer result: {res.status} {res.responses}")
162+
163+
print("\nDone!")
164+
165+
166+
if __name__ == "__main__":
167+
main()

0 commit comments

Comments
 (0)