Authentication
Comprehensive guide for Open Banking API authentication using OAuth 2.0 with mutual TLS.
🔐 Authentication Overview
- Overview
- Requirements
Authentication Methods
Open Banking API uses dual authentication:
-
🔑 Certificate Authentication (mTLS)
- QWAC certificate for TLS client authentication
- QSealC certificate for request signing
- Required for all API requests
-
🔐 OAuth 2.0 Authorization
- Authorization Code flow with PKCE
- User consent and authentication
- Access token for API requests
Technical Requirements
Certificates:
- ✅ Valid QWAC certificate (eIDAS qualified)
- ✅ QSealC certificate for payment signing
- ✅ Certificate chain validation
- ✅ TLS 1.2 minimum (1.3 recommended)
OAuth Implementation:
- ✅ Authorization Code flow
- ✅ PKCE (RFC 7636) support
- ✅ State parameter for CSRF protection
- ✅ Secure token storage
Security:
- ✅ HTTPS only communication
- ✅ Certificate pinning (mobile apps)
- ✅ Token rotation on refresh
- ✅ Secure redirect URI
OAuth 2.0 Implementation
- Authorization Flow
- 1️⃣ Authorization
- 2️⃣ Token Exchange
- 3️⃣ API Requests
- 🔄 Token Refresh
Authorization Code Flow with PKCE
Configuration Endpoint
Version-Specific Endpoints
OAuth endpoints are version-specific. Use the appropriate base path for your API version:
- Berlin Group v1.3:
https://open-banking-api.paysera.com/xs2a/berlin/1.3/ - Georgia v0.8:
https://open-banking-api.paysera.com/xs2a/georgia/0.8/
# For Berlin Group v1.3
https://open-banking-api.paysera.com/xs2a/berlin/1.3/.well-known/oauth-authorization-server
# For Georgia v0.8
https://open-banking-api.paysera.com/xs2a/georgia/0.8/.well-known/oauth-authorization-server
Step 1: Authorization Request
Generate PKCE parameters and redirect user:
// Generate PKCE parameters
const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(sha256(codeVerifier));
const state = base64url(crypto.randomBytes(16));
// Build authorization URL (use your API version's base path)
// For Berlin Group v1.3:
const authUrl = new URL('https://open-banking-api.paysera.com/xs2a/berlin/1.3/oauth/authorize');
// For Georgia v0.8: use 'https://open-banking-api.paysera.com/xs2a/georgia/0.8/oauth/authorize'
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.append('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.append('scope', 'accounts payments');
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');
// Redirect user
window.location.href = authUrl.toString();
Parameters:
| Parameter | Description | Required |
|---|---|---|
response_type | Must be code | ✅ |
client_id | Your client identifier | ✅ |
redirect_uri | Registered callback URL | ✅ |
scope | Requested permissions | ✅ |
state | CSRF protection token | ✅ |
code_challenge | PKCE challenge | ✅ |
code_challenge_method | Must be S256 | ✅ |
Step 2: Exchange Code for Token
After user authorization, exchange the code:
// Handle callback
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const returnedState = urlParams.get('state');
// Verify state
if (returnedState !== savedState) {
throw new Error('State mismatch - possible CSRF attack');
}
// Exchange code for token (use your API version's base path)
const tokenResponse = await fetch('https://open-banking-api.paysera.com/xs2a/berlin/1.3/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: savedCodeVerifier
})
});
const tokens = await tokenResponse.json();
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200a1b2c3...",
"scope": "accounts payments"
}
Step 3: Making API Requests
Use the access token in API calls:
// API request with access token (use your API version's base path)
const response = await fetch('https://open-banking-api.paysera.com/xs2a/berlin/1.3/v1/accounts', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-Request-ID': generateUUID(),
'Accept': 'application/json'
},
// mTLS configuration
agent: new https.Agent({
cert: fs.readFileSync('qwac-cert.pem'),
key: fs.readFileSync('qwac-key.pem'),
ca: fs.readFileSync('ca-bundle.pem')
})
});
const accounts = await response.json();
Required Headers:
| Header | Description |
|---|---|
Authorization | Bearer token |
X-Request-ID | Unique request identifier |
Accept | Content type (application/json) |
Content-Type | For POST/PUT requests |
Refreshing Access Tokens
Tokens expire after 1 hour. Use refresh token to get new access token:
// Refresh token request (use your API version's base path)
const refreshResponse = await fetch('https://open-banking-api.paysera.com/xs2a/berlin/1.3/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: savedRefreshToken,
client_id: 'YOUR_CLIENT_ID'
})
});
const newTokens = await refreshResponse.json();
// Update stored tokens
saveTokens({
accessToken: newTokens.access_token,
refreshToken: newTokens.refresh_token,
expiresAt: Date.now() + (newTokens.expires_in * 1000)
});
Token Lifecycle:
- Access token: 1 hour validity
- Refresh token: 180 days validity
- Automatic rotation on refresh
- Revocation endpoint available
Mutual TLS (mTLS)
🔑 Certificate Configuration
mTLS Setup
Configure your HTTP client with certificates:
- Node.js
- Python
- Java
const https = require('https');
const fs = require('fs');
// Certificate configuration
const httpsAgent = new https.Agent({
cert: fs.readFileSync('path/to/qwac-cert.pem'),
key: fs.readFileSync('path/to/qwac-key.pem'),
ca: fs.readFileSync('path/to/ca-bundle.pem'),
rejectUnauthorized: true
});
// Using with fetch
const response = await fetch('https://open-banking-api.paysera.com/v1/accounts', {
agent: httpsAgent,
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.poolmanager import PoolManager
import ssl
class SSLAdapter(HTTPAdapter):
def __init__(self, certfile, keyfile, cafile, *args, **kwargs):
self.certfile = certfile
self.keyfile = keyfile
self.cafile = cafile
super().__init__(*args, **kwargs)
def init_poolmanager(self, *args, **kwargs):
context = ssl.create_default_context(cafile=self.cafile)
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
kwargs['ssl_context'] = context
return super().init_poolmanager(*args, **kwargs)
# Setup session with certificates
session = requests.Session()
adapter = SSLAdapter(
certfile='qwac-cert.pem',
keyfile='qwac-key.pem',
cafile='ca-bundle.pem'
)
session.mount('https://', adapter)
# Make request
response = session.get(
'https://open-banking-api.paysera.com/v1/accounts',
headers={'Authorization': f'Bearer {access_token}'}
)
import javax.net.ssl.*;
import java.security.KeyStore;
import java.io.FileInputStream;
// Load client certificate
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("qwac-keystore.p12"),
"password".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keyStore, "password".toCharArray());
// Load CA certificates
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream("ca-truststore.jks"),
"password".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(trustStore);
// Create SSL context
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(),
tmf.getTrustManagers(),
new SecureRandom());
// Configure HTTP client
HttpsURLConnection.setDefaultSSLSocketFactory(
sslContext.getSocketFactory()
);
Certificate Validation
Paysera validates: