BankID
Pure Elixir client for Swedish BankID authentication and signing.
This library provides a complete, framework-agnostic implementation of the Swedish BankID API v6.0 with no external dependencies beyond standard Elixir libraries.
Disclaimer
This is an early version of the library and you are advised to use it at your own risk at this stage.
Features
- ✅ Pure Elixir - No Python or other external dependencies
- ✅ mTLS Support - Secure client certificate authentication
- ✅ Authentication - Full support for BankID authentication flow
- ✅ QR Code Generation - Native QR code support for mobile apps
- ✅ Test & Production - Bundled test certificates, easy production setup
- ✅ Framework Agnostic - Use with Phoenix, Plug, or any Elixir application
Installation
Add bankid to your list of dependencies in mix.exs:
def deps do
[
{:bankid, "~> 0.1.0"}
]
endOr for local development:
def deps do
[
{:bankid, path: "../bankid"}
]
endQuick Start
# 1. Start authentication
{:ok, auth} = BankID.authenticate("192.168.1.1")
# 2. Generate QR code for mobile BankID app
qr_svg = BankID.QRCode.generate_svg(
auth.qr_start_token,
auth.start_t,
auth.qr_start_secret
)
# 3. Poll for completion (repeat every 2 seconds)
{:ok, result} = BankID.collect(auth.order_ref)
case result.status do
"pending" ->
# Keep polling - show status based on result.hint_code
"complete" ->
# Success! Extract user information
user_info = BankID.extract_user_info(result.completion_data)
# %{
# "personal_number" => "199001011234",
# "given_name" => "Erik",
# "surname" => "Andersson"
# }
"failed" ->
# Handle error based on result.hint_code
end
# 4. Cancel if needed
:ok = BankID.cancel(auth.order_ref)Configuration
Testing (Default)
No configuration needed! The library includes bundled test certificates and uses BankID's test server by default:
# Works out of the box with test server
{:ok, auth} = BankID.authenticate("192.168.1.1")Test personal numbers (use with BankID test app):
198803290003199006292360
Production
You can configure certificates in two ways:
Option 1: Direct Certificate Content (Recommended for Serverless)
Ideal for serverless deployments (AWS Lambda, Google Cloud Functions, etc.) where file system access is limited.
Add to your config/runtime.exs:
config :bankid,
base_url: "https://appapi2.bankid.com/rp/v6.0",
cert: System.get_env("BANKID_CERT"),
key: System.get_env("BANKID_KEY"),
cacert: System.get_env("BANKID_CACERT")Then set environment variables with full PEM content:
export BANKID_CERT="-----BEGIN CERTIFICATE-----
MIIEyjCCArKgAwIBAgIIG8/maByOzV4w...
-----END CERTIFICATE-----"
export BANKID_KEY="-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAAS...
-----END PRIVATE KEY-----"
# Optional: CA certificate (defaults to BankID's CA cert)
export BANKID_CACERT="-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----"Option 2: File Paths (Traditional)
Use when certificates are stored as files on the file system:
Add to your config/runtime.exs:
config :bankid,
base_url: "https://appapi2.bankid.com/rp/v6.0",
cert_path: System.get_env("BANKID_CERT_PATH"),
key_path: System.get_env("BANKID_KEY_PATH")Then set environment variables before starting your application:
export BANKID_CERT_PATH="/etc/bankid/production-cert.pem"
export BANKID_KEY_PATH="/etc/bankid/production-key.pem"Important: Production certificates must be obtained from your bank after signing a BankID agreement.
Priority: If both direct content (:cert) and file path (:cert_path) are provided, direct content takes priority.
Usage
Authentication Flow
# Initiate authentication
{:ok, auth} = BankID.authenticate("192.168.1.1")
# auth contains:
# %{
# order_ref: "...", # For polling
# qr_start_token: "...", # For QR generation
# qr_start_secret: "...", # For QR generation (keep server-side!)
# auto_start_token: "...", # For same-device flow
# start_t: 1234567890 # Timestamp for QR generation
# }Require Specific User
{:ok, auth} = BankID.authenticate("192.168.1.1",
personal_number: "199001011234"
)Display Custom Message
{:ok, auth} = BankID.authenticate("192.168.1.1",
user_visible_data: "Login to MyApp",
user_visible_data_format: "simpleMarkdownV1"
)QR Code Generation
Generate an animated QR code that updates every second:
# In a LiveView or controller that runs every second
qr_svg = BankID.QRCode.generate_svg(
auth.qr_start_token,
auth.start_t,
auth.qr_start_secret,
width: 300,
color: "#0066CC"
)Security Note: Never send qr_start_secret to the client. QR codes must be generated server-side.
Polling for Status
Poll the BankID API every 2 seconds:
{:ok, result} = BankID.collect(auth.order_ref)
case result.status do
"pending" ->
# Check hint_code for user feedback:
case result.hint_code do
"outstandingTransaction" -> "Open BankID app"
"noClient" -> "Starting BankID app..."
"started" -> "Enter your security code"
"userSign" -> "Confirming..."
end
"complete" ->
user_info = BankID.extract_user_info(result.completion_data)
# User authenticated successfully
"failed" ->
# Check hint_code for error reason:
case result.hint_code do
"userCancel" -> "Cancelled by user"
"expiredTransaction" -> "Session expired"
"certificateErr" -> "Certificate error"
"startFailed" -> "Failed to start BankID"
end
endCancel Authentication
:ok = BankID.cancel(auth.order_ref)API Reference
Core Functions
BankID.authenticate/2- Start authenticationBankID.collect/2- Poll for statusBankID.cancel/2- Cancel authenticationBankID.extract_user_info/1- Extract user data from completion
QR Code Generation
BankID.QRCode.generate_svg/4- Generate QR code SVGBankID.QRCode.generate_content/3- Generate raw QR contentBankID.QRCode.elapsed_seconds/1- Calculate elapsed time
Low-Level Client
BankID.Client- Direct access to client functionsBankID.HTTPClient- HTTP client with mTLS
Framework Integration
This is a low-level client library designed to be framework-agnostic. For framework-specific integrations:
Ash Framework
Use the ash_authentication_bankid package for declarative authentication:
{:ash_authentication_bankid, "~> 0.1.0"}Phoenix/Plug
Build custom controllers using this library:
def create(conn, _params) do
client_ip = get_client_ip(conn)
{:ok, auth} = BankID.authenticate(client_ip)
conn
|> put_session(:order_ref, auth.order_ref)
|> put_session(:start_time, auth.start_t)
|> json(%{order_ref: auth.order_ref})
end
def poll(conn, %{"order_ref" => order_ref}) do
{:ok, result} = BankID.collect(order_ref)
json(conn, result)
endCustom Integration
Use in any Elixir application (GenServers, background jobs, etc.):
defmodule MyApp.BankIDAuth do
def authenticate_user(ip_address, personal_number) do
with {:ok, auth} <- BankID.authenticate(ip_address, personal_number: personal_number),
{:ok, result} <- wait_for_completion(auth.order_ref),
user_info <- BankID.extract_user_info(result.completion_data) do
{:ok, user_info}
end
end
defp wait_for_completion(order_ref) do
# Poll every 2 seconds for up to 3 minutes
# Implementation left as exercise
end
endSecurity Considerations
Certificate Security
- Never commit certificates to Git
-
Store production certificates securely (e.g.,
/etc/bankid/) - Use environment variables in production
-
Set proper file permissions:
chmod 600 /path/to/cert.pem
QR Code Secret
qr_start_secretmust NEVER be sent to the client- Always generate QR codes server-side
- Use server-side polling to check authentication status
Session Security
- Bind order references to user sessions to prevent hijacking
- Implement proper timeout mechanisms
- Clean up expired orders
IP Address
- Always use the actual client IP address
- Be careful with proxy configurations
Architecture
┌─────────────────────────────────────┐
│ BankID Module │
│ (Public API / Convenience Layer) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ BankID.Client │
│ (Core authentication operations) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ BankID.HTTPClient │
│ (mTLS HTTP communication) │
└──────────────┬──────────────────────┘
│
▼
BankID API v6.0Separate modules:
BankID.QRCode- QR code generation (independent)
Testing
The library includes test certificates and works out-of-the-box with BankID's test environment.
# In your tests
test "authenticate with BankID" do
{:ok, auth} = BankID.authenticate("192.168.1.1")
assert auth.order_ref
assert auth.qr_start_token
endNote: Actual authentication requires interaction with the BankID test app, so automated tests are limited to API communication testing.
Troubleshooting
Certificate Errors
If you see SSL/TLS errors:
- Verify certificate paths are correct
- Check file permissions
- Ensure certificates are valid and not expired
Connection Errors
If you can't connect to BankID:
- Check network connectivity
- Verify you're using the correct base URL (test vs production)
- Check firewall rules
Test Mode Not Working
If test mode fails:
- Bundled certificates should work out-of-the-box
- Verify the library is properly installed
- Check logs for detailed error messages
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
License
MIT
Related Projects
- ash_authentication_bankid - Ash Framework integration
- pybankid - Python implementation (inspiration)