@flink-app/bankid-plugin 0.12.1-alpha.25 → 0.12.1-alpha.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +777 -0
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
# BankID Plugin
|
|
2
|
+
|
|
3
|
+
A Flink plugin for Swedish BankID authentication and document signing. This plugin provides seamless integration with the Swedish BankID service for secure authentication and electronic signatures.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- BankID authentication (Mobile BankID and QR code)
|
|
8
|
+
- Document signing with BankID
|
|
9
|
+
- Automatic QR code generation and refresh
|
|
10
|
+
- Session management with MongoDB
|
|
11
|
+
- Test and production environments
|
|
12
|
+
- Built-in HTTP endpoints (optional)
|
|
13
|
+
- TypeScript support with full type safety
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @flink-app/bankid-plugin
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
### 1. BankID Certificate
|
|
24
|
+
|
|
25
|
+
You need a BankID certificate (PFX file) from your bank or the Swedish BankID organization:
|
|
26
|
+
|
|
27
|
+
- **Test Environment**: Use test certificates from [BankID Test Environment](https://www.bankid.com/utvecklare/test)
|
|
28
|
+
- **Production Environment**: Request a production certificate from your bank
|
|
29
|
+
|
|
30
|
+
### 2. MongoDB Connection
|
|
31
|
+
|
|
32
|
+
The plugin requires MongoDB to store BankID sessions.
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
### Step 1: Prepare Certificate
|
|
37
|
+
|
|
38
|
+
Convert your PFX certificate to base64:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Convert PFX to base64
|
|
42
|
+
cat certificate.pfx | base64 > certificate.txt
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Store the base64 string in your environment variables:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
BANKID_CERT_BASE64="MIIKZgIBAzCCCiw..."
|
|
49
|
+
BANKID_PASSPHRASE="your-certificate-passphrase"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Step 2: Configure Plugin
|
|
53
|
+
|
|
54
|
+
**`index.ts`:**
|
|
55
|
+
```typescript
|
|
56
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
57
|
+
import { bankIdPlugin } from "@flink-app/bankid-plugin";
|
|
58
|
+
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
|
|
59
|
+
import { genericAuthPlugin, User } from "@flink-app/generic-auth-plugin";
|
|
60
|
+
import { Ctx } from "./Ctx";
|
|
61
|
+
|
|
62
|
+
function start() {
|
|
63
|
+
const app = new FlinkApp<Ctx>({
|
|
64
|
+
name: "My App",
|
|
65
|
+
auth: jwtAuthPlugin({
|
|
66
|
+
secret: process.env.JWT_SECRET!,
|
|
67
|
+
getUser: async (tokenData) => {
|
|
68
|
+
const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
|
|
69
|
+
if (!user) throw new Error("User not found");
|
|
70
|
+
return {
|
|
71
|
+
id: user._id,
|
|
72
|
+
username: user.username,
|
|
73
|
+
roles: user.roles,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
rolePermissions: {
|
|
77
|
+
user: ["read", "write"],
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
db: {
|
|
81
|
+
uri: process.env.MONGODB_URI!,
|
|
82
|
+
},
|
|
83
|
+
plugins: [
|
|
84
|
+
genericAuthPlugin({
|
|
85
|
+
repoName: "userRepo",
|
|
86
|
+
}),
|
|
87
|
+
bankIdPlugin({
|
|
88
|
+
pfxBase64: process.env.BANKID_CERT_BASE64!,
|
|
89
|
+
passphrase: process.env.BANKID_PASSPHRASE!,
|
|
90
|
+
production: false, // Set to true for production
|
|
91
|
+
onGetEndUserIp: async (req) => {
|
|
92
|
+
// Get IP from X-Forwarded-For header if behind proxy
|
|
93
|
+
const forwardedFor = req.headers["x-forwarded-for"];
|
|
94
|
+
if (forwardedFor) {
|
|
95
|
+
return Array.isArray(forwardedFor)
|
|
96
|
+
? forwardedFor[0]
|
|
97
|
+
: forwardedFor.split(",")[0];
|
|
98
|
+
}
|
|
99
|
+
return req.ip || "127.0.0.1";
|
|
100
|
+
},
|
|
101
|
+
onAuthSuccess: async (userData, ip, payload) => {
|
|
102
|
+
// Find or create user based on personal number
|
|
103
|
+
let user = await app.ctx.repos.userRepo.findOne({
|
|
104
|
+
personalNumber: userData.user.personalNumber,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!user) {
|
|
108
|
+
// Create new user
|
|
109
|
+
const createResult = await app.ctx.plugins.genericAuthPlugin.createUser(
|
|
110
|
+
app.ctx.repos.userRepo,
|
|
111
|
+
app.ctx.auth,
|
|
112
|
+
userData.user.personalNumber,
|
|
113
|
+
"", // No password for BankID users
|
|
114
|
+
"bankid",
|
|
115
|
+
["user"],
|
|
116
|
+
{
|
|
117
|
+
name: userData.user.name,
|
|
118
|
+
givenName: userData.user.givenName,
|
|
119
|
+
surname: userData.user.surname,
|
|
120
|
+
},
|
|
121
|
+
undefined,
|
|
122
|
+
undefined,
|
|
123
|
+
userData.user.personalNumber
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (createResult.status !== "success") {
|
|
127
|
+
throw new Error("Failed to create user");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
user = await app.ctx.repos.userRepo.findById(createResult.user!._id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Create JWT token
|
|
134
|
+
const token = await app.ctx.auth.createToken(
|
|
135
|
+
{ userId: user!._id, username: user!.username },
|
|
136
|
+
user!.roles
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
user: {
|
|
141
|
+
_id: user!._id,
|
|
142
|
+
username: user!.username,
|
|
143
|
+
profile: user!.profile,
|
|
144
|
+
roles: user!.roles,
|
|
145
|
+
},
|
|
146
|
+
token,
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
onSignSuccess: async (userData, signature, payload) => {
|
|
150
|
+
// Handle signed document
|
|
151
|
+
console.log("Document signed by:", userData.user.name);
|
|
152
|
+
console.log("Signature:", signature.signature);
|
|
153
|
+
|
|
154
|
+
// Store signature in database or process document
|
|
155
|
+
// Implementation depends on your use case
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
app.start();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
start();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Configuration Options
|
|
168
|
+
|
|
169
|
+
### `BankIdPluginOptions`
|
|
170
|
+
|
|
171
|
+
| Option | Type | Required | Default | Description |
|
|
172
|
+
|--------|------|----------|---------|-------------|
|
|
173
|
+
| `pfxBase64` | `string` | Yes | - | BankID certificate in base64 format |
|
|
174
|
+
| `passphrase` | `string` | Yes | - | Certificate passphrase |
|
|
175
|
+
| `production` | `boolean` | No | `false` | Use production BankID environment |
|
|
176
|
+
| `allowNoIp` | `boolean` | No | `false` | Allow requests without IP (uses 127.0.0.1) |
|
|
177
|
+
| `onGetEndUserIp` | `Function` | Yes | - | Function to extract client IP address |
|
|
178
|
+
| `onAuthSuccess` | `Function` | Yes | - | Callback when authentication succeeds |
|
|
179
|
+
| `onSignSuccess` | `Function` | No | - | Callback when document signing succeeds |
|
|
180
|
+
| `keepSessionsSec` | `number` | No | `86400` (24h) | How long to keep sessions in database |
|
|
181
|
+
| `bankIdSessionsCollectionName` | `string` | No | `"bankid_sessions"` | MongoDB collection name for sessions |
|
|
182
|
+
| `registerRoutes` | `boolean` | No | `true` | Register built-in HTTP endpoints |
|
|
183
|
+
|
|
184
|
+
### Callback Functions
|
|
185
|
+
|
|
186
|
+
#### `onGetEndUserIp(req: FlinkRequest): Promise<string>`
|
|
187
|
+
|
|
188
|
+
Extract the end user's IP address from the request. Required by BankID for security.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
onGetEndUserIp: async (req) => {
|
|
192
|
+
// Behind a proxy
|
|
193
|
+
const forwardedFor = req.headers["x-forwarded-for"];
|
|
194
|
+
if (forwardedFor) {
|
|
195
|
+
return Array.isArray(forwardedFor)
|
|
196
|
+
? forwardedFor[0]
|
|
197
|
+
: forwardedFor.split(",")[0];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Direct connection
|
|
201
|
+
return req.ip || "127.0.0.1";
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### `onAuthSuccess(userData, ip?, payload?): Promise<AuthSuccessCallbackResponse>`
|
|
206
|
+
|
|
207
|
+
Called when BankID authentication is successful. Must return user object and JWT token.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
interface AuthSuccessCallbackResponse {
|
|
211
|
+
user: any;
|
|
212
|
+
token: string;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Parameters:**
|
|
217
|
+
- `userData`: BankID user data including personal number and name
|
|
218
|
+
- `ip`: Client IP address (optional)
|
|
219
|
+
- `payload`: Custom payload passed during auth initiation (optional)
|
|
220
|
+
|
|
221
|
+
#### `onSignSuccess(userData, signature, payload?): Promise<void>`
|
|
222
|
+
|
|
223
|
+
Called when document signing is successful.
|
|
224
|
+
|
|
225
|
+
**Parameters:**
|
|
226
|
+
- `userData`: BankID user data
|
|
227
|
+
- `signature`: Signature data including signature string and OCSP response
|
|
228
|
+
- `payload`: Custom payload passed during sign initiation (optional)
|
|
229
|
+
|
|
230
|
+
## Context API
|
|
231
|
+
|
|
232
|
+
The plugin exposes the following functions via `ctx.plugins.bankId`:
|
|
233
|
+
|
|
234
|
+
### `auth(options?): Promise<AuthResponse>`
|
|
235
|
+
|
|
236
|
+
Initiate BankID authentication.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const result = await ctx.plugins.bankId.auth({
|
|
240
|
+
endUserIp: "192.168.1.1",
|
|
241
|
+
payload: { returnUrl: "/dashboard" }, // Optional custom data
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Returns:**
|
|
246
|
+
```typescript
|
|
247
|
+
interface AuthResponse {
|
|
248
|
+
orderRef: string; // BankID order reference
|
|
249
|
+
autoStartToken: string; // Token for Mobile BankID app
|
|
250
|
+
qr: string; // QR code data URL for QR code authentication
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `sign(options): Promise<SignResponse>`
|
|
255
|
+
|
|
256
|
+
Initiate document signing.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
const result = await ctx.plugins.bankId.sign({
|
|
260
|
+
endUserIp: "192.168.1.1",
|
|
261
|
+
userVisibleData: "I approve this document",
|
|
262
|
+
userNonVisibleData: "Document ID: 12345", // Optional
|
|
263
|
+
payload: { documentId: "12345" },
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Returns:**
|
|
268
|
+
```typescript
|
|
269
|
+
interface SignResponse {
|
|
270
|
+
orderRef: string;
|
|
271
|
+
autoStartToken: string;
|
|
272
|
+
qr: string;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### `getAuthStatus(options): Promise<AuthStatusResponse>`
|
|
277
|
+
|
|
278
|
+
Check authentication status and get result when complete.
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
const result = await ctx.plugins.bankId.getAuthStatus({
|
|
282
|
+
orderRef: "abc123...",
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Returns:**
|
|
287
|
+
```typescript
|
|
288
|
+
interface AuthStatusResponse {
|
|
289
|
+
status: "pending" | "complete" | "failed";
|
|
290
|
+
hintCode?: string;
|
|
291
|
+
qr?: string; // Updated QR code if still pending
|
|
292
|
+
user?: any; // User data when complete
|
|
293
|
+
token?: string; // JWT token when complete
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### `getSignStatus(options): Promise<SignStatusResponse>`
|
|
298
|
+
|
|
299
|
+
Check signing status and get result when complete.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const result = await ctx.plugins.bankId.getSignStatus({
|
|
303
|
+
orderRef: "abc123...",
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### `cancelSession(options): Promise<CancelSessionResponse>`
|
|
308
|
+
|
|
309
|
+
Cancel an ongoing BankID session.
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const result = await ctx.plugins.bankId.cancelSession({
|
|
313
|
+
orderRef: "abc123...",
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Built-in API Endpoints
|
|
318
|
+
|
|
319
|
+
When `registerRoutes: true` (default), the following endpoints are automatically available:
|
|
320
|
+
|
|
321
|
+
### POST /bankid/auth
|
|
322
|
+
|
|
323
|
+
Initiate BankID authentication.
|
|
324
|
+
|
|
325
|
+
**Request:**
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"payload": {
|
|
329
|
+
"returnUrl": "/dashboard"
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Response:**
|
|
335
|
+
```json
|
|
336
|
+
{
|
|
337
|
+
"data": {
|
|
338
|
+
"orderRef": "131daac9-16c6-4618-beb0-365768f37288",
|
|
339
|
+
"autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfaf...",
|
|
340
|
+
"qr": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### GET /bankid/auth?orderRef=xxx
|
|
346
|
+
|
|
347
|
+
Get authentication status.
|
|
348
|
+
|
|
349
|
+
**Query Parameters:**
|
|
350
|
+
- `orderRef`: The order reference from initiation
|
|
351
|
+
|
|
352
|
+
**Response (Pending):**
|
|
353
|
+
```json
|
|
354
|
+
{
|
|
355
|
+
"data": {
|
|
356
|
+
"status": "pending",
|
|
357
|
+
"hintCode": "outstandingTransaction",
|
|
358
|
+
"qr": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Response (Complete):**
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"data": {
|
|
367
|
+
"status": "complete",
|
|
368
|
+
"user": {
|
|
369
|
+
"_id": "507f1f77bcf86cd799439011",
|
|
370
|
+
"username": "198001011234",
|
|
371
|
+
"profile": {
|
|
372
|
+
"name": "John Doe",
|
|
373
|
+
"givenName": "John",
|
|
374
|
+
"surname": "Doe"
|
|
375
|
+
},
|
|
376
|
+
"roles": ["user"]
|
|
377
|
+
},
|
|
378
|
+
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Response (Failed):**
|
|
384
|
+
```json
|
|
385
|
+
{
|
|
386
|
+
"data": {
|
|
387
|
+
"status": "failed",
|
|
388
|
+
"hintCode": "userCancel"
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### GET /bankid/sign?orderRef=xxx
|
|
394
|
+
|
|
395
|
+
Get signing status. Similar response structure to auth status.
|
|
396
|
+
|
|
397
|
+
### DELETE /bankid/session/:orderRef
|
|
398
|
+
|
|
399
|
+
Cancel a BankID session.
|
|
400
|
+
|
|
401
|
+
**Response:**
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"data": {
|
|
405
|
+
"status": "success"
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## BankID Status Codes
|
|
411
|
+
|
|
412
|
+
### Hint Codes (Pending)
|
|
413
|
+
|
|
414
|
+
- `outstandingTransaction` - User hasn't opened BankID app yet
|
|
415
|
+
- `noClient` - BankID app not installed
|
|
416
|
+
- `started` - BankID app has been started
|
|
417
|
+
- `userSign` - User is signing in BankID app
|
|
418
|
+
|
|
419
|
+
### Hint Codes (Failed)
|
|
420
|
+
|
|
421
|
+
- `userCancel` - User cancelled the operation
|
|
422
|
+
- `expiredTransaction` - Session timed out
|
|
423
|
+
- `certificateErr` - Certificate error
|
|
424
|
+
- `startFailed` - Failed to start BankID app
|
|
425
|
+
|
|
426
|
+
## Client Integration Examples
|
|
427
|
+
|
|
428
|
+
### React Example with QR Code
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import React, { useState, useEffect } from "react";
|
|
432
|
+
|
|
433
|
+
function BankIDLogin() {
|
|
434
|
+
const [orderRef, setOrderRef] = useState<string>();
|
|
435
|
+
const [qrCode, setQrCode] = useState<string>();
|
|
436
|
+
const [status, setStatus] = useState<string>("idle");
|
|
437
|
+
|
|
438
|
+
const initAuth = async () => {
|
|
439
|
+
const response = await fetch("/bankid/auth", {
|
|
440
|
+
method: "POST",
|
|
441
|
+
headers: { "Content-Type": "application/json" },
|
|
442
|
+
body: JSON.stringify({
|
|
443
|
+
payload: { returnUrl: "/dashboard" },
|
|
444
|
+
}),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const data = await response.json();
|
|
448
|
+
setOrderRef(data.data.orderRef);
|
|
449
|
+
setQrCode(data.data.qr);
|
|
450
|
+
setStatus("pending");
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (!orderRef) return;
|
|
455
|
+
|
|
456
|
+
const interval = setInterval(async () => {
|
|
457
|
+
const response = await fetch(`/bankid/auth?orderRef=${orderRef}`);
|
|
458
|
+
const data = await response.json();
|
|
459
|
+
|
|
460
|
+
if (data.data.status === "complete") {
|
|
461
|
+
clearInterval(interval);
|
|
462
|
+
setStatus("complete");
|
|
463
|
+
// Store token and redirect
|
|
464
|
+
localStorage.setItem("token", data.data.token);
|
|
465
|
+
window.location.href = "/dashboard";
|
|
466
|
+
} else if (data.data.status === "failed") {
|
|
467
|
+
clearInterval(interval);
|
|
468
|
+
setStatus("failed");
|
|
469
|
+
} else {
|
|
470
|
+
// Update QR code if provided
|
|
471
|
+
if (data.data.qr) {
|
|
472
|
+
setQrCode(data.data.qr);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}, 2000); // Poll every 2 seconds
|
|
476
|
+
|
|
477
|
+
return () => clearInterval(interval);
|
|
478
|
+
}, [orderRef]);
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
<div>
|
|
482
|
+
{status === "idle" && (
|
|
483
|
+
<button onClick={initAuth}>Login with BankID</button>
|
|
484
|
+
)}
|
|
485
|
+
|
|
486
|
+
{status === "pending" && (
|
|
487
|
+
<div>
|
|
488
|
+
<p>Scan QR code with BankID app:</p>
|
|
489
|
+
{qrCode && <img src={qrCode} alt="BankID QR Code" />}
|
|
490
|
+
<p>Or open BankID app on this device</p>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
|
|
494
|
+
{status === "failed" && (
|
|
495
|
+
<div>
|
|
496
|
+
<p>Authentication failed. Please try again.</p>
|
|
497
|
+
<button onClick={initAuth}>Retry</button>
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Mobile BankID Deep Link
|
|
506
|
+
|
|
507
|
+
For mobile devices, use the `autoStartToken` to open the BankID app:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
const response = await fetch("/bankid/auth", { method: "POST" });
|
|
511
|
+
const data = await response.json();
|
|
512
|
+
|
|
513
|
+
// iOS and Android
|
|
514
|
+
const bankIdUrl = `bankid:///?autostarttoken=${data.data.autoStartToken}&redirect=null`;
|
|
515
|
+
window.location.href = bankIdUrl;
|
|
516
|
+
|
|
517
|
+
// Then poll for status
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Document Signing
|
|
521
|
+
|
|
522
|
+
### Sign a Document
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
// In your handler
|
|
526
|
+
const handler: Handler<Ctx, SignDocumentReq, SignDocumentRes> = async ({ ctx, req }) => {
|
|
527
|
+
const documentText = "I agree to the terms and conditions...";
|
|
528
|
+
|
|
529
|
+
const result = await ctx.plugins.bankId.sign({
|
|
530
|
+
endUserIp: await getClientIp(req),
|
|
531
|
+
userVisibleData: documentText,
|
|
532
|
+
userNonVisibleData: `Document ID: ${documentId}`,
|
|
533
|
+
payload: {
|
|
534
|
+
documentId,
|
|
535
|
+
userId: req.user.id,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
return { data: result };
|
|
540
|
+
};
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Handle Signed Document
|
|
544
|
+
|
|
545
|
+
In your `onSignSuccess` callback:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
onSignSuccess: async (userData, signature, payload) => {
|
|
549
|
+
const { documentId, userId } = payload;
|
|
550
|
+
|
|
551
|
+
// Store signature
|
|
552
|
+
await ctx.repos.signatureRepo.create({
|
|
553
|
+
documentId,
|
|
554
|
+
userId,
|
|
555
|
+
personalNumber: userData.user.personalNumber,
|
|
556
|
+
name: userData.user.name,
|
|
557
|
+
signature: signature.signature,
|
|
558
|
+
ocspResponse: signature.ocspResponse,
|
|
559
|
+
signedAt: new Date(),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Update document status
|
|
563
|
+
await ctx.repos.documentRepo.update(documentId, {
|
|
564
|
+
status: "signed",
|
|
565
|
+
signedBy: userData.user.personalNumber,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## Session Management
|
|
571
|
+
|
|
572
|
+
The plugin automatically manages BankID sessions in MongoDB:
|
|
573
|
+
|
|
574
|
+
- Sessions are stored with automatic expiration (default: 24 hours)
|
|
575
|
+
- QR codes are automatically regenerated every second while pending
|
|
576
|
+
- Failed or completed sessions are cleaned up automatically
|
|
577
|
+
|
|
578
|
+
### Custom Session Retention
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
bankIdPlugin({
|
|
582
|
+
// ...other options
|
|
583
|
+
keepSessionsSec: 3600, // Keep sessions for 1 hour
|
|
584
|
+
bankIdSessionsCollectionName: "my_bankid_sessions",
|
|
585
|
+
})
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## Testing
|
|
589
|
+
|
|
590
|
+
### Test Environment
|
|
591
|
+
|
|
592
|
+
Use BankID test certificates and test personal numbers:
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
bankIdPlugin({
|
|
596
|
+
production: false, // Test environment
|
|
597
|
+
pfxBase64: process.env.BANKID_TEST_CERT_BASE64!,
|
|
598
|
+
passphrase: process.env.BANKID_TEST_PASSPHRASE!,
|
|
599
|
+
// ...
|
|
600
|
+
})
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Test Personal Numbers
|
|
604
|
+
|
|
605
|
+
BankID provides test personal numbers for development:
|
|
606
|
+
|
|
607
|
+
- `198001011234` - Standard test user
|
|
608
|
+
- `198001021234` - Test user with multiple BankIDs
|
|
609
|
+
|
|
610
|
+
See [BankID Test Documentation](https://www.bankid.com/utvecklare/test) for complete list.
|
|
611
|
+
|
|
612
|
+
## Security Best Practices
|
|
613
|
+
|
|
614
|
+
### 1. Certificate Security
|
|
615
|
+
|
|
616
|
+
- Never commit certificates to version control
|
|
617
|
+
- Store certificates in secure environment variables or secrets management
|
|
618
|
+
- Use different certificates for test and production
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
# .env
|
|
622
|
+
BANKID_CERT_BASE64=xxx
|
|
623
|
+
BANKID_PASSPHRASE=yyy
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### 2. IP Address Validation
|
|
627
|
+
|
|
628
|
+
Always verify and log IP addresses:
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
onGetEndUserIp: async (req) => {
|
|
632
|
+
const ip = extractIp(req);
|
|
633
|
+
|
|
634
|
+
// Log for audit
|
|
635
|
+
logger.info(`BankID request from IP: ${ip}`);
|
|
636
|
+
|
|
637
|
+
// Validate IP format
|
|
638
|
+
if (!isValidIp(ip)) {
|
|
639
|
+
throw new Error("Invalid IP address");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return ip;
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### 3. Rate Limiting
|
|
647
|
+
|
|
648
|
+
Implement rate limiting on BankID endpoints to prevent abuse:
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
import rateLimit from "express-rate-limit";
|
|
652
|
+
|
|
653
|
+
app.use("/bankid", rateLimit({
|
|
654
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
655
|
+
max: 10, // 10 requests per window
|
|
656
|
+
}));
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### 4. Session Validation
|
|
660
|
+
|
|
661
|
+
Validate session state before completing authentication:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
onAuthSuccess: async (userData, ip, payload) => {
|
|
665
|
+
// Verify personal number format
|
|
666
|
+
if (!/^\d{12}$/.test(userData.user.personalNumber)) {
|
|
667
|
+
throw new Error("Invalid personal number");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Additional validation...
|
|
671
|
+
|
|
672
|
+
return { user, token };
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### 5. HTTPS Only
|
|
677
|
+
|
|
678
|
+
BankID requires HTTPS in production. Never use HTTP for BankID in production.
|
|
679
|
+
|
|
680
|
+
## TypeScript Types
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
import {
|
|
684
|
+
BankIdPluginOptions,
|
|
685
|
+
BankIdUserInfo,
|
|
686
|
+
BankIdSignature,
|
|
687
|
+
AuthSuccessCallbackResponse,
|
|
688
|
+
AuthResponse,
|
|
689
|
+
SignResponse,
|
|
690
|
+
AuthStatusResponse,
|
|
691
|
+
SignStatusResponse,
|
|
692
|
+
BankIdSession,
|
|
693
|
+
} from "@flink-app/bankid-plugin";
|
|
694
|
+
|
|
695
|
+
// User info from BankID
|
|
696
|
+
interface BankIdUserInfo {
|
|
697
|
+
personalNumber: string;
|
|
698
|
+
name: string;
|
|
699
|
+
givenName: string;
|
|
700
|
+
surname: string;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Signature data
|
|
704
|
+
interface BankIdSignature {
|
|
705
|
+
signature: string;
|
|
706
|
+
ocspResponse: string;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Auth success response
|
|
710
|
+
interface AuthSuccessCallbackResponse {
|
|
711
|
+
user: any;
|
|
712
|
+
token: string;
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
## Troubleshooting
|
|
717
|
+
|
|
718
|
+
### QR Code Not Updating
|
|
719
|
+
|
|
720
|
+
**Issue:** QR code becomes stale after 1 second
|
|
721
|
+
|
|
722
|
+
**Solution:** The plugin automatically generates new QR codes. Ensure you're polling the status endpoint regularly (every 1-2 seconds).
|
|
723
|
+
|
|
724
|
+
### Certificate Error
|
|
725
|
+
|
|
726
|
+
**Issue:** `Error: unable to get local issuer certificate`
|
|
727
|
+
|
|
728
|
+
**Solution:**
|
|
729
|
+
- Verify certificate format is correct
|
|
730
|
+
- Check passphrase is correct
|
|
731
|
+
- Ensure certificate is valid for the environment (test/prod)
|
|
732
|
+
|
|
733
|
+
### IP Address Issues
|
|
734
|
+
|
|
735
|
+
**Issue:** `Failed to obtain endUserIp`
|
|
736
|
+
|
|
737
|
+
**Solution:**
|
|
738
|
+
```typescript
|
|
739
|
+
// For development only
|
|
740
|
+
bankIdPlugin({
|
|
741
|
+
allowNoIp: true, // Only for local development
|
|
742
|
+
onGetEndUserIp: async (req) => {
|
|
743
|
+
return req.ip || "127.0.0.1";
|
|
744
|
+
},
|
|
745
|
+
})
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
### User Cancel
|
|
749
|
+
|
|
750
|
+
**Issue:** Users frequently cancel
|
|
751
|
+
|
|
752
|
+
**Solution:** Improve UX:
|
|
753
|
+
- Show clear instructions
|
|
754
|
+
- Display QR code prominently
|
|
755
|
+
- Provide mobile deep link option
|
|
756
|
+
- Show timeout countdown
|
|
757
|
+
|
|
758
|
+
## Production Checklist
|
|
759
|
+
|
|
760
|
+
- [ ] Use production BankID certificate
|
|
761
|
+
- [ ] Set `production: true`
|
|
762
|
+
- [ ] Configure HTTPS
|
|
763
|
+
- [ ] Implement rate limiting
|
|
764
|
+
- [ ] Set up monitoring and logging
|
|
765
|
+
- [ ] Configure proper IP extraction behind proxy
|
|
766
|
+
- [ ] Set appropriate session retention
|
|
767
|
+
- [ ] Test certificate expiration handling
|
|
768
|
+
- [ ] Configure error alerting
|
|
769
|
+
- [ ] Validate personal number format
|
|
770
|
+
|
|
771
|
+
## Complete Example
|
|
772
|
+
|
|
773
|
+
See the configuration example at the beginning of this document for a complete working example integrating BankID with the Generic Auth Plugin.
|
|
774
|
+
|
|
775
|
+
## License
|
|
776
|
+
|
|
777
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/bankid-plugin",
|
|
3
|
-
"version": "0.12.1-alpha.
|
|
3
|
+
"version": "0.12.1-alpha.35",
|
|
4
4
|
"description": "Flink plugin for Swedish BankID authentication and document signing",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@types/node": "22.13.10"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@flink-app/flink": "^0.12.1-alpha.
|
|
25
|
+
"@flink-app/flink": "^0.12.1-alpha.35",
|
|
26
26
|
"@types/jasmine": "^3.7.1",
|
|
27
27
|
"@types/node": "22.13.10",
|
|
28
28
|
"jasmine": "^3.7.0",
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"tsc-watch": "^4.2.9",
|
|
33
33
|
"typescript": "5.4.5"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "f8e8c6565a9ca1dd3e5fdb4c2a791c99ae3ba51a"
|
|
36
36
|
}
|