Flink plugin for Swedish BankID authentication and document signing
npm install @flink-app/bankid-pluginA 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.
- BankID authentication (Mobile BankID and QR code)
- Document signing with BankID
- Automatic QR code generation and refresh
- Session management with MongoDB
- Test and production environments
- Built-in HTTP endpoints (optional)
- TypeScript support with full type safety
``bash`
npm install @flink-app/bankid-plugin
You need a BankID certificate (PFX file) from your bank or the Swedish BankID organization:
- Test Environment: Use test certificates from BankID Test Environment
- Production Environment: Request a production certificate from your bank
The plugin requires MongoDB to store BankID sessions.
Convert your PFX certificate to base64:
`bash`Convert PFX to base64
cat certificate.pfx | base64 > certificate.txt
Store the base64 string in your environment variables:
`bash`
BANKID_CERT_BASE64="MIIKZgIBAzCCCiw..."
BANKID_PASSPHRASE="your-certificate-passphrase"
index.ts:
`typescript
import { FlinkApp } from "@flink-app/flink";
import { bankIdPlugin } from "@flink-app/bankid-plugin";
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
import { genericAuthPlugin, User } from "@flink-app/generic-auth-plugin";
import { Ctx } from "./Ctx";
function start() {
const app = new FlinkApp
name: "My App",
auth: jwtAuthPlugin({
secret: process.env.JWT_SECRET!,
getUser: async (tokenData) => {
const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
if (!user) throw new Error("User not found");
return {
id: user._id,
username: user.username,
roles: user.roles,
};
},
rolePermissions: {
user: ["read", "write"],
},
}),
db: {
uri: process.env.MONGODB_URI!,
},
plugins: [
genericAuthPlugin({
repoName: "userRepo",
}),
bankIdPlugin({
pfxBase64: process.env.BANKID_CERT_BASE64!,
passphrase: process.env.BANKID_PASSPHRASE!,
production: false, // Set to true for production
onGetEndUserIp: async (req) => {
// Get IP from X-Forwarded-For header if behind proxy
const forwardedFor = req.headers["x-forwarded-for"];
if (forwardedFor) {
return Array.isArray(forwardedFor)
? forwardedFor[0]
: forwardedFor.split(",")[0];
}
return req.ip || "127.0.0.1";
},
onAuthSuccess: async (userData, ip, payload) => {
// Find or create user based on personal number
let user = await app.ctx.repos.userRepo.findOne({
personalNumber: userData.user.personalNumber,
});
if (!user) {
// Create new user
const createResult = await app.ctx.plugins.genericAuthPlugin.createUser(
app.ctx.repos.userRepo,
app.ctx.auth,
userData.user.personalNumber,
"", // No password for BankID users
"bankid",
["user"],
{
name: userData.user.name,
givenName: userData.user.givenName,
surname: userData.user.surname,
},
undefined,
undefined,
userData.user.personalNumber
);
if (createResult.status !== "success") {
throw new Error("Failed to create user");
}
user = await app.ctx.repos.userRepo.findById(createResult.user!._id);
}
// Create JWT token
const token = await app.ctx.auth.createToken(
{ userId: user!._id, username: user!.username },
user!.roles
);
return {
user: {
_id: user!._id,
username: user!.username,
profile: user!.profile,
roles: user!.roles,
},
token,
};
},
onSignSuccess: async (userData, signature, payload) => {
// Handle signed document
console.log("Document signed by:", userData.user.name);
console.log("Signature:", signature.signature);
// Store signature in database or process document
// Implementation depends on your use case
},
}),
],
});
app.start();
}
start();
`
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| pfxBase64 | string | Yes | - | BankID certificate in base64 format |passphrase
| | string | Yes | - | Certificate passphrase |production
| | boolean | No | false | Use production BankID environment |allowNoIp
| | boolean | No | false | Allow requests without IP (uses 127.0.0.1) |onGetEndUserIp
| | Function | Yes | - | Function to extract client IP address |onAuthSuccess
| | Function | Yes | - | Callback when authentication succeeds |onSignSuccess
| | Function | No | - | Callback when document signing succeeds |keepSessionsSec
| | number | No | 86400 (24h) | How long to keep sessions in database |bankIdSessionsCollectionName
| | string | No | "bankid_sessions" | MongoDB collection name for sessions |registerRoutes
| | boolean | No | true | Register built-in HTTP endpoints |
#### onGetEndUserIp(req: FlinkRequest): Promise
Extract the end user's IP address from the request. Required by BankID for security.
`typescript
onGetEndUserIp: async (req) => {
// Behind a proxy
const forwardedFor = req.headers["x-forwarded-for"];
if (forwardedFor) {
return Array.isArray(forwardedFor)
? forwardedFor[0]
: forwardedFor.split(",")[0];
}
// Direct connection
return req.ip || "127.0.0.1";
}
`
#### onAuthSuccess(userData, ip?, payload?): Promise
Called when BankID authentication is successful. Must return user object and JWT token.
`typescript`
interface AuthSuccessCallbackResponse {
user: any;
token: string;
}
Parameters:
- userData: BankID user data including personal number and nameip
- : Client IP address (optional)payload
- : Custom payload passed during auth initiation (optional)
#### onSignSuccess(userData, signature, payload?): Promise
Called when document signing is successful.
Parameters:
- userData: BankID user datasignature
- : Signature data including signature string and OCSP responsepayload
- : Custom payload passed during sign initiation (optional)
The plugin exposes the following functions via ctx.plugins.bankId:
Initiate BankID authentication.
`typescript`
const result = await ctx.plugins.bankId.auth({
endUserIp: "192.168.1.1",
payload: { returnUrl: "/dashboard" }, // Optional custom data
});
Returns:
`typescript`
interface AuthResponse {
orderRef: string; // BankID order reference
autoStartToken: string; // Token for Mobile BankID app
qr: string; // QR code data URL for QR code authentication
}
Initiate document signing.
`typescript`
const result = await ctx.plugins.bankId.sign({
endUserIp: "192.168.1.1",
userVisibleData: "I approve this document",
userNonVisibleData: "Document ID: 12345", // Optional
payload: { documentId: "12345" },
});
Returns:
`typescript`
interface SignResponse {
orderRef: string;
autoStartToken: string;
qr: string;
}
Check authentication status and get result when complete.
`typescript`
const result = await ctx.plugins.bankId.getAuthStatus({
orderRef: "abc123...",
});
Returns:
`typescript`
interface AuthStatusResponse {
status: "pending" | "complete" | "failed";
hintCode?: string;
qr?: string; // Updated QR code if still pending
user?: any; // User data when complete
token?: string; // JWT token when complete
}
Check signing status and get result when complete.
`typescript`
const result = await ctx.plugins.bankId.getSignStatus({
orderRef: "abc123...",
});
Cancel an ongoing BankID session.
`typescript`
const result = await ctx.plugins.bankId.cancelSession({
orderRef: "abc123...",
});
When registerRoutes: true (default), the following endpoints are automatically available:
Initiate BankID authentication.
Request:
`json`
{
"payload": {
"returnUrl": "/dashboard"
}
}
Response:
`json`
{
"data": {
"orderRef": "131daac9-16c6-4618-beb0-365768f37288",
"autoStartToken": "7c40b5c9-fa74-49cf-b98c-bfaf...",
"qr": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}
}
Get authentication status.
Query Parameters:
- orderRef: The order reference from initiation
Response (Pending):
`json`
{
"data": {
"status": "pending",
"hintCode": "outstandingTransaction",
"qr": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}
}
Response (Complete):
`json`
{
"data": {
"status": "complete",
"user": {
"_id": "507f1f77bcf86cd799439011",
"username": "198001011234",
"profile": {
"name": "John Doe",
"givenName": "John",
"surname": "Doe"
},
"roles": ["user"]
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
Response (Failed):
`json`
{
"data": {
"status": "failed",
"hintCode": "userCancel"
}
}
Get signing status. Similar response structure to auth status.
Cancel a BankID session.
Response:
`json`
{
"data": {
"status": "success"
}
}
- outstandingTransaction - User hasn't opened BankID app yetnoClient
- - BankID app not installedstarted
- - BankID app has been starteduserSign
- - User is signing in BankID app
- userCancel - User cancelled the operationexpiredTransaction
- - Session timed outcertificateErr
- - Certificate errorstartFailed
- - Failed to start BankID app
`typescript
import React, { useState, useEffect } from "react";
function BankIDLogin() {
const [orderRef, setOrderRef] = useState
const [qrCode, setQrCode] = useState
const [status, setStatus] = useState
const initAuth = async () => {
const response = await fetch("/bankid/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
payload: { returnUrl: "/dashboard" },
}),
});
const data = await response.json();
setOrderRef(data.data.orderRef);
setQrCode(data.data.qr);
setStatus("pending");
};
useEffect(() => {
if (!orderRef) return;
const interval = setInterval(async () => {
const response = await fetch(/bankid/auth?orderRef=${orderRef});
const data = await response.json();
if (data.data.status === "complete") {
clearInterval(interval);
setStatus("complete");
// Store token and redirect
localStorage.setItem("token", data.data.token);
window.location.href = "/dashboard";
} else if (data.data.status === "failed") {
clearInterval(interval);
setStatus("failed");
} else {
// Update QR code if provided
if (data.data.qr) {
setQrCode(data.data.qr);
}
}
}, 2000); // Poll every 2 seconds
return () => clearInterval(interval);
}, [orderRef]);
return (
{status === "pending" && (
Scan QR code with BankID app:
Or open BankID app on this device
{status === "failed" && (
Authentication failed. Please try again.
$3
For mobile devices, use the
autoStartToken to open the BankID app:`typescript
const response = await fetch("/bankid/auth", { method: "POST" });
const data = await response.json();// iOS and Android
const bankIdUrl =
bankid:///?autostarttoken=${data.data.autoStartToken}&redirect=null;
window.location.href = bankIdUrl;// Then poll for status
`Document Signing
$3
`typescript
// In your handler
const handler: Handler = async ({ ctx, req }) => {
const documentText = "I agree to the terms and conditions..."; const result = await ctx.plugins.bankId.sign({
endUserIp: await getClientIp(req),
userVisibleData: documentText,
userNonVisibleData:
Document ID: ${documentId},
payload: {
documentId,
userId: req.user.id,
},
}); return { data: result };
};
`$3
In your
onSignSuccess callback:`typescript
onSignSuccess: async (userData, signature, payload) => {
const { documentId, userId } = payload; // Store signature
await ctx.repos.signatureRepo.create({
documentId,
userId,
personalNumber: userData.user.personalNumber,
name: userData.user.name,
signature: signature.signature,
ocspResponse: signature.ocspResponse,
signedAt: new Date(),
});
// Update document status
await ctx.repos.documentRepo.update(documentId, {
status: "signed",
signedBy: userData.user.personalNumber,
});
}
`Session Management
The plugin automatically manages BankID sessions in MongoDB:
- Sessions are stored with automatic expiration (default: 24 hours)
- QR codes are automatically regenerated every second while pending
- Failed or completed sessions are cleaned up automatically
$3
`typescript
bankIdPlugin({
// ...other options
keepSessionsSec: 3600, // Keep sessions for 1 hour
bankIdSessionsCollectionName: "my_bankid_sessions",
})
`Testing
$3
Use BankID test certificates and test personal numbers:
`typescript
bankIdPlugin({
production: false, // Test environment
pfxBase64: process.env.BANKID_TEST_CERT_BASE64!,
passphrase: process.env.BANKID_TEST_PASSPHRASE!,
// ...
})
`$3
BankID provides test personal numbers for development:
-
198001011234 - Standard test user
- 198001021234 - Test user with multiple BankIDsSee BankID Test Documentation for complete list.
Security Best Practices
$3
- Never commit certificates to version control
- Store certificates in secure environment variables or secrets management
- Use different certificates for test and production
`bash
.env
BANKID_CERT_BASE64=xxx
BANKID_PASSPHRASE=yyy
`$3
Always verify and log IP addresses:
`typescript
onGetEndUserIp: async (req) => {
const ip = extractIp(req); // Log for audit
logger.info(
BankID request from IP: ${ip}); // Validate IP format
if (!isValidIp(ip)) {
throw new Error("Invalid IP address");
}
return ip;
}
`$3
Implement rate limiting on BankID endpoints to prevent abuse:
`typescript
import rateLimit from "express-rate-limit";app.use("/bankid", rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 10, // 10 requests per window
}));
`$3
Validate session state before completing authentication:
`typescript
onAuthSuccess: async (userData, ip, payload) => {
// Verify personal number format
if (!/^\d{12}$/.test(userData.user.personalNumber)) {
throw new Error("Invalid personal number");
} // Additional validation...
return { user, token };
}
`$3
BankID requires HTTPS in production. Never use HTTP for BankID in production.
TypeScript Types
`typescript
import {
BankIdPluginOptions,
BankIdUserInfo,
BankIdSignature,
AuthSuccessCallbackResponse,
AuthResponse,
SignResponse,
AuthStatusResponse,
SignStatusResponse,
BankIdSession,
} from "@flink-app/bankid-plugin";// User info from BankID
interface BankIdUserInfo {
personalNumber: string;
name: string;
givenName: string;
surname: string;
}
// Signature data
interface BankIdSignature {
signature: string;
ocspResponse: string;
}
// Auth success response
interface AuthSuccessCallbackResponse {
user: any;
token: string;
}
`Troubleshooting
$3
Issue: QR code becomes stale after 1 second
Solution: The plugin automatically generates new QR codes. Ensure you're polling the status endpoint regularly (every 1-2 seconds).
$3
Issue:
Error: unable to get local issuer certificateSolution:
- Verify certificate format is correct
- Check passphrase is correct
- Ensure certificate is valid for the environment (test/prod)
$3
Issue:
Failed to obtain endUserIpSolution:
`typescript
// For development only
bankIdPlugin({
allowNoIp: true, // Only for local development
onGetEndUserIp: async (req) => {
return req.ip || "127.0.0.1";
},
})
`$3
Issue: Users frequently cancel
Solution: Improve UX:
- Show clear instructions
- Display QR code prominently
- Provide mobile deep link option
- Show timeout countdown
Production Checklist
- [ ] Use production BankID certificate
- [ ] Set
production: true`See the configuration example at the beginning of this document for a complete working example integrating BankID with the Generic Auth Plugin.
MIT