Skip to content

Commit

Permalink
Implement decoupled TAN process
Browse files Browse the repository at this point in the history
  • Loading branch information
raphaelm committed Mar 28, 2024
1 parent 83c8573 commit c296283
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 20 deletions.
7 changes: 5 additions & 2 deletions docs/debits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ You can easily generate XML using the ``sepaxml`` python library:
pain_descriptor='urn:iso:std:iso:20022:tech:xsd:pain.008.002.02'
)
if isinstance(res, NeedTANResponse):
while isinstance(res, NeedTANResponse):
print("A TAN is required", res.challenge)
if getattr(res, 'challenge_hhduc', None):
Expand All @@ -76,7 +76,10 @@ You can easily generate XML using the ``sepaxml`` python library:
except KeyboardInterrupt:
pass
tan = input('Please enter TAN:')
if result.decoupled:
tan = input('Please press enter after confirming the transaction in your app:')
else:
tan = input('Please enter TAN:')
res = client.send_tan(res, tan)
print(res.status)
Expand Down
7 changes: 5 additions & 2 deletions docs/transfers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Full example
endtoend_id='NOTPROVIDED',
)
if isinstance(res, NeedTANResponse):
while isinstance(res, NeedTANResponse):
print("A TAN is required", res.challenge)
if getattr(res, 'challenge_hhduc', None):
Expand All @@ -64,7 +64,10 @@ Full example
except KeyboardInterrupt:
pass
tan = input('Please enter TAN:')
if result.decoupled:
tan = input('Please press enter after confirming the transaction in your app:')
else:
tan = input('Please enter TAN:')
res = client.send_tan(res, tan)
print(res.status)
Expand Down
20 changes: 12 additions & 8 deletions docs/trouble.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ the problem.
getpass.getpass('PIN: '),
'REPLACEME' # ENDPOINT
)
product_id = 'REPLACEME'
f = FinTS3PinTanClient(*client_args)
f = FinTS3PinTanClient(*client_args, product_id=product_id)
minimal_interactive_cli_bootstrap(f)
Expand All @@ -57,19 +58,22 @@ the problem.
terminal_flicker_unix(response.challenge_hhduc)
except KeyboardInterrupt:
pass
tan = input('Please enter TAN:')
if response.decoupled:
tan = input('Please press enter after confirming the transaction in your app:')
else:
tan = input('Please enter TAN:')
return f.send_tan(response, tan)
# Open the actual dialog
with f:
# Since PSD2, a TAN might be needed for dialog initialization. Let's check if there is one required
if f.init_tan_response:
ask_for_tan(f.init_tan_response)
while isinstance(f.init_tan_response, NeedTANResponse):
f.init_tan_response = ask_for_tan(f.init_tan_response)
# Fetch accounts
accounts = f.get_sepa_accounts()
if isinstance(accounts, NeedTANResponse):
while isinstance(accounts, NeedTANResponse):
accounts = ask_for_tan(accounts)
if len(accounts) == 1:
account = accounts[0]
Expand All @@ -85,7 +89,7 @@ the problem.
client_data = f.deconstruct(including_private=True)
f = FinTS3PinTanClient(*client_args, from_data=client_data)
f = FinTS3PinTanClient(*client_args, product_id=product_id, from_data=client_data)
with f.resume_dialog(dialog_data):
while True:
operations = [
Expand Down Expand Up @@ -167,7 +171,7 @@ the problem.
endtoend_id='NOTPROVIDED',
)
if isinstance(res, NeedTANResponse):
ask_for_tan(res)
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
except FinTSUnsupportedOperation as e:
print("This operation is not supported by this bank:", e)
48 changes: 40 additions & 8 deletions fints/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,11 +1004,13 @@ class NeedTANResponse(NeedRetryResponse):
challenge_html = None #: HTML-safe challenge text, possibly with formatting
challenge_hhduc = None #: HHD_UC challenge to be transmitted to the TAN generator
challenge_matrix = None #: Matrix code challenge: tuple(mime_type, data)
decoupled = None #: Use decoupled process

