App2App support (public preview)¶
BankID with biometrics supports a direct app-to-app based authentication flow, where the user directly jumps in and out of the BankID App, without the merchant app needing to open an in-app browser.
Are you looking for the app_callback_uri
parameter?
If you simply want to return the user to your native app after the user has completed the BankID with biometrics authentication, you should instead achieve this using the app_callback_uri
parameter.
For iOS only
The App2App flow is an optimization which is only relevant for iOS devices. This is because Android devices are able to use their key directly in a Custom Tab launched inside your Android app without needing to involve a second app.
See the page on implementing in a native application on android for further details on how to implement an optimal flow for Android.
In preview 🚧
The App2App flow is currently in preview, and your client will need explicit access to use this flow. Get in touch to enable access to this flow for you client.
Prerequisites
For now we require merchants to supply both:
- The user's NNIN
- Information about the device the merchant's app is installed on.
That information is required so that we can be reasonably sure the flow will start correctly.
This means you cannot currently use the App2App flow for initial user-authentication, and you are required to implement another flow, like authorization code grant with app_callback_uri
for the cases when BankID biometrics refuses to initiate an App2App flow due to device mismatch.
Steps
- 1. Register a client with BankID OIDC
- 2. Get the BankID OIDC Configuration
- 3. Get an access token from BankID OIDC
- 4. Get the BankID with biometrics OIDC configuration
- 5. Register the permission upfront
- 6. Acquiring Device Information
- 7. Starting the flow
- 8. Make the user authenticate
- 9. Verifying callback URI
- 10. Poll for the result
- 11. Validate the ID token
1. Register a client with BankID OIDC¶
If you don't have a client registered with BankID OIDC
yet, you'll need to register one. Follow the instructions
here to register your
app and obtain the necessary client credentials (client_id
and client_secret
).
Info
Ensure that your client has the permissions/client
and permissions/app2app
scopes available.
2. Get the BankID OIDC Configuration¶
To ensure seamless integration with BankID OIDC and avoid hardcoding specific endpoints in your app, follow these steps to dynamically fetch the OpenID Connect configuration:
-
Determine the appropriate configuration URL based on the environment:
Environment Url Production https://auth.bankid.no/auth/realms/prod/.well-known/openid-configuration Current (public test environment) https://auth.current.bankid.no/auth/realms/current/.well-known/openid-configuration -
Send an HTTP GET request to the respective configuration URL using your preferred programming language or API tool.
- Capture the response which will contain a JSON document with the configuration. By dynamically fetching the OIDC configuration, your app remains flexible, allowing for future changes or updates to the endpoints without requiring modifications to your code.
Example response from Current (relevant fields only)
{
...
"token_endpoint": "https://auth.current.bankid.no/auth/realms/current/protocol/openid-connect/token",
...
}
This is a sample response. The actual response may differ.
3. Get an access token from BankID OIDC¶
To obtain an access token from OIDC, follow these steps:
-
Retrieve the token endpoint URL
token_endpoint
dynamically from the OIDC configuration. -
Call the token endpoint URL with your client credentials.
You can find more detailed information about acquiring access tokens in the BankID OIDC documentation.
-
In the scope field, include the following:
permissions/app2app
andpermissions/client
. This field specifies the scope of access granted to the token.
Here's an example of a valid request in the current
environment:
POST /auth/realms/current/protocol/openid-connect/token HTTP/1.1
Host: auth.current.bankid.no
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <Base64-encoded client id:client secret>
grant_type=client_credentials&scope=permissions/app2app%20permissions/client
Make sure to replace the <Base64-encoded client id:client secret>
placeholder in the Authorization header with your actual client credentials.
4. Get the BankID with biometrics OIDC configuration¶
Note
The BankID with biometrics OIDC configuration is a separate configuration from the BankID OIDC configuration.
To ensure seamless integration and avoid hardcoding specific endpoints in your app, follow these steps to dynamically fetch the BankID with biometrics OpenID Connect configuration:
-
Determine the appropriate configuration URL based on the environment:
Environment Url Production https://app.bankid.no/.well-known/openid-configuration Current (public test environment) https://current.aletheia-test.idtech.no/.well-known/openid-configuration -
Send an HTTP GET request to the respective configuration URL using your preferred programming language or API tool.
- Capture the response which will contain a JSON document with the configuration. By dynamically fetching the OIDC configuration, your app remains flexible, allowing for future changes or updates to the endpoints without requiring modifications to your code.
- To enhance the reliability and performance of your integration, we strongly advise implementing response caching for the OIDC configuration.
5. Register the permission upfront¶
To register a permission, follow these steps:
-
Construct a permission statement. See the permissions API for detailed information of how to construct the different permissions. Encode the permission statement using URL safe Base64 encoding. Below is an example of a permission statement for a payment:
const permissionStatement = { "nonce": "dW5pcXVlIHZhbHVl", // Must be unique "id": "YmFza2V0IGlk", // Your reference "payments": [ { "paymentId": "cGF5bWVudCBpZA", // Your reference "amount": "123.45", "currency": "NOK", "creditorName": "Scrooge McDuck" } ] } // Example implementation of a b64 URL Safe Encoder function b64URLSafeEncode(stringData) { return btoa(stringData) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); } b64URLSafeEncode( JSON.stringify( permissionStatement ) ); // -> "eyJub25jZSI6ImRXNXBjWFZsSUhaaGJIVmwiLCJpZCI6IlltRnphMlYwSUdsayIsInBheW1lbnRzIjpbeyJwYXltZW50SWQiOiJjR0Y1YldWdWRDQnBaQSIsImFtb3VudCI6IjEyMy40NSIsImN1cnJlbmN5IjoiTk9LIiwiY3JlZGl0b3JOYW1lIjoiU2Nyb29nZSBNY0R1Y2sifV19"
Note
- The
nonce
must be a unique value for each permission you register. - Use
id
(basket ID) andpaymentId
as references to the payment(s) in your systems. These will not be shown to the end user. - The
amount
is a decimal amount represented as a string and must use the dot as the decimal separator.
- The
-
Send a POST request to the permissions endpoint, providing the access token received in the first step as a bearer token. Include the permission, which is the URL safe Base64 encoding of the permission statement object.
Intent specifies how the client wants to complete the process using the permission. For the case of the App2App flow, include
app2app
as the intent.You'll very likely also want to include the
app_callback_uri
parameter to specify where the user will return after the authentication has completed. See theapp_callback_uri
parameter for more information.POST /permissions/v1/ HTTP/1.1 Host: api.current.aletheia-test.idtech.no Content-Type: application/json Authorization: Bearer <access token from step 3> { "type": "payment.v1", "loa": "sub", "iat": 1617091752, "exp": 1617092652, "permission": < URL safe b64 encoded permission statement from step 2a >, "intents": [ "app2app" ], "loginHint": [ { "scheme": "nnin", "value": "<nnin_as_string>" } ], "app_callback_uri": "https://merchant.app/app_callback", }
Note
iat
andexp
are timestamps in seconds since the UNIX epoch.iat
can be at most 1 minute into the past.exp
sets the maximum time by which the permission must be granted and will be set just a few minutes into the future. -
On a successful request, you will receive a response body containing a permission ID and a permission token:
{ "id": "1.BY.MEKrA-00GVtmvWOCDYQko_fSg93LO5n2rBnlj-X4MQg.BiDEl_CgfakqahcXjLrFet_GGpEkR_W8D-RK4hORBO0", "permissionToken": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjB5Z3hDX3UzQlpPWXlKZ0Y3eDJ0eWtQaTRCTml2cG9KMDBmYjFzTGliUVUiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2MTcwOTU3NDQsImlhdCI6MTYxNzA5MjE0NCwicyI6IjA6LTEjOTIzIiwicCI6IkJpREVsX0NnZmFrcWFoY1hqTHJGZXRfR0dwRWtSX1c4RC1SSzRoT1JCTzAiLCJuYmYiOjE2MTcwOTIxNDh9.39GWvNjNMSKKhKOF4t4HcDnZGNJmLmPT3f612z1VKko" }
6. Acquiring Device Information¶
We require the following information to perform device-matching
- Device Platform, which is the literal string
ios
- Device Manufacturer
- Device model
Field name | Value on iOS | JSON field name |
---|---|---|
Platform | ios |
p |
Manufacturer | Apple |
ma |
Device Model | See below | mo |
The values will be formatted in a JSON-document, that will further be encoded in a URL-safe Base64 encoding just like in step 5.
Example document for an iPhone XS
Finding the device model on iOS¶
Your device model value must be equivalent to the result of the following Swift-snippet for an app2app to successfully initiate.
import Foundation
/// Returns a model identifier for the current device.
/// e.g. iPhone12,3
func modelIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
return machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
}
7. Starting the flow¶
Make a POST request to the app-authorize
endpoint. You can find the endpoint in the BankID with biometrics' OIDC-configuration under the app_authentication_endpoint
key.
- Include the
permissionToken
obtained from the permission as thelogin_hint_token
in the request to theapp-authorize
endpoint. - In the scope, include the following scopes:
- Required:
openid
- Optional:
profile
- to get the user's profile information.- Claims:
name
,given_name
,family_name
,birthdate
,updated_at
- Claims:
sub_nnin
- to get the user's national identity number (NNIN).- Claims:
sub_nnin
- Note: This is only available for clients with explicit access!
- Claims:
sub_bankid
- to get the user's BankID PID.- Claims:
sub_bankid
- Claims:
- Provide
device_info
as the URL safe Base64 encoded JSON-document of device information as specified in step 6.
Below is an example of a request to the app-authorize
endpoint:
POST /oidc/v1/app-authorize HTTP/1.1
Host: oidc.current.aletheia-test.idtech.no
Content-Type: application/x-www-form-urlencoded
client_assertion=<access token from step 3>
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&login_hint_token=string
&scope=openid
&device_info=<URL safe base64 encoded JSON-document>
&requested_expriy=300
Ensure to replace the string
placeholder values with actual data and provide appropriate values for other fields.
After calling the app-authorize
endpoint, you will receive the following response:
{
"authorize_uri": "stringstring",
"auth_req_id": "stringstring",
"expires_in": 300,
"interval": 3
}
authorize_uri
is where you send your user from your app.auth_req_id
is used to poll for the final token exchange, similarly to CIBA.interval
is the polling interval in seconds to be used between each call to the token endpoint.expires_in
may be different from the requested value.
If the user is not registered for BankID with biometrics, or if the device does not match the user's. See the OpenAPI-specification for detailed error codes.
8. Make the user authenticate¶
The authorize_uri
from the previous step must be sent to the app. The URI is a universal link.
That link should only be opened if the BankID app is installed on the same device. That can be controlled using universalLinksOnly on iOS.
Once you open the link you will have to poll on the backend to know when the authentication has succeeded.
If the link you receive is not a universal link you cannot perform an App2App authentication and have to fall back to a different method.
9. Verifying callback URI¶
Apps using deep links for app switching are susceptible to interception and misuse, potentially enabling phishing attacks. To reduce this risk and make exploitation significantly harder, we recommend the following measures:
- Include a Secure Identifier in the Callback URL
- Incorporate a hash derived from a session or app identifier into the callback URL. Using a hash ensures that sensitive identifiers are not transmitted in plaintext, adding an additional layer of security.
- Verify the Callback on Redirection
- When your app receives the redirect from the BankID app, validate that the callback matches the expected session or device. This step ensures the redirect originated from the correct source and not a malicious attempt.
- Use the Verified Redirect as a Trigger
- Only proceed to the next step in your process after successfully verifying the callback. This ensures the integrity of the flow and minimizes the risk of unauthorized actions.
10. Poll for the result¶
Make a POST request to the token endpoint using the CibaTokenRequest
schema.
POST /oidc/v1/token HTTP/1.1
Host: oidc.current.aletheia-test.idtech.no
Content-Type: application/x-www-form-urlencoded
client_assertion=<access token from step 3>
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&grant_type=urn%3Aopenid%3Aparams%3Agrant%2Dtype%3Aciba
&auth_req_id=string
Note: Make sure to replace the placeholders for auth_req_id
and the BankID OIDC access token with your actual values.
The token endpoint will respond with a 400 authorization_pending
error if the user has not finished authenticating.
We recommend triggering a poll on the user returning to your app. If the authentication succeeded you will get a 200 response
with the id_token
in the response body.
A successful response will have the following structure:
We might return other error responses as described in our OpenAPI-specification.
Note
If the user returns to your app and the token exchange is still pending, you may present
the authorize_uri
again, or wait for another poll. You may also offer a different flow as a fallback.
If the user declines the authentication you will get an explicit access_denied
error.
11. Validate the ID token¶
To ensure the authenticity and integrity of the received ID token, you must perform the following verification:
- Verify that the token is signed by a key from our JWKS (JSON Web Key Set)
Document. The URI for the JWKS document can be found under the
jwks_uri
key in the BankID with biometrics OIDC configuration document you retrieved in step 4. You must also verify that the algorithm used is found in the BankID with biometrics OIDC configuration underid_token_signing_alg_values_supported
. - We recommend verifying these additional properties:
iss
(issuer) - The issuer of the token must match the issuer in the BankID with biometrics OIDC configuration.aud
(audience) - The audience of the token must match your client ID.exp
(expiration time) - The token must not be expired.iat
(issued at) - The token must not be issued in the future.acr
(authentication context class reference) is only relevant if you've asked for forced step up. If relevant must be verified to be the expected value.
Further information on how to validate the ID token can be found in the OpenID Connect Core specification.
Example flow using app2app
sequenceDiagram
autonumber
participant MerchantApp as Merchant App
participant MerchantBackend as Merchant Backend
participant Backend as BankID Biometrics Backend
participant BAPP as Authenticating app
Note left of MerchantBackend: Need to have NNIN of user
MerchantApp ->> MerchantApp: gatherDeviceInfo
MerchantApp ->> MerchantBackend: beginAuth w/ device_info<br/>{"p": "ios", "ma": "Apple", "mo": "iPhone11,2"}
MerchantBackend ->> Backend: POST /permissions<br/>{<br/>app_callback_uri: "https://merchant.com/callback",<br/>intents: ["app2app"],<br/>loginHint: [{scheme: "nnin", value: "12345678910"}]<br/>}
MerchantBackend ->> Backend: POST /app-authorize<br/>device_info=X&login_hint_token=Y&scope=Z
break device mismatch
Backend ->> MerchantBackend: Error
Note left of MerchantBackend: Need to fall back to alternate flow
end
Backend ->> MerchantBackend: authorize_uri
MerchantBackend ->> MerchantApp: authorize_uri
MerchantApp ->> BAPP: open authorize_uri w/ universalLinksOnly
break app not installed, universal link does not open
MerchantApp ->> MerchantApp: Error
Note right of MerchantApp: Need to fall back to alternate flow
end
BAPP ->> BAPP: user authenticates
BAPP ->> MerchantApp: app switch OUT w/ app_callback_uri
MerchantApp ->> MerchantBackend: completeAuth
MerchantBackend ->> Backend: POST /token<br/>auth_req_id=A
Backend ->> MerchantBackend: id_token