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:
- ✅ Certificate chain to trusted CA
- ✅ Certificate validity period
- ✅ TPP authorization number in certificate
- ✅ Certificate not revoked (OCSP check)
Scopes and Permissions
📋 Available OAuth Scopes
Account Information (AIS)
| Scope | Description | Access Level |
|---|---|---|
accounts | List accounts | Basic account info |
balances | Account balances | Real-time balances |
transactions | Transaction history | 90 days history |
accounts-details | Full account details | All account info |
Payment Initiation (PIS)
| Scope | Description | Payment Types | Status |
|---|---|---|---|
payments | Single payments | Domestic, SEPA, RON | ✅ Supported |
bulk-payments | Bulk payments | Multiple transfers | ❌ Not supported |
periodic-payments | Standing orders | Recurring payments | ❌ Not supported |
payment-cancellation | Cancel payments | Before execution | ✅ Supported |
Consent Management
| Scope | Description | Operations |
|---|---|---|
consents | Manage consents | Create, view, delete |
consent-validation | Validate consent | Check validity |
Scope Combinations
Common scope combinations:
// Read-only account access
scope: "accounts balances transactions"
// Payment initiation
scope: "payments payment-cancellation"
// Full access
scope: "accounts balances transactions payments consents"
Error Handling
⚠️ Common Errors and Solutions
OAuth Errors
| Error | Description | Solution |
|---|---|---|
invalid_request | Missing or invalid parameter | Check required parameters |
unauthorized_client | Client not authorized | Verify client_id |
access_denied | User denied consent | Handle gracefully |
invalid_scope | Requested scope invalid | Check available scopes |
server_error | Internal server error | Retry with backoff |
Certificate Errors
| Error | Description | Solution |
|---|---|---|
certificate_invalid | Invalid certificate | Check certificate validity |
certificate_expired | Certificate expired | Renew certificate |
certificate_not_found | Certificate not registered | Register with Paysera |
certificate_revoked | Certificate revoked | Obtain new certificate |
Token Errors
| Error | Description | Solution |
|---|---|---|
invalid_token | Token invalid or expired | Refresh token |
insufficient_scope | Token lacks required scope | Request new consent |
token_expired | Access token expired | Use refresh token |
invalid_grant | Refresh token invalid | Re-authenticate user |
Error Response Format
{
"error": "invalid_request",
"error_description": "The redirect_uri is missing",
"error_uri": "https://docs.paysera.com/errors#invalid_request"
}
Testing & Debugging
Common Issues
Certificate Issues:
# Test certificate connectivity
openssl s_client -connect open-banking-api.paysera.com:443 \
-cert qwac-cert.pem \
-key qwac-key.pem \
-CAfile ca-bundle.pem
Token Issues:
// Debug token expiry
console.log('Token expires at:', new Date(tokenExpiryTimestamp));
console.log('Time remaining:', tokenExpiryTimestamp - Date.now());
Request Debugging:
// Log all requests for debugging
console.log('Request:', {
url: request.url,
headers: request.headers,
method: request.method
## Resources
- 📖 [Getting Started Guide](/guides/open-banking/getting-started)
- 🔐 [Security Requirements](/guides/open-banking/getting-started/security)
- 💻 [Code Examples](/guides/open-banking/examples)
- 📚 [API Reference](/api/open-banking)
- ❓ [FAQ](/guides/open-banking/resources/faq)
:::tip[Pro Tips]
- Always implement PKCE for OAuth flows
- Cache discovery endpoint response
- Implement automatic token refresh before expiry
- Use SDK libraries when available
- Monitor certificate expiry dates proactively
:::