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

Installation

Add bankid to your list of dependencies in mix.exs:

def deps do
  [
    {:bankid, "~> 0.1.0"}
  ]
end

Or for local development:

def deps do
  [
    {:bankid, path: "../bankid"}
  ]
end

Quick 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):

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
end

Cancel Authentication

:ok = BankID.cancel(auth.order_ref)

API Reference

Core Functions

QR Code Generation

Low-Level Client

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)
end

Custom 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
end

Security Considerations

  1. 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
  2. QR Code Secret

    • qr_start_secret must NEVER be sent to the client
    • Always generate QR codes server-side
    • Use server-side polling to check authentication status
  3. Session Security

    • Bind order references to user sessions to prevent hijacking
    • Implement proper timeout mechanisms
    • Clean up expired orders
  4. 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.0

Separate modules:

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
end

Note: 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:

Connection Errors

If you can't connect to BankID:

Test Mode Not Working

If test mode fails:

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

License

MIT

Related Projects

Resources