def __init__(self, command_seg, tan_request, resume_method=None, tan_request_structured=False):
def __init__(self, command_seg, tan_request, resume_method=None, tan_request_structured=False, decoupled=False):
self.command_seg = command_seg
self.tan_request = tan_request
self.tan_request_structured = tan_request_structured
self.decoupled = decoupled
if hasattr(resume_method, '__func__'):
self.resume_method = resume_method.__func__.__name__
else:
Expand Down Expand Up @@ -1206,10 +1208,10 @@ def _get_tan_segment(self, orig_seg, tan_process, tan_seg=None):
if tan_process == '4' and tan_mechanism.VERSION >= 6:
seg.segment_type = orig_seg.header.type

if tan_process in ('2', '3'):
if tan_process in ('2', '3', 'S'):

Check warning on line 1211 in fints/client.py

View check run for this annotation

Codecov / codecov/patch

fints/client.py#L1211

Added line #L1211 was not covered by tests
seg.task_reference = tan_seg.task_reference

if tan_process in ('1', '2'):
if tan_process in ('1', '2', 'S'):

Check warning on line 1214 in fints/client.py

View check run for this annotation

Codecov / codecov/patch

fints/client.py#L1214

Added line #L1214 was not covered by tests
seg.further_tan_follows = False

return seg
Expand All @@ -1236,8 +1238,14 @@ def _send_with_possible_retry(self, dialog, command_seg, resume_func):
response = dialog.send(command_seg, tan_seg)

for resp in response.responses(tan_seg):
if resp.code == '0030':
return NeedTANResponse(command_seg, response.find_segment_first('HITAN'), resume_func, self.is_challenge_structured())
if resp.code in ('0030', '3955'):
return NeedTANResponse(
command_seg,
response.find_segment_first('HITAN'),
resume_func,
self.is_challenge_structured(),

Check warning on line 1246 in fints/client.py

View check run for this annotation

Codecov / codecov/patch

fints/client.py#L1246

Added line #L1246 was not covered by tests
resp.code == '3955',
)
if resp.code.startswith('9'):
raise Exception("Error response: {!r}".format(response))
else:
Expand All @@ -1255,17 +1263,41 @@ def send_tan(self, challenge: NeedTANResponse, tan: str):
"""
Sends a TAN to confirm a pending operation.
If ``NeedTANResponse.decoupled`` is ``True``, the ``tan`` parameter is ignored and can be kept empty.
If the operation was not yet confirmed using the decoupled app, this method will again return a
``NeedTANResponse``.
:param challenge: NeedTANResponse to respond to
:param tan: TAN value
:return: Currently no response
:return: New response after sending TAN
"""

with self._get_dialog() as dialog:
tan_seg = self._get_tan_segment(challenge.command_seg, '2', challenge.tan_request)
self._pending_tan = tan
if challenge.decoupled:
tan_seg = self._get_tan_segment(challenge.command_seg, 'S', challenge.tan_request)
else:
tan_seg = self._get_tan_segment(challenge.command_seg, '2', challenge.tan_request)
self._pending_tan = tan

response = dialog.send(tan_seg)

if challenge.decoupled:
# TAN process = S
status_segment = response.find_segment_first('HITAN')
if not status_segment:
raise FinTSClientError(
"No TAN status received."
)
for resp in response.responses(tan_seg):
if resp.code == '3956':

Check warning on line 1292 in fints/client.py

View check run for this annotation

Codecov / codecov/patch

fints/client.py#L1292

Added line #L1292 was not covered by tests
return NeedTANResponse(
challenge.command_seg,
challenge.tan_request,
challenge.resume_method,
challenge.tan_request_structured,
challenge.decoupled,
)

resume_func = getattr(self, challenge.resume_method)
return resume_func(challenge.command_seg, response)

Expand Down

0 comments on commit c296283

Please sign in to comment.