Dynamic client registration via bank's API with eIDAS certificates
TL;DR
Importance of Additional Assets for API Security: Besides eIDAS certificates, TPPs need client IDs, API keys, and other assets for secure API access. These assets enhance security and allow ASPSPs (banks) to verify TPP identities.
Dynamic Client Registration to Simplify API Access: Dynamic client registration APIs eliminate the need for manual setup across multiple banks by automating client ID and credential acquisition, provided a TPP has valid eIDAS certificates.
Implementation Focus on Open Banking UK Standards
We illustrate dynamic registration following the Open Banking UK standard, including Python code for generating keys, creating software statements, and completing registration.
To access open banking APIs, third-party providers (TPPs) typically need additional assets beyond eIDAS certificates, such as a client ID, API key, secret key, or similar. These assets allow the Account Servicing Payment Service Provider (ASPSP), or bank, to verify the TPP’s identity when using PSD2-compliant APIs. They also provide an extra layer of security in case a TPP’s eIDAS certificate private keys are compromised.
Usually the set of the assets depends on the platform used by ASPSP to provide the APIs and/or certain open banking standard according to which the APIs are implemented. In many cases these assets can only be acquired manually via bank's developer portal; a TPP is supposed to sign up for the developer portal using web interface and in the profile UI do certain actions, such as creation of an application and selection of APIs to be used. This leads to the problem that in order to access open banking (PSD2) APIs of multiple ASPSPs (banks) TPPs are required to make significant number of manual actions.
Dynamic client registration helps to avoid manual actions preceding usage of the APIs by providing dedicated APIs for creation of client ID and similar assets. The only requirement to use these APIs is to have valid eIDAS certificates (in sandbox environments test certificates shall be used). A number of different implementations are used across banks in Europe, but in this post we are focusing on Open Banking UK standard for dynamic client registration, which is the most used.
Background
As mentioned in the introduction the registration is done by sending an HTTP-request with desired client data to the authorization server. The registration response contains a client credentials to be used when accessing account information or payment initiation APIs.
In this post we are going through the basic concepts of dynamic client registration and provide code snippets in Python. A number of abbreviations is used throughout the post, you can consult with the glossary at the end of the article, in case you are unfamiliar with some.
Prior to the dynamic client registration process you would need to obtain QWAC (mTLS) and QSeal (signing) certificates. For production use you would need to have the certificates issued by a QTSP, but in sandbox environments many banks allow to use even self-signed certificates. Previously we wrote how to generate self-signed certificate using OpenSSL.
Open Banking UK standard provides detailed description of the dynamic TPP registration process (how to register in bank's API) and it can be found here. The standard itself is based on OAuth 2.0 dynamic client registration protocol.
The registration API endpoint supports HTTP-methods:
POST: register a new client
GET: get client information
PUT: update client information
DELETE: delete client information
We will focus on the first method.
Prerequisites
All examples below are written using Python and following dependencies:
requests==2.22.0
cryptography==2.6.1
For this tutorial we consider that our bank's URL is https://example-bank.com and our company's URL is https://example-company.com
We also have our certificates files according to the following scheme:
qwac:
qwac.crt
qwac.key
qseal:
qseal.key
Some aspects may vary between banks (even declaring to implement their APIs according to the Open Banking UK standard), so the examples may need to be a little bit adjusted.
Step-by-step guide
1.First thing you need to do is to convert your qseal.key (in PEM format) to a JWKS format.
It is possible to use some online or ready-made tools for this purpose.
For example, you can use this docker image.
Note that you need public representation of your key. Be careful not to expose any key private data!
We are going to save it under qseal.jwks
The result should look something like this:
{
"kty": "RSA",
"e": "AQAB",
"kid": "11111111-2222-aaaa-bbbb-ccccccff3344",
"n": "SGVyZSBpcyBzb21lIGluZm9ybWF0aW9uIG9mIG91ciBwcml2YXRlIGtleQ"
}
2.You are also need to create same format file but with list of revoked certificates. We will save it under qseal-revoked.jwks
If you don't have revoked certificates, you can create an empty file:
{
"keys": []
}
3.Next, we are going to create a software statement.
RFC 7591 defines a Software Statement as:
A digitally signed or MACed JSON Web Token (JWT) that asserts metadata values about the client software. In some cases, a software statement will be issued directly by the client developer. In other cases, a software statement will be issued by a third-party organization for use by the client developer. In both cases, the trust relationship the authorization server has with the issuer of the software statement is intended to be used as an input to the evaluation of whether the registration request is accepted. A software statement can be presented to an authorization server as part of a client registration request.
Below are functions we are going to use in order to create JWT and sign it.
You need to replace urls and path to a private key on your computer
import base64
import datetime
import json
import os
import uuid
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
import requests
def sign(payload: str) -> str:
key_content = open('qseal/qseal.key', 'rb').read() # you probably need to replace this path
backend = default_backend()
key = backend.load_pem_private_key(key_content, None)
payload_encoded: bytes = payload.encode('utf-8')
signature = key.sign(payload_encoded, padding.PKCS1v15(), hashes.SHA256())
return base64\
.b64encode(signature)\
.decode('utf8')\
.replace('=', '')\
.replace('/', '_')\
.replace('+', '-')
def get_software_statement() -> str:
header = {
'alg': 'RS256',
'kid': '11111111-2222-aaaa-bbbb-ccccccff3344', # our private key id
'typ': 'JWT'
}
timestamp = int(datetime.datetime.now().timestamp())
jti = str(uuid.uuid4())
print(f'jti is: {jti}')
software_id = str(uuid.uuid4())
print(f'software_id is: {software_id}')
countries = ['UK']
body = {
'iss': 'PSDUK-UKBXXX-12345678', # Global unique reference number assigned to your company along with issued certificates
'iat': timestamp,
'exp': timestamp + 60 * 10,
'aud': 'https://example-bank.com',
'jti': jti,
'software_id': software_id,
'software_client_name': 'Company Name',
'software_version': 1,
'scope': [
{
"role": "AIS",
"member_state": countries
},
{
"role": "PIS",
"member_state": countries
}
],
'software_contacts': [
{
"name": "First Second",
"email": "example@example-company.com",
"phone": "+1234567890",
"type": "Business"
}
],
'software_redirect_uris': [
'https://example-company.com/auth_redirect/' # link to your company's website redirect url
],
'software_client_uri': 'https://example-company.com/',
'software_logo_uri': 'https://example-company.com/company-logo.png', # link to a company logo
'software_tos_uri': 'https://example-company.com/tos/', # link to a company's terms of service
'software_policy_uri': 'https://example-company.com/policy/', # link to a company's policy
'software_jwks_endpoint': 'https://example-company.com/qseal.jwks', # link to a created on the step 2 certificate
'software_jwks_revoked_endpoint': 'https://example-company.com/qseal-revoked.jwks' # link to a created on the step 3 certificate
}
header_base64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode()
header_base64 = header_base64.replace('=', '')
body_base64 = base64.urlsafe_b64encode(json.dumps(body).encode()).decode()
body_base64 = body_base64.replace('=', '')
signature = sign(f'{header_base64}.{body_base64}')
return f'{header_base64}.{body_base64}.{signature}'
4.After we created a software statement, we are ready to construct a registration request:
def register() -> requests.Response:
payload = {
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["authorization_code", "client_credentials"],
"software_statement": get_software_statement(),
"id_token_signed_response_alg": "RS256",
"request_object_signing_alg": "RS256"
}
response = requests.post(
'https://example-bank.com/register', # registration endpoint
json=payload,
cert=('qwac/qwac.crt', 'qwac/qwac.key') # paths to a qwac certificate and a key
)
return response
5.Finally, execute the request:
register_data = register()
print(register_data.text)
If our request was successfull then response should look something like this:
{
"client_secret": "some_secret",
"client_id": "some_id",
"redirect_uris": [
"https://example-company.com/auth_redirect/"
],
"grant_types": [
"authorization_code",
"client_credentials"
],
"token_endpoint_auth_method": "client_secret_post",
"logo_uri": "https://example-company.com/company-logo.png",
"jwks_uri": "https://example-company.com/qseal.jwks",
"software_roles": [
{
"role": "AIS",
"member_state": [
"UK"
]
},
{
"role": "PIS",
"member_state": [
"UK"
]
}
],
"id_token_signed_response_alg": "RS256",
"request_object_signing_alg": "RS256",
"software_client_name": "Example Company",
"scope": "openid accounts payments"
}
Now you can use client_id and client_secret to access other open banking API endpoints!
Glossary
ASPSP - Account Servicing Payment Service Provider, i.e. a bank
eIDAS certificate – X.509 public key certificate containing certain fields
JWKS – JSON Web Key Set
JWT – JSON Web Token
QWAC – Qualified Website Application Certificate, i.e. client TLS certificate
QSealC – Qualified Electronic Sealing Certificate, i.e. certificate used for signing API requests
PSU – Payment Service User, i.e. end-user / bank’s customer
SSA – Software Statement Assertion
TPP – Third party provider, i.e. a service using banks’ APIs