Skip to content

Commit 695da41

Browse files
authored
Merge pull request #155 from constantine2nd/develop
feature: Add Berlin Group PSD2 signature support
2 parents 89ded03 + 28fdfd4 commit 695da41

8 files changed

Lines changed: 572 additions & 9 deletions

File tree

.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ VITE_OBP_CONSUMER_KEY=your-obp-oidc-client-id
4848
# VITE_CUSTOM_OIDC_CLIENT_ID=your-custom-client-id
4949
# VITE_CUSTOM_OIDC_CLIENT_SECRET=your-custom-client-secret
5050

51+
### Berlin Group TPP Signature Certificate Configuration (Optional) ###
52+
# VITE_BG_PRIVATE_KEY_PATH=./certs/private_key.pem
53+
# VITE_BG_CERTIFICATE_PATH=./certs/certificate.pem
54+
# VITE_BG_KEY_ID=SN=1082, CA=CN=Your Name, O=YourOrg
55+
# VITE_BG_API_VERSION=v1.3
56+
# VITE_BG_PSU_DEVICE_ID=device-1234567890
57+
# VITE_BG_PSU_DEVICE_NAME=API-Explorer-II
58+
# VITE_BG_PSU_IP_ADDRESS=127.0.0.1
59+
# VITE_BG_TPP_REDIRECT_URI=https://your-app.com/berlin-group/redirect
60+
# VITE_BG_TPP_NOK_REDIRECT_URI=https://your-app.com/berlin-group/error
61+
5162
### Chatbot Configuration (Optional) ###
5263
VITE_CHATBOT_ENABLED=false
5364
# VITE_CHATBOT_URL=http://localhost:5000
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# Berlin Group TPP Signature Certificate Support
2+
3+
## Overview
4+
5+
API Explorer II now supports the Berlin Group NextGenPSD2 standard. When configured with a TPP (Third Party Provider) certificate, the server automatically signs every request to a Berlin Group API endpoint. This enables PSD2-compliant access to Account Information Services (AIS) and Payment Initiation Services (PIS) through banks that implement the Berlin Group standard.
6+
7+
The feature is entirely optional. Without certificate configuration, the application behaves exactly as before.
8+
9+
## What You Need Before Starting
10+
11+
### 1. TPP Certificate Files
12+
13+
You need two PEM files issued by your bank or a qualified trust service provider (QTSP):
14+
15+
- **Private key** (`private_key.pem`) -- RSA private key used to sign requests
16+
- **Certificate** (`certificate.pem`) -- The corresponding TPP certificate that the bank uses to verify your signatures
17+
18+
These are typically provided as part of the TPP onboarding process with the bank.
19+
20+
### 2. Key Identifier
21+
22+
The `keyId` string that identifies your certificate to the bank. This is part of the Signature header and typically looks like:
23+
24+
```
25+
SN=1082, CA=CN=Your Name, O=YourOrg
26+
```
27+
28+
Your bank will tell you what value to use, or it can be derived from the certificate's serial number and issuer.
29+
30+
### 3. OBP-API Backend with Berlin Group Support
31+
32+
The OBP-API instance must serve Berlin Group endpoints at paths like:
33+
34+
```
35+
/berlin-group/v1.3/consents
36+
/berlin-group/v1.3/accounts
37+
/berlin-group/v1.3/payments/sepa-credit-transfers
38+
```
39+
40+
### 4. Redirect URIs (for consent and payment flows)
41+
42+
Two redirect URLs that the bank will use during PSU (Payment Service User) authorization:
43+
44+
- **Success redirect** -- where the user returns after authorizing a consent or payment
45+
- **Error redirect** -- where the user returns if authorization fails or is cancelled
46+
47+
## Setup
48+
49+
### Step 1: Place Certificate Files
50+
51+
Put your PEM files in a location accessible to the server, for example:
52+
53+
```
54+
certs/
55+
private_key.pem
56+
certificate.pem
57+
```
58+
59+
### Step 2: Configure Environment Variables
60+
61+
Add these to your `.env` file:
62+
63+
```bash
64+
# Required -- paths to your certificate files
65+
VITE_BG_PRIVATE_KEY_PATH=./certs/private_key.pem
66+
VITE_BG_CERTIFICATE_PATH=./certs/certificate.pem
67+
68+
# Required -- your certificate's key identifier
69+
VITE_BG_KEY_ID=SN=1082, CA=CN=Your Name, O=YourOrg
70+
71+
# Berlin Group API version (default: v1.3)
72+
VITE_BG_API_VERSION=v1.3
73+
74+
# PSU device identification
75+
VITE_BG_PSU_DEVICE_ID=device-1234567890
76+
VITE_BG_PSU_DEVICE_NAME=API-Explorer-II
77+
VITE_BG_PSU_IP_ADDRESS=192.168.1.42
78+
79+
# Redirect URIs for consent/payment authorization flows
80+
VITE_BG_TPP_REDIRECT_URI=https://your-app.com/berlin-group/redirect
81+
VITE_BG_TPP_NOK_REDIRECT_URI=https://your-app.com/berlin-group/error
82+
```
83+
84+
Only `VITE_BG_PRIVATE_KEY_PATH` and `VITE_BG_CERTIFICATE_PATH` are required to enable the feature. Everything else has sensible defaults.
85+
86+
### Step 3: Start the Server
87+
88+
On startup, the console confirms whether the feature is active:
89+
90+
```
91+
--- Berlin Group TPP Signature Certificate -----------------------
92+
OK Berlin Group TPP Signature Certificate is configured and loaded
93+
API Version: v1.3
94+
-----------------------------------------------------------------
95+
```
96+
97+
If not configured, you will see:
98+
99+
```
100+
--- Berlin Group TPP Signature Certificate -----------------------
101+
Berlin Group TPP Signature Certificate is NOT configured
102+
Set VITE_BG_PRIVATE_KEY_PATH and VITE_BG_CERTIFICATE_PATH to enable
103+
-----------------------------------------------------------------
104+
```
105+
106+
## How It Works
107+
108+
```
109+
Browser API Explorer II OBP-API / Bank
110+
| | |
111+
| GET /api/get?path= | |
112+
| /berlin-group/v1.3/accounts | |
113+
| (+ optional X-BG-Consent-ID) | |
114+
| -------------------------------->| |
115+
| | |
116+
| Detects "/berlin-group/" in path |
117+
| Generates signature headers: |
118+
| - SHA-256 body digest |
119+
| - RSA-SHA256 digital signature |
120+
| - TPP certificate |
121+
| - PSU identification |
122+
| - Consent-ID (if provided) |
123+
| Includes OAuth2 Bearer token (if logged in) |
124+
| | |
125+
| | GET /berlin-group/v1.3/accounts |
126+
| | + Digest, Signature, Certificate |
127+
| | + Consent-ID, PSU headers |
128+
| | ------------------------------------>|
129+
| | |
130+
| | JSON response |
131+
| |<------------------------------------ |
132+
| JSON response | |
133+
|<---------------------------------| |
134+
```
135+
136+
Key points:
137+
138+
- **Automatic detection** -- Any request whose path contains `/berlin-group/` triggers the signing. Standard OBP paths (e.g., `/obp/v5.1.0/banks`) are unaffected.
139+
- **Transparent to the frontend** -- The browser uses the same proxy endpoints (`/api/get`, `/api/create`, `/api/update`, `/api/delete`) as for any other API call.
140+
- **OAuth2 and TPP coexist** -- If the user is logged in via OAuth2, the Bearer token is sent alongside the TPP signature headers. Both authentication mechanisms can be active on the same request.
141+
- **Consent-ID passthrough** -- For endpoints that require a consent (e.g., reading accounts), the frontend includes an `X-BG-Consent-ID` header on its request to the proxy. The server forwards it as the standard `Consent-ID` header to the bank.
142+
143+
## Typical PSD2 Workflows
144+
145+
### Account Information (AIS)
146+
147+
1. **Create a consent** -- POST to `/berlin-group/v1.3/consents` with the desired account access scope. The bank returns a `consentId` and a redirect link for the PSU to authorize.
148+
2. **PSU authorizes** -- The user is redirected to the bank's authorization page, then back to your redirect URI.
149+
3. **Check consent status** -- GET `/berlin-group/v1.3/consents/{consentId}/status` to confirm it is `valid`.
150+
4. **Read accounts** -- GET `/berlin-group/v1.3/accounts` with the `Consent-ID` header.
151+
5. **Read transactions** -- GET `/berlin-group/v1.3/accounts/{accountId}/transactions` with the `Consent-ID` header.
152+
6. **Delete consent** -- DELETE `/berlin-group/v1.3/consents/{consentId}` when access is no longer needed.
153+
154+
### Payment Initiation (PIS)
155+
156+
1. **Initiate payment** -- POST to `/berlin-group/v1.3/payments/sepa-credit-transfers` with debtor/creditor accounts and amount. The bank returns a `paymentId` and a redirect link.
157+
2. **PSU authorizes** -- The user is redirected to the bank to authorize the payment, then back to your redirect URI.
158+
3. **Check payment status** -- GET `/berlin-group/v1.3/payments/sepa-credit-transfers/{paymentId}/status`.
159+
160+
## What Gets Signed
161+
162+
Every outgoing Berlin Group request includes these headers, generated fresh per request:
163+
164+
| Header | Purpose |
165+
| ------------------------------------------------------ | ------------------------------------------------------------------- |
166+
| `Date` | Timestamp in RFC 7231 format |
167+
| `X-Request-ID` | Unique UUID per request for traceability |
168+
| `Digest` | SHA-256 hash of the request body |
169+
| `Signature` | RSA-SHA256 digital signature of the digest, date, and request ID |
170+
| `TPP-Signature-Certificate` | The TPP certificate for the bank to verify the signature |
171+
| `PSU-Device-ID` / `PSU-Device-Name` / `PSU-IP-Address` | PSU device identification |
172+
| `TPP-Redirect-URI` / `TPP-Nok-Redirect-URI` | Where to redirect the user after authorization (POST requests only) |
173+
| `Consent-ID` | References an existing consent (when provided by the frontend) |
174+
175+
## Configuration Reference
176+
177+
| Variable | Required | Default | Description |
178+
| ------------------------------ | ----------- | --------------------------- | -------------------------------- |
179+
| `VITE_BG_PRIVATE_KEY_PATH` | Yes | -- | Path to RSA private key PEM file |
180+
| `VITE_BG_CERTIFICATE_PATH` | Yes | -- | Path to TPP certificate PEM file |
181+
| `VITE_BG_KEY_ID` | Recommended | `SN=unknown, CA=CN=unknown` | Certificate key identifier |
182+
| `VITE_BG_API_VERSION` | No | `v1.3` | Berlin Group API version |
183+
| `VITE_BG_PSU_DEVICE_ID` | No | `device-api-explorer-ii` | PSU device identifier |
184+
| `VITE_BG_PSU_DEVICE_NAME` | No | `API-Explorer-II` | PSU device name |
185+
| `VITE_BG_PSU_IP_ADDRESS` | No | `127.0.0.1` | PSU IP address |
186+
| `VITE_BG_TPP_REDIRECT_URI` | No | (empty) | Success redirect URI |
187+
| `VITE_BG_TPP_NOK_REDIRECT_URI` | No | (empty) | Error redirect URI |
188+
189+
## Troubleshooting
190+
191+
| Symptom | Cause | Fix |
192+
| ------------------------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------- |
193+
| Startup says "NOT configured" | Certificate env vars not set | Set `VITE_BG_PRIVATE_KEY_PATH` and `VITE_BG_CERTIFICATE_PATH` in `.env` |
194+
| "Failed to load certificate files" in logs | File paths are wrong or files are unreadable | Check that the PEM files exist at the specified paths and have read permissions |
195+
| Bank returns 401/403 on signed requests | Key ID mismatch or expired certificate | Verify `VITE_BG_KEY_ID` matches what the bank expects; check certificate validity |
196+
| Berlin Group requests not being signed | Path doesn't contain `/berlin-group/` | Ensure the API path follows the pattern `/berlin-group/{version}/...` |
197+
| Consent endpoints fail without Consent-ID | Frontend not sending the header | Frontend must include `X-BG-Consent-ID` header for account data endpoints |

