Skip to content

How to test 90-day reauthentication, consent expiry, and token refresh in open banking

Test open banking reauthentication by driving each consent through an injected clock: EU strong customer authentication through an account information provider at day 180 and 181, UK consent reconfirmation before day 90, token refresh, revocation, and expiry. The check that catches silent data loss is two parallel refreshes of one rotating OAuth 2.0 refresh token.

One defect is a scheduler that still sends customers back to their bank every 90 days in the EU, or counts 180 days from the day the consent was created instead of from the last strong customer authentication. Customers drop out at a prompt the rule no longer requires, and the product keeps showing stale account data as current. The second defect is quieter: two workers refresh the same OAuth token at once, the bank revokes the whole grant, and every background pull fails until the customer returns to the app.

For an aggregator paid per connection and per request, each dead grant is a connection that stops earning. For a lender or accounting product reading the feed, a credit or reconciliation decision runs on balances that stopped updating days earlier. The open banking segment page maps these failures to each revenue model.

The EU rule on 22 September 2026

Commission Delegated Regulation (EU) 2022/2360 of 3 August 2022 (OJ L 312, 5 December 2022, read on 2026-09-22) amended the regulatory technical standards (RTS) in Delegated Regulation (EU) 2018/389 and applies from 25 July 2023:

  • Article 10, direct access. A bank "shall be allowed not to apply" strong customer authentication (SCA) when the customer accesses only the balance or the payment transactions executed in the last 90 days. It must apply SCA on first access, or when more than 180 days have elapsed since the last access with SCA.
  • Article 10a(1) and (2), access through an account information provider. For the same two data items, the bank "shall not apply" SCA, except on first access through that provider, or when more than 180 days have elapsed since the last access through that provider with SCA.
  • Article 10a(3). The bank may still apply SCA when it has objectively justified and duly evidenced reasons relating to unauthorised or fraudulent access, and documents them to its national competent authority on request.
  • Article 30(4a). Interface changes made to comply with Article 10a reach providers at least 2 months before implementation.

On 22 September 2026, the Legislative Observatory shows both PSD3, file 2023/0209(COD), and the Payment Services Regulation, file 2023/0210(COD), at "Awaiting Council's 1st reading position", with ECON committee approval of the agreed texts on 5 May 2026 and an indicative plenary date of 14 December 2026. Neither act is in the Official Journal, so Article 10a remains the rule; the PSD3 and PSR page tracks the agreed articles, and the PSD2 SCA page covers the other exemptions.

The UK rule

The FCA removed 90-day reauthentication with the bank through PS21/19, published 29 November 2021 (read on 2026-09-22). Instrument FCA 2021/45 brought UK SCA-RTS Articles 10A and 36(6) into force on 26 March 2022:

  • Article 10A. A bank "shall be allowed not to apply" SCA for the balance and last-90-day transactions requested through an account information provider, provided SCA was applied on at least one previous access by that provider. The exemption is permissive; PS21/19 states that the bank may require SCA at any time and strongly encourages it to rely on the exemption.
  • Article 36(6). The provider may access data without the customer actively requesting it only if the customer confirmed with the provider, within the previous 90 days, that they continue to consent. PS21/19 states that where the customer does not reconfirm after 90 days, the provider must stop access, and that access may restart after a later reconfirmation.

The transition described on the FCA SCA page (read on 2026-09-22) ended on 30 September 2022.

In the US, the authorization duration under 12 CFR 1033.421 and the rule's current status are on the CFPB Section 1033 page.

Procedure

1. Put the reauthentication rules behind an injected clock

The two rules have different anchors. The EU count starts at the last access through the provider with SCA. The UK count starts at the customer's last confirmation to the provider, which is the original consent or a later reconfirmation. Neither count starts at the last data pull, and the EU count does not start at consent creation.

Python
from datetime import datetime, timedelta, timezone

