fix: link Tink Link to the user created in Step 2
All checks were successful
Build and Deploy / deploy (push) Successful in 22s

Previously Step 2 (create user) and Step 3 (Tink Link) were disconnected —
the bank connection went to an anonymous new user, not the one just created.

Fix: Step 3 now calls /api/v1/oauth/authorization-grant/delegate with the
user_id from session to get an authorization_code, which is injected into
the Tink Link URL. This binds the bank connection to the correct customer.

Also stores user_market in session so Step 3 uses the same market as Step 2.
Shows a note confirming which user Tink Link is bound to.
This commit is contained in:
Henrik Jess Nielsen
2026-05-22 21:45:58 +02:00
parent 5e14a219b1
commit 179424a78f

View File

@@ -172,6 +172,7 @@ async def step2_post(request: Request,
result = await client.create_user(app_token, external_user_id, market=market)
sess["user_id"] = result.get("user_id", "")
sess["external_user_id"] = external_user_id
sess["user_market"] = market
except Exception as e:
error = str(e)
@@ -201,17 +202,41 @@ CONSOLE_CALLBACK = "https://console.tink.com/callback"
async def step3_get(request: Request):
sess = _session(request)
s = get_settings()
client = _client()
client = _client(log_cb=_logger(sess))
tink_link_url = client.get_tink_link_url(market="DK")
dev_tink_link_url = client.get_tink_link_url(
market="DK", redirect_uri_override=CONSOLE_CALLBACK
)
error = None
credentials = None
auth_code_error = None
cb_success = request.query_params.get("cb_success")
cb_error = request.query_params.get("cb_error")
# If we have a user from Step 2, generate an authorization_code for them
# so Tink Link connects the bank to THAT specific user (not a new anonymous one)
user_id = sess.get("user_id", "")
app_token = sess.get("app_token", "")
authorization_code = None
if user_id and app_token and not sess.get("user_token"):
try:
grant = await client.get_authorization_grant_token(
app_token=app_token,
user_id=user_id,
scope="accounts:read,transactions:read,credentials:read",
)
authorization_code = grant.get("code", "")
except Exception as e:
auth_code_error = str(e)
tink_link_url = client.get_tink_link_url(
market=sess.get("user_market", "DK"),
authorization_code=authorization_code or None,
)
dev_tink_link_url = client.get_tink_link_url(
market=sess.get("user_market", "DK"),
authorization_code=authorization_code or None,
redirect_uri_override=CONSOLE_CALLBACK,
)
# Demo mode: auto-mark as connected with mock data
if s.demo_mode and not sess.get("user_token"):
sess["user_token"] = "demo-mode-token"
@@ -227,35 +252,46 @@ async def step3_get(request: Request):
try:
credentials = await client.list_credentials(user_token)
except Exception as e:
# credentials:read may not be granted in simple Tink Link flow — not fatal
if "403" in str(e):
credentials = {"note": "credentials:read kræver authorization-grant flow"}
else:
error = str(e)
external_user_id = sess.get("external_user_id", "")
uid_display = user_id or "$USER_ID"
curl_example = (
"# Simpelt Tink Link flow — ingen pre-oprettet bruger nødvendig\n"
f"# Redirect brugeren direkte til:\n"
f"https://link.tink.com/1.0/transactions/connect-accounts\n"
f" ?client_id=$CLIENT_ID\n"
f" &redirect_uri=$REDIRECT_URI\n"
f" &market=DK\n"
f" &scope=accounts:read,transactions:read,credentials:read\n\n"
"# Tink Link redirecter tilbage med ?code=... som du exchangeer for user token:\n"
"# Step 1: Hent authorization_code for den specifikke bruger\n"
"curl -X POST https://api.tink.com/api/v1/oauth/authorization-grant/delegate \\\n"
" -H 'Authorization: Bearer $APP_TOKEN' \\\n"
" -d 'actor_client_id=$CLIENT_ID' \\\n"
f" -d 'user_id={uid_display}' \\\n"
" -d 'scope=accounts:read,transactions:read'\n\n"
"# Step 2: Send authorization_code med i Tink Link URL\n"
"https://link.tink.com/1.0/transactions/connect-accounts\n"
" ?client_id=$CLIENT_ID\n"
" &redirect_uri=$REDIRECT_URI\n"
" &authorization_code=$CODE ← binder til din bruger!\n\n"
"# Step 3: Exchange callback code for user token\n"
"curl -X POST https://api.tink.com/api/v1/oauth/token \\\n"
" -d 'grant_type=authorization_code' \\\n"
" -d 'client_id=$CLIENT_ID' \\\n"
" -d 'client_secret=$CLIENT_SECRET' \\\n"
" -d 'code=$CODE'"
" -d 'code=$CALLBACK_CODE'"
)
linked_user_note = None
if authorization_code:
linked_user_note = f"Tink Link er bundet til bruger <code class='text-violet-300 font-mono'>{external_user_id}</code> via authorization_code"
elif user_id:
linked_user_note = f"⚠️ Kunne ikke hente authorization_code: {auth_code_error or 'mangler app token'} — Tink Link opretter en ny anonym bruger"
return templates.TemplateResponse("step.html", _ctx(request, {
"step": 3,
"title": "Tilslut Bank",
"subtitle": "Tink Link — Bank Connection",
"endpoint": "Tink Link /1.0/transactions/connect-accounts",
"api_version": "Link v1",
"description": "Brugeren åbner Tink Link, vælger Tink Demo Bank og logger ind med test-credentials fra Console. Tink redirecter tilbage med en authorization_code som automatisk exchang'es til et user token.",
"endpoint": "GET /api/v1/oauth/authorization-grant/delegate",
"api_version": "v1",
"description": linked_user_note or "Åbn Tink Link, vælg Tink Demo Bank og log ind med test-credentials.",
"curl_example": curl_example,
"result": credentials,
"tink_link_url": tink_link_url,