server/app.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { Container } from 'typedi'
3636
import path from 'path'
3737
import { execSync } from 'child_process'
3838
import { OAuth2ProviderManager } from './services/OAuth2ProviderManager.js'
39+
import { BerlinGroupSignatureService } from './services/BerlinGroupSignatureService.js'
3940
import { fileURLToPath } from 'url'
4041
import { dirname } from 'path'
4142

@@ -169,6 +170,18 @@ let instance: any
169170
}
170171
console.log(`-----------------------------------------------------------------`)
171172

173+
// Berlin Group TPP Signature Certificate Setup
174+
console.log('--- Berlin Group TPP Signature Certificate -----------------------')
175+
const bgService = Container.get(BerlinGroupSignatureService)
176+
if (bgService.isEnabled()) {
177+
console.log('OK Berlin Group TPP Signature Certificate is configured and loaded')
178+
console.log(` API Version: ${bgService.getApiVersion()}`)
179+
} else {
180+
console.log('Berlin Group TPP Signature Certificate is NOT configured')
181+
console.log(' Set VITE_BG_PRIVATE_KEY_PATH and VITE_BG_CERTIFICATE_PATH to enable')
182+
}
183+
console.log(`-----------------------------------------------------------------`)
184+
172185
const routePrefix = '/api'
173186

