Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Bankid Laravel Package

ljsystem/bankid

A Laravel package for integrating Swedish BankID electronic identification and signing into your application. It provides a simple, configurable way to initiate authentication, verify users, and handle BankID flows within PHP projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require ljsystem/bankid
php artisan vendor:publish --tag=bankid

This gives you config/bankid.php. The package wraps the BankID RP JSON API (/rp/v6.0), so the first things to set are:

// config/bankid.php
'endpoint'    => env('BANKID_ENDPOINT', 'https://appapi2.test.bankid.com/rp/v6.0'),
'certificate' => env('BANKID_CERTIFICATE'),        // path to your .p12/.pem
'password'    => env('BANKID_PASSWORD'),
'ca_certificate' => env('BANKID_CA_CERTIFICATE'),  // required for the test env

Get test credentials before writing any code. Download the BankID test RP certificate from the BankID developer portal, drop the .p12 (and test CA) into storage/app/bankid/, and point the config there. Without a valid cert every request fails with certificateErr — nothing else works until this is done.

First use case — authenticate a user

A minimal controller action that kicks off an auth order:

use Ljsystem\BankID\Facades\BankID;

public function start(Request $request)
{
    $result = BankID::auth($request->ip()); // personalNumber optional

    return response()->json([
        'orderRef'       => $result['orderRef'],
        'autoStartToken' => $result['autoStartToken'],
    ]);
}

That single call is the whole "hello world". From there the flow is: client gets autoStartToken → opens bankid:///?autostarttoken=... (mobile) or renders a QR → your app polls collect → user is identified by completionData.user.personalNumber.

Implementation Patterns

The poll loop is the core pattern. auth/sign only create the order; collect drives everything. Put it behind a JSON endpoint and poll from the browser at ~1–2s:

public function collect(Request $request)
{
    $result = BankID::collect($request->validate(['orderRef' => 'required|string'])['orderRef']);

    return match ($result['status']) {
        'complete' => $this->handleComplete($result['completionData']),
        'failed'   => $this->handleFailure($result['hintCode']),
        default    => response()->json($result), // pending
    };
}

Keep orderRef server-side in the session (or a short-lived DB row) rather than trusting a client-supplied value in production — an attacker who guesses an orderRef can probe someone else's flow.

QR flow. For desktop users, the response from auth also carries qrStartToken and qrStartSecret. You must generate the animated QR yourself (the package just exposes the fields — HMAC is not done for you):

$time = now()->diffInSeconds($startedAt);
$qrAuthCode = hash_hmac('sha256', (string) $time, $qrStartSecret);
$qrData = "bankid.{$qrStartToken}.{$time}.{$qrAuthCode}";

Regenerate every second in JS (or a small /qr endpoint) and encode $qrData as a QR. Cache-bust it — a static QR will fail with expiredTransaction.

Sign flow. Use BankID::sign(...) for contracts / consent rather than plain auth. userVisibleData must be base64-encoded text (BankID enforces an encoding) and shows in the user's app; userNonVisibleData is your own reference (e.g. order ID) — useful for auditing.

$result = BankID::sign(
    $request->ip(),
    base64_encode("Sign agreement #{$order->id}"),
    base64_encode(json_encode(['order_id' => $order->id])),
);

Same-device vs. cross-device. Mobile users tapping bankid:///?autostarttoken=... complete on the same device. Desktop users get the QR path. Decide per-request whether to show a "Open BankID" button or a QR — you can't reliably know from the API, so detect client type in your own code.

Post-completion handling. Treat completionData as the trusted identity: completionData.user.personalNumber, name, givenName, surname. Resolve to a local User via firstOrCreate, log the orderRef and completionData.signature for audit, then issue a session token. Don't put BankID state in the user model — it's an external identity assertion, not a credential.

Cancel on abandonment. Wire a beforeunload / modal-close handler that hits BankID::cancel($orderRef). Orders linger and count against your rate limits otherwise.

Gotchas and Tips

  • endUserIp must be the real client IP. Behind a load balancer, configure Laravel's TrustProxies middleware first or BankID rejects requests. Never pass the server's IP.
  • Certificates are environment-specific. The test CA cert (appapi2.test.bankid.com) is different from production (appapi2.bankid.com). Keep two config blocks and never ship the test cert to prod. If you see certificateErr, it's almost always a wrong cert / missing password / wrong endpoint pairing.
  • P12 vs PEM. Some setups need a .p12 with password; others need a PEM chain. If Guzzle throws cURL error 58/60, convert with openssl pkcs12 -in ... -out ... and re-point the config.
  • Polling too fast gets throttled. BankID recommends ~1s. If you see outstandingTransaction stuck or failed with rate-limit hint codes, back off. Also respect the hint codes: userSign (user needs to confirm), started, userCancel, expiredTransaction.
  • orderRef expires in ~30s of inactivity and is single-use. Never reuse it across flows, and don't store it beyond the auth attempt.
  • Config caching bites. php artisan config:cache freezes env() values into the cached config; changing certs afterwards does nothing until you re-run config:cache (or config:clear).
  • HTTPS is required for bankid:// autostart. On localhost, the mobile autostart link won't fire. Test the real flow with ngrok/valet share or by typing the QR on a phone.
  • completionData is the only trustworthy signal. Don't infer success from a redirect or a poll that returns pending then times out — only status === 'complete' with a valid signature means success.
  • Extension points are thin. The package is a thin HTTP wrapper, so if you need retries, circuit breaking, or request logging, wrap calls in your own BankIDService and inject the client there. This is also the seam to use for a fake/mocked client in tests — swap the binding in AppServiceProvider::register().
  • Log hintCode on failures. The API returns terse codes (userCancel, certificateErr, expiredTransaction, startFailed). Logging the raw response plus orderRef and IP will save you hours when a support ticket says "it just didn't work."
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings