BankID
On Zkrinet: add the block under Building blocks in the panel — one click, your own code is untouched and a snapshot is taken first. The installation steps below apply if you run PocketBase yourself, for example after an export.
Log in with Swedish BankID through an EU-based OIDC broker: Idura (formerly Criipto, Denmark) or Signicat (Norway). Tested against PocketBase 0.40.4.
#Honestly, what is what
The login itself is PocketBase's own. PocketBase has a built-in OpenID Connect provider (the slots oidc, oidc2 and oidc3 under OAuth2). It handles the login page, PKCE, the code exchange, the token and the user data. The plugin does not build its own login. It adds four things:
- A check that tells you exactly what is missing and compares your settings against the broker's discovery document.
- Preparation of
users: BankID does not hand out an email address, and without this change PocketBase cannot create the account (MEASURED:400 email: Cannot be blank). Email becomes optional for BankID accounts but is still required for regular sign-up. - Name: the broker's
name, or first name + last name (given_name+family_name) ifnameis missing. - Personal identity number (personnummer): not stored by default, and removed from the response to the browser (see GDPR below). If you want, you can turn on linking via hash, so that the account survives a change of broker.
#Install
Copy pb_hooks/ and pb_migrations/ into the project and restart. Set Application URL under Settings to your real address (https).
#The check
GET /api/zk/bankid/kontroll (superuser)
GET /api/zk/bankid/kontroll?discovery=https://…/.well-known/openid-configuration
Responds with:
aterkopplingsadress: the one you paste in at the broker (https://dinapp.se/api/oauth2-redirect).leverantorer: per BankID slot, which broker it is (idura/signicat/okand), what is missing, andforslag, that is the exact values from the broker's discovery document to paste into PocketBase (including the correctacr_values).varningarabout:- Application URL missing, being http, or pointing to localhost
- OAuth2 being turned off, or email being required again
- no OIDC provider, or Client ID/secret/URL missing
- an address being http
- the addresses not matching discovery
- issuer missing or wrong
acr_valuesmissing, so that the broker shows a picker with all e-IDs instead of BankID- the broker not listing the scopes PocketBase requests
- the ID token being read without signature verification
- discovery not being readable
Without ?discovery=, <first issuer>/.well-known/openid-configuration is fetched, or otherwise the same path on the Auth URL's address. At Signicat the issuer is under a path, so in that case give the address from Signicat's Dashboard (Well-Known URL).
#Step by step: Idura
- Create an account at Idura and an Application in the Idura Dashboard. You get a domain of the form
ditt-namn.idura.broker(test domain first; the production domain is registered separately). - Add the callback address from the check as the callback URL. Turn on OAuth2 Code Flow and create a client secret. Copy the Client ID (realm) and the secret.
- In PocketBase: Collections → users → ⚙ → OAuth2 → Add provider → OpenID Connect (the slot
oidc).- Client ID + Client secret from step 2. Display name:
BankID. - Run
GET /api/zk/bankid/kontroll?discovery=https://ditt-namn.idura.broker/.well-known/openid-configurationand paste inforslag: Auth URL (ends with?acr_values=urn:grn:authn:se:bankid), Token URL, User info URL.
- Client ID + Client secret from step 2. Display name:
- Run the check again until
varningaris empty. - Test BankID: Idura refers to BankID's test portal (developers.bankid.com → test portal). The BankID app can only be in test or production mode, so preferably use a separate phone. You choose the name and a valid test personal identity number yourself.
- Production: according to Idura, the BankID certificate requires an EU VAT number, and Idura orders it via Swedbank (8–15 business days).
Other BankID variants at Idura: urn:grn:authn:se:bankid:same-device (same device) and urn:grn:authn:se:bankid:qr (QR).
#Step by step: Signicat
- Create a sandbox account at Signicat. According to Signicat, Swedish BankID can be added in the sandbox without an agreement (eID Hub → ID Methods).
- Create an OIDC client, add the callback address as the redirect URI, and create a secret.
- Scopes: PocketBase always requests
openid email profileand this cannot be changed. Allow those three on the client (the check warns if discovery does not list them). - In PocketBase: OpenID Connect as above. Get the addresses with
GET /api/zk/bankid/kontroll?discovery=<Well-Known URL from Signicat's Dashboard>. The Auth URL should end with?acr_values=idp:sbid. - Test BankID: the same test portal at BankID as above.
Prices: see each provider. We have not been able to cite a source for any prices.
#The button in your app
const { record, meta } = await pb.collection("users").authWithOAuth2({ provider: "oidc" })
BankID accounts have no email (record.email === ""). If the app needs to email the user, you have to ask for the address separately. Whether the account is linked to BankID, the user can see for themselves (MEASURED):
const kopplingar = await pb.collection("_externalAuths").getFullList({ filter: 'provider = "oidc"' })
#Personal identity number and GDPR
Default: the personal identity number is stored nowhere. The plugin removes it from the response to the browser, because PocketBase otherwise sends along the entire broker response (meta.rawUser), including the personal identity number in plain text (MEASURED). The test searches all database files in pb_data for the personal identity number, byte by byte. What is stored is the broker's sub (in _externalAuths) and the name.
sub is a pseudonym per broker: at Idura it is, according to the documentation, unique per user and Idura tenant. This means: if you change broker (or tenant), every user gets a new sub and thereby a new, empty account. We state this plainly because it is a lock-in.
Optional: linking via hash. Set ZK_BANKID_PNR_NYCKEL (at least 32 random characters) and HMAC-SHA256(personnummer, nyckel) is stored in the hidden field users.bankid_pnr_hmac. If the same person logs in with a new sub, the login is linked to the same account (tested: change within the same slot and change between slots). Consequences, honestly:
- The hash is still personal data (pseudonymised, not anonymous). It belongs in your record of processing activities and must be included on erasure.
- There are only a few hundred million possible personal identity numbers. If the key leaks, the hash can be computed back to the personal identity number in a short time. Protect the key like a password. It lives only in the environment, never in the database.
- Under the Swedish Data Protection Act (2018:218), Chapter 3, Section 10, personal identity numbers may be processed without consent only when it is clearly justified. Being able to change provider without users losing their accounts may be such a reason, but that is your assessment.
- Only the slots listed in
ZK_BANKID_LEVERANTORER(defaultoidc) can link. Another OIDC service onoidc2, where the user might be able to set their own claims, can never take over a BankID account (tested). - At Signicat the personal identity number comes in the claim
nin, and it requires the scopenin, which PocketBase does not request. With Signicat the linking will therefore probably be empty (not measured). At Idura,ssncomes without an extra scope according to the documentation.
The broker processes personal data on your behalf. Sign a data processing agreement with it.
#Security
- Use the User info URL (recommended). Then PocketBase fetches the user data directly from the broker over TLS. If you use the ID token instead: set
jwksURLandissuersunder extra, so that PocketBase verifies signature, issuer and audience (MEASURED against a local broker: wrong key and wrong issuer are rejected). Without them the ID token is read unverified, and the check warns. - The check requires superuser and never shows secrets (tested).
- Users cannot set
bankid_pnr_hmacthemselves (tested). - Email is still required for all sign-ups that are not an OAuth2 login, including via
/api/batch(tested).
#Limitations
- PocketBase always requests the scope
openid email profile(source codetools/auth/oidc.goin 0.40.4) and has no field for custom scopes.acr_valuesworks as part of the Auth URL. PocketBase appends its parameters with&(MEASURED). - The name is set when the account is created (PocketBase only maps it then). If the person changes their name, it is not updated.
- An existing password account is not linked to BankID automatically (there is no email to match on). PocketBase links BankID to the account if the user is logged in when
authWithOAuth2is called (according to PocketBase's source code, tested here). - Even superusers have to provide an email when they create users in the admin panel. Only the OAuth2 login is exempt.
- The check fetches the discovery document over the network when it runs.
- Addresses to
127.0.0.1/localhostgive no https warning (local test broker). Application URL on localhost still warns. - The down migration makes email required again and therefore does not go through as long as there are BankID accounts without email.
#What we have NOT been able to measure
- Real Idura and Signicat: TLS, their JWKS and key rotations, that
acr_valuesgives BankID directly, how Signicat reacts to theemailscope, and that the claim names (ssn,nin,given_name,family_name) look like that in reality. All of that comes from their documentation (Idura: docs.idura.app → Swedish BankID, Signicat: developer.signicat.com → Swedish BankID). - ID token signature algorithms other than RS256.
#Environment variables
| Variable | Default | What |
|---|---|---|
ZK_BANKID_LEVERANTORER | oidc | Which OIDC slots are BankID (e.g. oidc,oidc2 during a broker change). |
ZK_BANKID_PNR_NYCKEL | (off) | At least 32 characters. Turns on linking via personal identity number hash. |
#Testing
./bank.py bankid
19 tests against a fake broker on 127.0.0.1 (discovery, token, userinfo, JWKS). Nothing reaches the internet. The tests cover the migration, the email requirement (including via batch), the check's warnings, login without email, name, the personal identity number (searched byte by byte in pb_data and in the response), broker change, that another OIDC slot cannot take over an account, the hidden field and PocketBase's own id_token verification. The security tests and the warnings are verified with mutants: 14 of 14 broken protections were caught. The ID token test signs with RS256 and requires the Python package cryptography on the test machine. The plugin itself has no dependencies.