From 179424a78f43e1ffcce368dc75a604f3ac55a6dd Mon Sep 17 00:00:00 2001 From: Henrik Jess Nielsen Date: Fri, 22 May 2026 21:45:58 +0200 Subject: [PATCH] fix: link Tink Link to the user created in Step 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/routes/demo.py | 72 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/routes/demo.py b/src/routes/demo.py index 1468f75..39db0e4 100644 --- a/src/routes/demo.py +++ b/src/routes/demo.py @@ -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 {external_user_id} 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,