QR codes
Animated QR codes are used with BankID when the user visits the RP’s service on one device, i.e. a computer, but use BankID on another device, i.e. their mobile. The flow is:
The RP service generates the QR code.
The RP presents the QR code to the user.
The user scans the QR code using their BankID app.
If successful, the BankID app will proceed with the identification or signature.
Animation and timings
To increase security, animated QR codes are used. This means that the RP continuously updates the QR code, thereby making remote fraud more difficult.
The RP should update the QR code every second.
The codes should be generated continuously, not in advance since pre-generation would lower security.
The order to our server is valid for 30 seconds, meaning the user must scan the QR code within this time limit.
If the user approaches a time-out, the RP should provide the user with an option to extend the session. If the time is extended, a new order must be created.
QR code generation
QR codes are generated by the RP. There are many third-party tools and libraries available on the market for this. For generation, use the pattern "bankid.qrStartToken.time.qrAuthCode" as link in the QR code, where:
bankid is a fixed prefix.
qrStartToken is from the auth or sign response.
time is the number of seconds since the order was created from auth or sign was returned.
qrAuthCode is computed as HMACSHA256(qrStartSecret, time) where time is the number of seconds since the response from auth or sign was returned and qrStartSecret is from the auth or sign response.
Note: The qrStartSecret shouldn’t be sent to the client. It’s a secret only to be shared by the BankID service and the RP service.
Tips and recommendations:
The code is supposed to be read from the screen. Therefore, the ‘QR code error correction level’ can be low.
Colours in the QR code should be kept to a minimum, we recommend using black.
Allow for some margin to enhance contrast.
Avoid including other information or graphics in the QR code, e.g. layered logotypes.
If the QR code is too old or too fresh, the RP must initiate a new request and the user must try again. If this error occurs frequently, the RP should consider adjusting their implementation of how the QR codes are computed. Make sure to check the timing.
Example
If the qeStartToken is 67df3917-fa0d-44e5-b327-edcc928297f8 and the and qrStartSecret is d28db9a7-4cde-429e-a983-359be676944c, the following urls and times will be generated:
Time | QR data | QR |
---|---|---|
t=0 |
bankid.67df3917-fa0d-44e5-b327-edcc928297f8.0.dc69358e712458a66a7525beef148ae8526b1c71610eff2c16cdffb4cdac9bf8 |
|
t=1 |
bankid.67df3917-fa0d-44e5-b327-edcc928297f8.1.949d559bf23403952a94d103e67743126381eda00f0b3cbddbf7c96b1adcbce2 |
|
t=2 |
bankid.67df3917-fa0d-44e5-b327-edcc928297f8.2.a9e5ec59cb4eee4ef4117150abc58fad7a85439a6a96ccbecc3668b41795b3f3 |
t=0
bankid.67df3917-fa0d-44e5-b327-edcc928297f8.0.dc69358e712458a66a7525beef148ae8526b1c71610eff2c16cdffb4cdac9bf8
t=1
bankid.67df3917-fa0d-44e5-b327-edcc928297f8.1.949d559bf23403952a94d103e67743126381eda00f0b3cbddbf7c96b1adcbce2
t=2
bankid.67df3917-fa0d-44e5-b327-edcc928297f8.2.a9e5ec59cb4eee4ef4117150abc58fad7a85439a6a96ccbecc3668b41795b3f3
Sample code
The sample code is included as a detailed description of how to compute the QR codes.
Python
import hashlib
import hmac
import time
qr_start_token = rp_response["qrStartToken"]
# "67df3917-fa0d-44e5-b327-edcc928297f8"
qr_start_secret = rp_response["qrStartSecret"]
# "d28db9a7-4cde-429e-a983-359be676944c"
order_time = time.time()
# (The time in seconds when the response from the BankID service was delivered)
qr_time = str(int(time.time() - order_time))
# ("0" or another string with a higher number depending on order_time and current time)
qr_auth_code = hmac.new(qr_start_secret.encode(), qr_time.encode(), hashlib.sha256).hexdigest()
# "dc69358e712458a66a7525beef148ae8526b1c71610eff2c16cdffb4cdac9bf8" (qr_time="0")
# "949d559bf23403952a94d103e67743126381eda00f0b3cbddbf7c96b1adcbce2" (qr_time="1")
# "a9e5ec59cb4eee4ef4117150abc58fad7a85439a6a96ccbecc3668b41795b3f3" (qr_time="2")
# (64 chars hex)
qr_data = str.join(".", ["bankid", qr_start_token, qr_time, qr_auth_code])
# "bankid.67df3917-fa0d-44e5-b327-edcc928297f8.0.dc69358e712458a66a7525beef148ae8526b1c71610eff2c16cdffb4cdac9bf8" (qr_time="0")
# "bankid.67df3917-fa0d-44e5-b327-edcc928297f8.1.949d559bf23403952a94d103e67743126381eda00f0b3cbddbf7c96b1adcbce2" (qr_time="1")
# "bankid.67df3917-fa0d-44e5-b327-edcc928297f8.2.a9e5ec59cb4eee4ef4117150abc58fad7a85439a6a96ccbecc3668b41795b3f3" (qr_time="2")
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.time.temporal.ChronoUnit;
String qrStartToken = rpResponse.getQrStartToken();
// "67df3917-fa0d-44e5-b327-edcc928297f8"
String qrStartSecret = rpResponse.getQrStartSecret();
// "d28db9a7-4cde-429e-a983-359be676944c"
String qrTime = Long.toString(orderTime.until(Instant.now(), ChronoUnit.SECONDS));
// ("0" or another string with a higher number depending on order time and current time)
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(qrStartSecret.getBytes(StandardCharsets.US_ASCII), "HmacSHA256"));
mac.update(qrTime.getBytes(StandardCharsets.US_ASCII));
String qrAuthCode = String.format("%064x", new BigInteger(1, mac.doFinal()));
// "dc69358e712458a66a7525beef148ae8526b1c71610eff2c16cdffb4cdac9bf8" (qr_time="0")
// "949d559bf23403952a94d103e67743126381eda00f0b3cbddbf7c96b1adcbce2" (qr_time="1")
// "a9e5ec59cb4eee4ef4117150abc58fad7a85439a6a96ccbecc3668b41795b3f3" (qr_time="2")
// (64 chars hex)
String qrData = String.join(".", "bankid", qrStartToken, qrTime, qrAuthCode)
// "bankid.67df3917-fa0d-44e5-b327-edcc928297f8.0.dc69358e712458a66a7525beef148ae8526b1c71610eff2c16cdffb4cdac9bf8" (qr_time="0")
// "bankid.67df3917-fa0d-44e5-b327-edcc928297f8.1.949d559bf23403952a94d103e67743126381eda00f0b3cbddbf7c96b1adcbce2" (qr_time="1")
// "bankid.67df3917-fa0d-44e5-b327-edcc928297f8.2.a9e5ec59cb4eee4ef4117150abc58fad7a85439a6a96ccbecc3668b41795b3f3" (qr_time="2")