EU_SCA_RENEWAL = timedelta(days=180)  # RTS Article 10a(2)(b), Delegated Regulation (EU) 2022/2360
UK_RECONFIRM = timedelta(days=90)     # UK SCA-RTS Article 36(6)


def eu_sca_required(last_sca_via_provider: datetime | None, now: datetime) -> bool:
    """SCA on first access through the provider, or when more than 180 days have elapsed."""
    if last_sca_via_provider is None:
        return True
    return now - last_sca_via_provider > EU_SCA_RENEWAL


def uk_unattended_access_allowed(last_confirmed: datetime | None, now: datetime) -> bool:
    """Our reading of 'within the previous 90 days': exactly 90 days is outside."""
    if last_confirmed is None:
        return False
    return now - last_confirmed < UK_RECONFIRM


T0 = datetime(2026, 3, 2, 9, 0, tzinfo=timezone.utc)


def test_eu_first_access_needs_sca():
    assert eu_sca_required(None, T0)


def test_eu_exactly_180_days_is_exempt():
    assert not eu_sca_required(T0, T0 + timedelta(days=180))


def test_eu_one_second_past_180_days_needs_sca():
    assert eu_sca_required(T0, T0 + timedelta(days=180, seconds=1))


def test_uk_day_89_allowed():
    assert uk_unattended_access_allowed(T0, T0 + timedelta(days=89, hours=23))


def test_uk_day_90_blocked():
    assert not uk_unattended_access_allowed(T0, T0 + timedelta(days=90))

Run the product's own scheduler against the same clock values. A reauthentication prompt shown to an EU customer on day 90 is a defect; a UK background pull more than 90 days after the last reconfirmation breaks Article 36(6).

2. Refresh the access token as RFC 6749 section 6 defines it

RFC 6749 section 6 defines the refresh request. When the client authenticates with mutual TLS, RFC 8705 requires the client_id parameter in the request.

Request
POST /as/token.oauth2 HTTP/1.1
Host: auth.api.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=rt_test_7Qx2Lw&client_id=tpp_test_client
JSON
{
  "access_token": "at_test_n4Vb1k",
  "token_type": "Bearer",
  "expires_in": 300,
  "refresh_token": "rt_test_Q9pZr3"
}

Section 6 lets the server issue a new refresh token; the client then must discard the old one, and the server may revoke it. A rejected refresh returns HTTP 400 with invalid_grant, which section 5.2 defines for a refresh token that is "invalid, expired, revoked" or issued to another client:

Request
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store

{"error":"invalid_grant","error_description":"refresh token is not active"}

The test asserts that the product stores the new refresh token before it uses the new access token, and that a lost response does not leave the stored token pointing at a revoked value.

3. Race two refreshes on one grant

Python
import concurrent.futures

import requests

TOKEN_URL = "https://auth.api.example.com/as/token.oauth2"
CLIENT_CERT = ("tpp_test.crt", "tpp_test.key")
CLIENT_ID = "tpp_test_client"


def refresh(token: str) -> tuple[int | None, dict]:
    form = {"grant_type": "refresh_token", "refresh_token": token, "client_id": CLIENT_ID}
    try:
        resp = requests.post(TOKEN_URL, data=form, cert=CLIENT_CERT, timeout=10)
    except requests.RequestException as exc:
        return None, {"error": "transport", "detail": type(exc).__name__}
    try:
        return resp.status_code, resp.json()
    except ValueError:
        return resp.status_code, {"error": "non_json", "detail": resp.text[:200]}


def race(refresh_token: str) -> str:
    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
        results = list(pool.map(refresh, [refresh_token, refresh_token]))
    winners = [body for status, body in results if status == 200]
    if not winners:
        return f"no refresh succeeded: {results}"
    follow_up = winners[0].get("refresh_token", refresh_token)
    status, body = refresh(follow_up)
    if status == 200:
        return "grant survived the race"
    return f"grant revoked after the race: {status} {body.get('error')}"


