API Reference
View SourceThis section provides comprehensive API documentation for the BankID authentication strategy.
HTTP Endpoints
All BankID authentication endpoints are automatically generated by Ash Authentication based on your configuration. The base path follows the pattern: /{subject_name}/{strategy_name}
Initiate Authentication
Endpoint: POST /{subject_name}/{strategy_name}/initiate
Starts a new BankID authentication order.
Request Headers
Content-Type: application/json
Accept: application/jsonRequest Body
{
"return_url": "https://yourapp.com/auth/callback",
"device_info": {
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"ip_address": "192.168.1.100"
},
"auto_start": false
}Parameters:
return_url(optional, string) - URL to redirect after successful authenticationdevice_info(optional, object) - Device information for security loggingauto_start(optional, boolean) - Set totruefor same-device authentication
Response
Success (200 OK):
{
"status": "pending",
"order_ref": "131daac9-16c6-4333-99c4-2e12fb44897c",
"auto_start_token": "c23b5106-d0f2-4252-8c45-83f63ab0c221",
"qr_start_token": "6b33e2b4-9f4a-4d8b-8c3a-1d9e5f7a2b3c",
"qr_start_secret": "a1b2c3d4e5f6789012345678901234567890abcd",
"expires_at": "2024-01-01T12:05:00Z"
}Error (400 Bad Request):
{
"error": "invalid_request",
"message": "Invalid device info format"
}Error (500 Internal Server Error):
{
"error": "bankid_error",
"message": "Failed to create BankID order",
"details": {
"code": "alreadyInProgress",
"hint_code": "alreadyInProgress"
}
}Poll Authentication Status
Endpoint: GET /{subject_name}/{strategy_name}/poll
Checks the status of an authentication order.
Query Parameters
order_ref(required, string) - The order reference from initiate response
Response
Pending (200 OK):
{
"status": "pending",
"hint_code": "outstandingTransaction",
"expires_at": "2024-01-01T12:05:00Z"
}Order Renewed (200 OK):
{
"status": "pending",
"hint_code": "orderExpired",
"auto_start_token": "new-token-here",
"qr_start_token": "new-qr-token-here",
"expires_at": "2024-01-01T12:06:00Z"
}Complete (200 OK):
{
"status": "complete",
"completion_data": {
"user": {
"personal_number": "199001011234",
"name": "Anna Svensson",
"given_name": "Anna",
"surname": "Svensson"
},
"device": {
"ip_address": "192.168.1.100"
},
"bankid_issue_date": "2024-01-01T12:03:45Z",
"signature": "<base64-encoded-signature>",
"ocsp_response": "<base64-encoded-ocsp-response>"
}
}Failed (200 OK):
{
"status": "failed",
"hint_code": "userCancel"
}Error (400 Bad Request):
{
"error": "invalid_order_ref",
"message": "Invalid or missing order_ref parameter"
}Error (404 Not Found):
{
"error": "order_not_found",
"message": "Authentication order not found"
}Renew Authentication Order
Endpoint: POST /{subject_name}/{strategy_name}/renew
Creates a new order when the current one expires.
Request Headers
Content-Type: application/jsonRequest Body
{
"order_ref": "131daac9-16c6-4333-99c4-2e12fb44897c"
}Response
Same format as the initiate endpoint with a new order reference.
Complete Sign-in
Endpoint: POST /{subject_name}/{strategy_name}
Completes the authentication and creates a user session.
Request Headers
Content-Type: application/jsonRequest Body
{
"order_ref": "131daac9-16c6-4333-99c4-2e12fb44897c",
"completion_data": {
"user": {
"personal_number": "199001011234",
"name": "Anna Svensson",
"given_name": "Anna",
"surname": "Svensson"
},
"device": {
"ip_address": "192.168.1.100"
},
"bankid_issue_date": "2024-01-01T12:03:45Z"
}
}Response
Success (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "def502003232a6c3b8b0b8c6e5d2f9a1...",
"expires_in": 3600,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"personal_number": "199001011234",
"given_name": "Anna",
"surname": "Svensson",
"bankid_verified_at": "2024-01-01T12:03:45Z"
}
}Error (400 Bad Request):
{
"error": "invalid_completion_data",
"message": "Missing or invalid completion data"
}Error (401 Unauthorized):
{
"error": "authentication_failed",
"message": "Authentication could not be completed"
}Error Codes
BankID Hint Codes
| Hint Code | Description | Recommended Action |
|---|---|---|
outstandingTransaction | User has not yet opened BankID app | Continue polling |
noClient | No BankID client found on device | Show "install BankID" message |
started | User has opened BankID app | Continue polling |
userSign | User is confirming identification | Continue polling |
alreadyInProgress | Authentication already in progress | Use existing order |
userCancel | User cancelled authentication | Show "try again" button |
expiredTransaction | Transaction expired | Show "try again" button |
certificateErr | Certificate error | Show generic error message |
unknown | Unknown error occurred | Show generic error message |
API Error Codes
| Error Code | HTTP Status | Description |
|---|---|---|
invalid_request | 400 | Request body is malformed |
invalid_order_ref | 400 | Invalid order reference format |
order_not_found | 404 | Order does not exist or expired |
order_already_consumed | 400 | Order already used for authentication |
order_expired | 400 | Order has expired |
completion_data_missing | 400 | No completion data provided |
completion_data_invalid | 400 | Invalid completion data format |
bankid_error | 500 | BankID API returned error |
internal_error | 500 | Server internal error |
Configuration Options
Strategy Configuration
bank_id do
order_resource MyApp.Accounts.BankIDOrder
personal_number_field :personal_number
given_name_field :given_name
surname_field :surname
verified_at_field :bankid_verified_at
ip_address_field :ip_address
order_ttl 300
order_renewal_interval 28
max_renewals 10
poll_interval 2000
cleanup_interval 300_000
consumed_order_ttl 86_400
endField Mappings
| Option | Type | Default | Description |
|---|---|---|---|
order_resource | atom | required | Ash resource for storing orders |
personal_number_field | atom | :personal_number | Field for Swedish personal number |
given_name_field | atom | :given_name | Field for given name |
surname_field | atom | :surname | Field for surname |
verified_at_field | atom | :bankid_verified_at | Field for verification timestamp |
ip_address_field | atom | :ip_address | Field for IP address |
Timing Configuration
| Option | Type | Default | Description |
|---|---|---|---|
order_ttl | pos_integer | 300 | Total auth window in seconds |
order_renewal_interval | pos_integer | 28 | Renewal interval in seconds |
max_renewals | pos_integer | 10 | Maximum number of renewals |
poll_interval | pos_integer | 2000 | Recommended poll interval in ms |
cleanup_interval | pos_integer | 300000 | Cleanup interval in ms |
consumed_order_ttl | pos_integer | 86400 | Retention time for consumed orders |
Data Models
User Resource Fields
These fields are required on your user resource:
attributes do
attribute :personal_number, :string, allow_nil?: false
attribute :given_name, :string, allow_nil?: false
attribute :surname, :string, allow_nil?: false
attribute :bankid_verified_at, :utc_datetime_usec, allow_nil?: true
attribute :ip_address, :string, allow_nil?: true
end
identities do
identity :unique_personal_number, [:personal_number]
endOrder Resource Fields
Required fields for the BankID order resource:
attributes do
attribute :order_ref, :string, allow_nil?: false
attribute :status, :atom, default: :pending
attribute :auto_start_token, :string, allow_nil?: false
attribute :qr_start_token, :string, allow_nil?: true
attribute :qr_start_secret, :string, allow_nil?: true, sensitive?: true
attribute :expires_at, :utc_datetime_usec, allow_nil?: false
attribute :consumed_at, :utc_datetime_usec, allow_nil?: true
attribute :session_id, :string, allow_nil?: false
attribute :completion_data, :map, allow_nil?: true
attribute :ip_address, :string, allow_nil?: true
end
identities do
identity :unique_order_ref, [:order_ref]
endJavaScript Client Examples
Basic Usage
class BankIDClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.pollInterval = 2000;
}
async initiate(deviceInfo = {}) {
const response = await fetch(`${this.baseUrl}/user/bank_id/initiate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_info: deviceInfo })
});
if (!response.ok) throw new Error('Failed to initiate');
return await response.json();
}
async poll(orderRef) {
const response = await fetch(`${this.baseUrl}/user/bank_id/poll?order_ref=${orderRef}`);
if (!response.ok) throw new Error('Poll failed');
return await response.json();
}
async authenticate(deviceInfo = {}) {
const initResult = await this.initiate(deviceInfo);
return new Promise((resolve, reject) => {
const pollTimer = setInterval(async () => {
try {
const pollResult = await this.poll(initResult.order_ref);
if (pollResult.status === 'complete') {
clearInterval(pollTimer);
const signInResult = await this.signIn(initResult.order_ref, pollResult.completion_data);
resolve(signInResult);
} else if (pollResult.status === 'failed') {
clearInterval(pollTimer);
reject(new Error(`Authentication failed: ${pollResult.hint_code}`));
} else if (pollResult.auto_start_token) {
// Order was renewed
initResult.auto_start_token = pollResult.auto_start_token;
initResult.qr_start_token = pollResult.qr_start_token;
}
} catch (error) {
clearInterval(pollTimer);
reject(error);
}
}, this.pollInterval);
});
}
async signIn(orderRef, completionData) {
const response = await fetch(`${this.baseUrl}/user/bank_id`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_ref: orderRef, completion_data: completionData })
});
if (!response.ok) throw new Error('Sign-in failed');
return await response.json();
}
}QR Code Generation
import QRCode from 'qrcode';
class QRCodeGenerator {
static generateAnimatedQR(qrStartToken, qrStartSecret) {
return (time) => {
const timeStr = time.toString().padStart(10, '0');
const data = `${qrStartToken}${timeStr}`;
const hash = this.simpleHash(data + qrStartSecret);
return `${qrStartToken}${timeStr}${hash}`;
};
}
static simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(16, '0');
}
static async renderQRCode(canvas, data) {
await QRCode.toCanvas(canvas, data, {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
});
}
}
// Usage
const qrGenerator = QRCodeGenerator.generateAnimatedQR(qrStartToken, qrStartSecret);
const canvas = document.getElementById('qr-code');
// Update QR code every second
setInterval(() => {
const time = Math.floor(Date.now() / 1000);
const qrData = qrGenerator(time);
QRCodeGenerator.renderQRCode(canvas, qrData);
}, 1000);Security Considerations
Session Binding
Orders are bound to the Phoenix session ID to prevent session hijacking:
# The session_id is automatically extracted and validated
%BankIDOrder{session_id: session_id} = orderSensitive Data Protection
qr_start_secretis marked as sensitive and never exposed to clients- All certificates and private keys should be protected with proper file permissions
- User personal numbers are stored securely in the database
Rate Limiting
Consider implementing rate limiting for:
- Initiate requests per IP
- Poll requests per order
- Total requests per user
Example with Phoenix plug:
# In your router
pipeline :rate_limit do
plug Hammer.Plug,
rate_limit: {"auth:requests", 60_000, 100}, # 100 requests per minute
by: :ip
end
scope "/auth" do
pipe_through [:api, :rate_limit]
# ... your auth routes
endTesting
Mock BankID Client
For testing, you can mock the BankID client:
# test/support/bankid_mock.ex
defmodule BankIDMock do
def auth(params, _opts) do
case params["personalNumber"] do
"199001011234" ->
{:ok, %{
"orderRef" => "test-order-ref",
"autoStartToken" => "test-auto-start",
"qrStartToken" => "test-qr-token",
"qrStartSecret" => "test-secret"
}}
_ ->
{:error, "invalidPersonalNumber"}
end
end
def collect(order_ref, _opts) do
if order_ref == "test-order-ref" do
{:ok, %{
"status" => "complete",
"completionData" => %{
"user" => %{
"personalNumber" => "199001011234",
"name" => "Test User",
"givenName" => "Test",
"surname" => "User"
}
}
}}
else
{:ok, %{"status" => "pending"}}
end
end
endIntegration Tests
defmodule MyAppWeb.BankIDAuthTest do
use MyAppWeb.ConnCase
test "initiate authentication", %{conn: conn} do
conn = post(conn, "/auth/user/bank_id/initiate", %{
"return_url" => "https://example.com/callback"
})
assert json_response(conn, 200)["status"] == "pending"
assert json_response(conn, 200)["order_ref"]
end
test "complete authentication flow", %{conn: conn} do
# Initiate
conn = post(conn, "/auth/user/bank_id/initiate", %{})
order_ref = json_response(conn, 200)["order_ref"]
# Mock completion
# ... set up test completion data
# Complete sign-in
conn = post(conn, "/auth/user/bank_id", %{
"order_ref" => order_ref,
"completion_data" => completion_data
})
assert json_response(conn, 200)["access_token"]
end
end