174187
// Register all routes (plain Express)

server/routes/obp.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ router.get('/get', async (req: Request, res: Response) => {
5454
const path = req.query.path as string
5555
const session = req.session as any
5656

57-
const oauthConfig = session.clientConfig
57+
const oauthConfig = session.clientConfig || {}
58+
const bgConsentId = req.headers['x-bg-consent-id'] as string | undefined
59+
if (bgConsentId) {
60+
oauthConfig.berlinGroup = { consentId: bgConsentId }
61+
}
5862

5963
const result = await obpClientService.get(path, oauthConfig)
6064
res.json(result)
@@ -85,7 +89,11 @@ router.post('/create', async (req: Request, res: Response) => {
8589
const data = req.body
8690
const session = req.session as any
8791

88-
const oauthConfig = session.clientConfig
92+
const oauthConfig = session.clientConfig || {}
93+
const bgConsentId = req.headers['x-bg-consent-id'] as string | undefined
94+
if (bgConsentId) {
95+
oauthConfig.berlinGroup = { consentId: bgConsentId }
96+
}
8997

9098
// Debug logging to diagnose authentication issues
9199
console.log('OBP.create - Debug Info:')
@@ -95,6 +103,7 @@ router.post('/create', async (req: Request, res: Response) => {
95103
console.log(' oauth2 exists:', oauthConfig?.oauth2 ? 'YES' : 'NO')
96104
console.log(' accessToken exists:', oauthConfig?.oauth2?.accessToken ? 'YES' : 'NO')
97105
console.log(' oauth2_user exists:', session?.oauth2_user ? 'YES' : 'NO')
106+
console.log(' berlinGroup consentId:', bgConsentId || 'N/A')
98107

99108
const result = await obpClientService.create(path, data, oauthConfig)
100109
res.json(result)
@@ -120,7 +129,11 @@ router.put('/update', async (req: Request, res: Response) => {
120129
const data = req.body
121130
const session = req.session as any
122131

123-
const oauthConfig = session.clientConfig
132+
const oauthConfig = session.clientConfig || {}
133+
const bgConsentId = req.headers['x-bg-consent-id'] as string | undefined
134+
if (bgConsentId) {
135+
oauthConfig.berlinGroup = { consentId: bgConsentId }
136+
}
124137

125138
const result = await obpClientService.update(path, data, oauthConfig)
126139
res.json(result)
@@ -144,7 +157,11 @@ router.delete('/delete', async (req: Request, res: Response) => {
144157
const path = req.query.path as string
145158
const session = req.session as any
146159

147-
const oauthConfig = session.clientConfig
160+
const oauthConfig = session.clientConfig || {}
161+
const bgConsentId = req.headers['x-bg-consent-id'] as string | undefined
162+
if (bgConsentId) {
163+
oauthConfig.berlinGroup = { consentId: bgConsentId }
164+
}
148165

149166
const result = await obpClientService.discard(path, oauthConfig)
150167
res.json(result)

0 commit comments

Comments
 (0)