if __name__ == "__main__":
    print(race("rt_test_7Qx2Lw"))

Run it once against each bank sandbox and record the result per bank. "Grant revoked after the race" means the product must serialise refreshes per consent.

4. Read expired and revoked consents the way the bank reports them

The UK Read/Write Data API profile v4.0.1 (read on 2026-09-22) returns 401 for an expired access token. When the bank expires the token because the consent end date passed, the consent moves to EXPD with reason TKXP; when the bank requires SCA again, it moves to CANC with TKSP and U028. Normal access token expiry must not change the consent status. The provider reads the status with a client credentials token, subject to the bank's fair usage policy:

Request
GET /open-banking/v4.0/aisp/account-access-consents/urn-testbank-intent-0001 HTTP/1.1
Host: api.example.com
Authorization: Bearer cc_test_token_not_real
x-fapi-interaction-id: 7b0e6c1e-3f7d-4d52-9b1a-2f0c8e5d6a10
Accept: application/json
JSON
{
  "Data": {
    "ConsentId": "urn-testbank-intent-0001",
    "Status": "EXPD",
    "StatusReason": [{ "StatusReasonCode": "TKXP" }],
    "StatusUpdateDateTime": "2027-03-01T00:00:05+00:00",
    "CreationDateTime": "2026-09-01T10:00:00+00:00",
    "Permissions": ["ReadAccountsDetail", "ReadBalances", "ReadTransactionsCredits", "ReadTransactionsDebits", "ReadTransactionsDetail"],
    "ExpirationDateTime": "2027-03-01T00:00:00+00:00"
  },
  "Risk": {},
  "Links": { "Self": "https://api.example.com/open-banking/v4.0/aisp/account-access-consents/urn-testbank-intent-0001" },
  "Meta": { "TotalPages": 1 }
}

The Account and Transaction API profile allows re-authentication of the same consent while its status is AUTH or CANC and its ExpirationDateTime has not passed. A product that creates a new consent for every reauthorisation leaves duplicates at the bank.

Berlin Group NextGenPSD2 (OpenAPI file v1.3.16, dated 27 November 2025, read on 2026-09-22) exposes the status separately. consentStatus takes received, rejected, valid, revokedByPsu, expired, terminatedByTpp, or partiallyAuthorised, and its HTTP 401 message codes for account information include CONSENT_EXPIRED and CONSENT_INVALID:

Request
GET /v1/consents/test-consent-0001/status HTTP/1.1
Host: api.example.com
X-Request-ID: 99391c7e-ad88-49ec-a2ad-99ddcb1f7721
Authorization: Bearer at_test_not_real
JSON
{ "consentStatus": "revokedByPsu" }
JSON
{ "tppMessages": [{ "category": "ERROR", "code": "CONSENT_EXPIRED" }] }

Map each value to one product state. On revokedByPsu, the customer withdrew access at the bank, so the product stops pulls without retrying.

5. Check every clock the flow depends on

expires_in is relative: RFC 6749 section 5.1 counts it from the time the server generated the response. The UK http://openbanking.org.uk/refresh_token_expires_at claim, the Berlin Group validUntil date, and the RTS day counts are absolute and meet the provider's own clock.

Python
from datetime import date, datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo


def access_token_deadline(expires_in: int, request_sent_at: datetime) -> datetime:
    """Anchor on the send time, which precedes the server's response time, minus a 30-second margin."""
    return request_sent_at + timedelta(seconds=expires_in - 30)


def berlin_group_consent_end(valid_until: str, bank_zone: str) -> datetime:
    """validUntil is an inclusive date in the bank's local time; access ends at the next local midnight."""
    day = date.fromisoformat(valid_until)
    return datetime.combine(day + timedelta(days=1), time.min, tzinfo=ZoneInfo(bank_zone))


sent = datetime(2026, 9, 22, 8, 0, 0, tzinfo=timezone.utc)
assert access_token_deadline(300, sent) == datetime(2026, 9, 22, 8, 4, 30, tzinfo=timezone.utc)
assert berlin_group_consent_end("2027-03-31", "Europe/Berlin").astimezone(timezone.utc) == datetime(
    2027, 3, 31, 22, 0, tzinfo=timezone.utc
)

The anchors and races vendor documentation leaves to you

Rotation turns a race into a lost grant. RFC 9700 section 4.14.2 (January 2025) describes reuse detection: when an invalidated refresh token is presented, the server cannot tell attacker from client, so it revokes the active refresh token too, "at the cost of forcing the legitimate client to obtain a fresh authorization grant". A bank may rotate tokens for confidential clients too, under RFC 6749 section 6. Two workers sharing a consent, or a retry after a timed-out refresh, then present the old token and kill the grant. A fresh grant needs the customer back in a redirect to the bank. Serialise refreshes with a lock keyed by consent ID, and treat invalid_grant straight after a parallel refresh as a race signal before you mark the consent revoked. The UK profile adds one rule: after re-authentication, the provider must not use access or refresh tokens issued earlier for that consent.

The 180-day anchor moves. Any access through the provider where SCA was applied restarts the EU count, including SCA the bank applied under Article 10a(3). Record the timestamp of every SCA and derive the next due date from the latest one. The exemption also covers only the balance and transactions executed in the last 90 days; a request for older history falls outside it on any day, which the SCA exemptions testing guide treats as a separate case.

validUntil ends at midnight in the bank's time zone. The Berlin Group OpenAPI defines validUntil as "the local ASPSP date", inclusive, and allows the bank to adjust future dates. A provider in UTC that reads "2027-03-31" as ending at 23:59:59 UTC keeps calling a Berlin bank for two hours after its consent has expired and collects CONSENT_EXPIRED errors. A provider that reads it as 00:00 UTC stops almost a day early. Read the date back after creation, because the bank may have shortened it.

Test matrix

CaseClock or inputExpected result
EU first access through providerNo prior SCASCA required
EU day 180Exactly 180 days after last SCABackground pull succeeds
EU past day 180180 days and 1 second after last SCACustomer asked for SCA
EU older historyTransactions older than 90 daysTreated as outside the exemption
UK day 8989 days 23 hours after reconfirmationBackground pull succeeds
UK day 9090 days after reconfirmationPulls stop, reconfirmation requested
UK bank asks for SCAConsent CANC with TKSP and U028Same consent re-authenticated
Access token expiryToken past expires_inRefresh, consent status unchanged
Parallel refreshTwo refreshes, one tokenOne grant survives
Revoked at bankrevokedByPsuPulls stop, no retries
Consent end datevalidUntil 2027-03-31, Berlin bankLast call before 2027-03-31T22:00:00Z

The open banking API testing service runs this matrix as the consent and reauthorisation flow test suite, and the open banking standards page lists the UK and Berlin Group versions it runs against.

Questions

How long does open banking consent last?

An open banking consent lasts until its expiry date; a UK Account Access Consent without an ExpirationDateTime is open ended. Background access has shorter limits: in the EU, the bank applies strong customer authentication again once more than 180 days have passed since the last one through that provider, and in the UK, the account information provider reconfirms consent with the customer every 90 days.

How long do refresh tokens last?

OAuth 2.0 (RFC 6749) sets no refresh token lifetime. RFC 9700 says refresh tokens should expire after a period of client inactivity chosen by the authorization server. Under the UK Open Banking Read/Write Data API profile, a bank that issues an expiring refresh token states the expiry in the http://openbanking.org.uk/refresh_token_expires_at claim of the ID token.

Scope your testing

Send FintechAssuranceLabs your product, segment, money flows, and the standards that need evidence, and get a proposed test scope with its deliverables.

Scope your testing