Implementing the PSD2 Dynamic Linking and CIBA Flow


In the previous article on this topic, we described the central concepts of Dynamic Linking under PSD2, and what role OAuth2 and CIBA (Client-Initiated Backchannel Authentication) can play in meeting the technical requirements set out in the PSD2 Regulatory Technical Standards.

In this article, we are going to show how to implement CIBA in ForgeRock® Access Management. We’ll use Access Management in conjunction with mobile push notification to link authentication to payment details.

Overview of implementation

We are going to put together a payment authorization flow using the following building blocks:

  • ForgeRock Access Management (AM) version 6.5.2 or later
  • ForgeRock Authenticator mobile app, for strong authentication

We’ll loosely follow the guidelines included in the ForgeRock OAuth2 guide.

High-level steps follow:

  1. Install AM
  2. Enable and configure push notification
  3. Configure the OAuth2 service, including CIBA support
  4. Add a test user and register their mobile device
  5. Send a test CIBA request and acknowledge it on the mobile device

You will need to enable push notification services within AM to implement the end-to-end flow. This requires a ForgeRock subscription: if you don’t have one, you can follow the logic of this exercise, but you won’t be able to complete all the steps yourself.

Step 1: Setup

First, you’ll need to deploy AM if you haven’t already.

You’ll need to make your deployment publicly available (or at least available to your mobile device over HTTPS) in order to register the ForgeRock Authenticator for an end user. For this example, we will be deploying AM at

All the details you need are provided in the ForgeRock documentation.

Step 2: Enable push notification services

We are going to use push notification as the channel to the end user for transaction verification. This is not the only way to do it, but it works well.

In order to support push notification messages in AM, you’ll need to configure the AWS ANS settings in AM.

First, get your customer specific settings as per the ForgeRock knowledge base.

Next, copy these settings to your AM configuration via the admin UI (Configure>Global Services>Push Notification Service). We are doing this at the global level; you can also configure this at the realm level if you prefer (more on realms later):

Step 3: Create a realm

Next task is to set up a new realm for this exercise, to keep things tidy (in the admin UI, Realms>New Realm). We’ll call the realm online, leaving the rest of the values to default:

Step 4: Enable push registration

In order to support push notification for end users, those users will need to register their mobile authenticator app.

To enable registration, we will define an authentication chain consisting of two modules: a DataStore module to validate the userid and password, and a push registration module to enable QR based registration of the ForgeRock authenticator app.

Ideally, we would do this via an authentication tree rather than chain, since trees have superseded chains as the preferred authentication framework since AM 6.0. However, at time of writing, AM 6.5.2 does not support push registration via tree out of the box, so we’ll be using a chain instead.

Note: if you do want to use an authentication tree for registration, you can opt to download and deploy the push registration node available on the ForgeRock marketplace.

First, we’ll enable the push registration module via the admin UI (Authentication>Modules>Add Module). We’ll call the module “pushreg”:

Next, we’ll create an authentication chain (Authentication>Chains>Add Chain). We’ll call the chain pushreg, consisting of the DataStore and pushreg modules:

We are now ready to accept registrations for push authentication.

Step 5: Enable CIBA push

Once a user has registered an authenticator device, we want to use that device as a channel for CIBA.

To do this, we create an authentication tree for push authentication (Authentication>Trees>Create Tree). Although we don’t support push registration via trees in AM 6.5.2, we do support push authentication via trees.

The tree consists of a Username Collector, followed by the PushSendernode. After the push is sent, we move to the Push Result Verifiernode, which will loop round the Polling Wait Node, polling for the push response until the user acknowledges the push:

The tree starts with a username collector, which is invoked by the CIBA process within AM. This is a very specific use of the username collector: unlike a normal authentication tree, where the username collector is a prompt for the end user, the collection of the userid is transparent to the end user—the username is automatically populated from the CIBA request.

This is a single method of authentication, specifically for CIBA requests. It is not intended for creating user sessions, and is independent of any sessions which may be active for the same user (unlike transactional authorization, which requires an existing user session).

If there are any failures—for example, the user has not registered their authenticator yet—then, we won’t try to recover: we will go straight to failure.

6.Make sure that there is no skip option.* Click on the Push Sender node and enable the option to “Remove ‘skip’ option”. Otherwise the requests will fail during authentication.

Step 6: Create the OAuth2 service

CIBA is an OAuth2 grant type, and therefore needs an OAuth2 service to handle requests.

We need to create an OAuth2 service for our online realm in AM (Services>New Service>OAuth2 Provider). We’ll enable the service for the openid scope:

We need to configure a few more things in the service to enable CIBA.

Within a CIBA request, the client speciNes the acr value for the authentication process (acr = Authentication Context Class Reference). This tells the authorization service what type of authentication is requested. Within Access Management, we need to create a mapping from all supported acr values to their corresponding authentication trees.

You’ll find acr mapping under the “Advanced OpenID Connect” tab in theOAuth2 Provider settings. We are going to add a mapping from the acr value ciba to the authentication tree we created earlier (ciba-push):

Next, we’re going to make a small change to the CIBA configuration in the OAuth2 provider, to support RS256 signing of CIBA requests. This is so that we can use our chosen signing service for client requests…more on this later.

CIBA settings are accessed via the arrow to the right of the main tabs in the provider settings:

Add RS256 to the signing algorithms supported as follows:

Step 7: Add an OAuth2 client

Next, we’ll add an OAuth2 client so that our test CIBA client is known to AM.

We’ll create a client in our online realm (Applications>OAuth 2.0>Add Client), allowing the scope openid:

In order for this client to issue CIBA requests, we’ll need to allow the appropriate OAuth2 grant type. Under the Advanced tab, add Back Channel Request to the permitted grant types:

Finally, we’re going to register the signing key used by our CIBA client. For this exercise, we are using a self signed certificate. Under the Signing and Encryption tab, cut and paste the certificate below into the Client JWT Bearer Public Key field, and select X509 as the Public key selector:

The certificate to paste is as follows (if you are using the private key specified later on):


In real life, we are more likely to use a JWKS instead of a certificate, either statically in the client configuration, or published via a JWKS URI. In our case, our chosen signing service does not support key ids in the JOSE header, so we are stuck with a static certificate.

Step 8: Add a test user

Add a test user to your realm’s data store (Identities>Add Identity). In our case, we are using jane.doe.

Next, download the ForgeRock authenticator app from your favourite app store onto your mobile device.

Now register the app to Jane Doe’s identity. You’ll need to point your browser to the realm and authentication chain you created earlier to start the registration process.

In our case, we can hit the registration chain via

After entering the userid and password for jane.doe, you’ll be presented with a QR code on screen. Scan this with the ForgeRock Authenticator app, and you should see the registration complete successfully.


This is the exciting bit. We are now going to make a CIBA request to authenticate a transaction.

  • Build a CIBA request We are going to build a CIBA request as follows:
      "login_hint": "jane.doe",
      "scope": "openid",
      "acr_values": "ciba",
      "iss": "cibatest",
      "aud": "",
      "exp": 1571075243,
      "binding_message": "Allow ExampleBank to transfer £500.00 from your Current account to account ending 5589?"

Breaking this down:

login_hint: this tells the authorization server which user should receive the transaction verification request.

scope: this is set to “openid” so that we get an OIDC token back in the response. This is the minimum scope for CIBA — we could ask for additional scopes such as “profile” but we are keeping it simple here.

acr_values: this tells the authorisation server which authentication method to use when verifying the transaction. Here we are using “ciba”, which we have mapped to the authentication tree “ciba-push” within AM.

iss: this matches the OAuth2 client ID for the request.

aud: this has to match the issuer ID of the authorisation server. If in doubt, you can get this from the “issuer” Neld of the authorisation server’s openid configuration: for our online realm, this is published at openid-configuration.

exp: this is the epoch time at which this JWT will expire. Lots of ways to get this value; if you’re a bash fan, you can get an expiry time of 10 minutes from now like this:

echo $((`date '+%s'` + 600))

binding_message: this is the whole point of what we are doing here. This is the message sent to the end user to verify the transaction. It needs to contain details from the transaction itself in order to provide dynamic linking.

  • Sign the request.

We are now going to sign the CIBA request so that the authorization server accepts it. We’ll create a signed request JWT as per the CIBA standard, using the private key corresponding to the certificate we configured in the OAuth2 client settings in AM.

For simplicity, we are going to use a third party signing service to do this for us. In real life of course we wouldn’t do this, given that we are uploading our private key to a third-party test site.

We are going to use (thank you, Jamie Kurtz) to sign our CIBA request as follows:

curl -X POST \ \
 -H 'Content-Type: application/json' \
 -d '{
      "login_hint": "jane.doe",
      "scope": "openid",
      "acr_values": "ciba-push",
      "iss": "cibatest",
      "aud": "",
      "exp": 1571075243,
      "binding_message": "Allow ExampleBank to transfer £500.00 from your Current account to account ending 5589?"
    "key":"-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEWDBqLQTUj3x7\ngHZxjLGN7vu56CMJhUQKt/zpiNNHm5VLPl+iD6JfVQyS0Td66Bpb5DVzMpvewmH4\nfTKN38Nl/RhlV2Diq/l/3tE7f9uaaMefI1glEWsmUfP5Ig4kPvKNVE7wp8z1zuKz\ngagACfL/Q9c3REYhjiIpTlsYxixkte118awVJfaytH385WZlovGqNCK7geefE4bh\ntkaqlkpCNXUkym9HvSDrunmwp0CT7w8aK07SaJ+pKHr1hW42Gx8ELcArS9y2RUNh\nFc77WMEQCa2Vh3/NYNakFdHrjxXK+Ixgw7Pwdpnehk2eSm5Jo9tBvn4IwWRkJRuP\nRMKMXl8LAgMBAAECggEBAKFlnOaqev3/tOQQhUxxysJRYLtBBwwccAIfm9ackpCa\nY+6mJxago2iaEOve7ywo/wHqZcV8JITIZKBOye+1sHl1w0gKu6mYlE94aaXvRCV5\nXB6Ef0B5QQsO8u4oAFfrJpbmZr2MMf8dQV0th1wA0a7jpVXY2Y1buNkbf+atgHCH\nay6q6ptXnedDUrEp53tLiaUnc1syMCnHNl+Afif2nP07zRZaJ+5jXxcxEzNlCdh6\nWvCcdccLTZjYqjUy8/FxBKHAsAGtvbTUh+CdlQVj3nORBJ2M3d8Rogo15QCZF6Uh\nNaCQ905EVWuLLSS9PnmCw8cyF9r6J4cnNx3lQOwn6QECgYEA5Q+3SLZTVz9ZZGE4\nAvpdDCcdtPCktwb0QG+pTH+giP8le7bDb30/MWd+wUC67G4XzjY+ZowSm2C2QZ4R\nloPotwR7esBteVg8D0R+W4NTcCfj+2RctHnKfDK0jO2+TzSeJIQAsfU1tllh7KOQ\nJuX5djk4I2yXpwffRk2wuCuAy/kCgYEA2295mk/Vd9fdS7Eib8mdT1htrFqwu60S\nbM9PNlhUFlLV1jd6WRaL3IjLXyeAYFMi0CifjWZXIpmeYlVC1Z9RPxSUdU5xg11X\nTdyy3zExUgMQrB3FngfcmgaEZmormErPrA3/y8Y0LILvMO1y3pXDzWDFKTP9Uzak\nk8Sswa9dXCMCgYEAsBs/HMYgmQl5XrVn7NIzy24fZsdEu/q6uveeP9Q3xlvzo4PG\nCedPOFqLl2R+0dtqrf4CR7EVdSQLu7Mdbo0H0/28OYnMIOj3c/2C8DStZ6Mjolls\n9MxWItqQ+XCnShn4I7bhGfCeVQ2vPdIat+1Dt+MSCBorFh31SQXeAhpgMXECgYEA\niFP9bI1kxgvqhHgMOP89KGp72LSPUEn6RHeXct/1fdkA2RGmhWqogd7K2tcjvrRn\n0IMsfSCzyd8+s0DdQPK1+0bB5Q9THpYDA3C2AEwDpDwbQ5NLjx67Q0YBQ896Pidc\nVjxsSyFckLrX98HNt9O7zgDs/Og73lL4dIWf/sUAb5kCgYAleITkFvD0s66UkVt+\n60bn8cPRVHb7YsUIsr0XdoUS/F+A9t3J1lh3Qgcey4AiQjdMb42M2mvY/ede/xpE\n2RttXixw0X2ZOLyORYaown68ebIy+z9ImhPnud00TIeXx4ZLqygHc1ysBz4BS8a6\ntGOvItryjEH85JPx4yDbhF5FEQ==\n-----END PRIVATE KEY-----",
' returns a signed JWT (thanks again) as follows:

  • Send the signed request to the authorization server

We are now going to send our signed request to the backchannel authorization endpoint (/bc-authorize) at AM. Along with the request itself, we need to supply the client ID and secret we configured earlier on:

curl -X POST \ \
  -d 'client_id=cibatest&client_secret=password&request=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJsb2dpbl9oaW50IjoiamFuZS5kb2UiLCJzY29wZSI6Im9wZW5pZCIsImFjcl92YWx1ZXMiOiJjaWJhLXB1c2giLCJpc3MiOiJjaWJhdGVzdCIsImF1ZCI6Imh0dHBzOi8vYW0uZXhhbXBsZS5jb20vb2F1dGgyL3JlYWxtcy9yb290L3JlYWxtcy9vbmxpbmUiLCJleHAiOjE1NzEwNzUyNDMsImJpbmRpbmdfbWVzc2FnZSI6IkFsbG93IEV4YW1wbGVCYW5rIHRvIHRyYW5zZmVyIMKjNTAwLjAwIGZyb20geW91ciBDdXJyZW50IGFjY291bnQgdG8gYWNjb3VudCBlbmRpbmcgNTU4OT8ifQ.ZvmVHK2Yn9nBrtGQQDYGa-yN7m7OpTL9W_3ItiaiLrV_QI3eOc3n892NsnZjBAxzK0NhEgTYvosHIeEFkok_ROeqJqz9PrYuH4TRgLxaA9vZb4onoqms8UTAFhVFXbPyd4M8j3vTa74PqDZDEo6SW97o0H5irkY1uSJCpCdBvQElOIONJihWvzTN0czHEUfY35ViB7h4FPiezjnZDGJvh4bKkP1phFul7CR-ZX4jV2nLgg2HrkwyN_kOhSsfXvikPcCyjJoWAUtZNJg868nOSCsvBimyAURieDuYS5LH60wgTNGW6MiXNQxldLPGqCnl1_mF4eOG23f-aQBGxQLthA'

We’ll get back a response with the parameter auth_req_id. This is our unique reference for this request:


We then poll the authorization server’s token endpoint for the status of this request, sending in the auth_req_id value we got before, like this:

curl -X POST \ \
  -d 'client_id=cibatest&client_secret=password&grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba&auth_req_id=WS9G-GUCyzpEMq1AnHUFLeeZXxE'

Note the special grant type urn:openid:params:grant-type:ciba which tells AM that this is linked to a CIBA request.

Until the user notification and authentication process has finished, we’ll keep getting this back from the authorization server:

  "error_description": "End user has not yet been authenticated",
  "error": "authorization_pending"

Meanwhile, the user is going to get a push notification on the mobile device we registered earlier on. The notification shows the text we specified as the binding message in the CIBA request. On an iOS device, the user is prompted to approve the request with Touch ID:

Once the user has approved the request, the next time we poll the authorization server, we’ll get a positive response with the access and id tokens:

  "access_token": "-gzZ6gLFTmZMShoKDCWhDMvJ__E",
  "id_token": "eyJ0eXAiOiJKV1QiLCJraWQiOiJ3VTNpZklJYUxPVUFSZVJCL0ZHNmVNMVAxUU09IiwiYWxnIjoiUlMyNTYifQ.eyJhdF9oYXNoIjoiS01zcm9yNUFhQm5od1hYMnB4VFdndyIsInN1YiI6ImphbmUuZG9lIiwiYXVkaXRUcmFja2luZ0lkIjoiYzQ4NDI1MzEtMzE2ZC00OTcxLWFhZTUtOTU3N2UxNmNkOWVhLTI2OTgxIiwiaXNzIjoiaHR0cDovL2xvZ2luLnBzZDJhY2NlbGVyYXRvcnMuZnJpZGFtLmFlZXQtZm9yZ2Vyb2NrLmNvbTo4MC9vYXV0aDIvcmVhbG1zL3Jvb3QvcmVhbG1zL29ubGluZSIsInRva2VuTmFtZSI6ImlkX3Rva2VuIiwiYXVkIjoiY2liYXRlc3QiLCJhenAiOiJjaWJhdGVzdCIsImF1dGhfdGltZSI6MTU3MTA0NzM0NiwicmVhbG0iOiIvb25saW5lIiwiZXhwIjoxNTcxMDUwOTQ3LCJ0b2tlblR5cGUiOiJKV1RUb2tlbiIsImlhdCI6MTU3MTA0NzM0N30.cIogRAW07uLwNa_IhbhMUUoknIIFKvPtKFrZRHdWiesWEkkQGu7f3KSxrNli7gxo00eQSQZeLdCCOEjPObpqPSuI1LkdndEhVm1yJwCOFTP-uhBp-z7vTgbanIIsK5RydhAqbzgRpGL11DAiqMCR6D-kOJozdrBCxsX2q3jrDOjDrYbSzMxiCKr-13ZHe2F8s8plkPRnFJvpyyprtaMuZjZtTSDzXrHLIQ4QIIW7MR1zaq40T9QIi3QqDVXVQdeBE-6V7IiGCry926cbbf3ek63Czqw0CJUfoYYZCVI08OKoyVZcPZUsbb9Fw6TpI6RJskkL0zs9zNSxrqPkVVirzQ",
  "token_type": "Bearer",
  "expires_in": 3598

We can use the ID token to validate the identity of the end user who verified the transaction; the sub-value should match the userid we specified in the login_hint within the original CIBA request. For example, the above ID token decodes as follows:

  "at_hash": "KMsror5AaBnhwXX2pxTWgw",
  "sub": "jane.doe",
  "auditTrackingId": "c4842531-316d-4971-aae5-9577e16cd9ea-26981",
  "iss": "",
  "tokenName": "id_token",
  "aud": "cibatest",
  "azp": "cibatest",
  "auth_time": 1571047346,
  "realm": "/online",
  "exp": 1571050947,
  "tokenType": "JWTToken",
  "iat": 1571047347

If we need more information, we can either configure AM to include it in the OIDC token (for example, by a custom OIDC claims script in AM, or request additional scopes in the CIBA request and fetch this information via a follow up call to AM (using the access token).

What just happened?

We just authenticated a payment request, using a digital signature to provide a dynamic link between the transaction details and the authentication code.

In detail:

  • We delivered the transaction details to a trusted device (i.e. the device with the registered ForgeRock authenticator app).
  • We know that the transaction details were displayed intact to Jane Doe (because we know and trust the app).
  • The app ensured that Jane authenticated herself with two of the elements required for strong customer authentication under PSD2: 1. Possession: we have proved that she is in possession of the registered mobile device, because the unique secret key only exists on that device. 2. Inherence (through Touch ID) or Knowledge (if there was fallback from Touch ID to device PIN)The transaction request was signed within the authenticator app, and the signature delivered back to AM verified the signature against Jane’s registered key, then returned an OIDC token back to the calling application.How do we know that the authenticator secret key is protected according to these requirements? Could Jane be using a cloned or third party app instead of the authenticator app we trust? If we don’t know for sure that we are using our own authenticator app, then we can’t be sure that the signing key is protected appropriately if at all. This is where push comes in. We can be assured that Jane is using our trusted authenticator app because of the push notification process. The challenge (i.e. the transaction detail) is sent to Jane’s mobile device via push notification, which can only be delivered by the app’s publisher to the publisher’s app. A third party or rogue app is incapable of receiving the push. In this way, we are effectively providing remote attestation of the app in use.

Could we do better?

Yes! In real life, we would be embedding the verification process within our own app, rather than using a standalone authenticator. Along the way, we can make some improvements to how the process works.Firstly, we can use public key based cryptography for enhanced security. The default ForgeRock Authenticator app uses symmetric encryption—that is, there is a shared secret which is stored within the app, with a copy in the user store on the backend. AM uses the copy of the secret to verify the signature.As an alternative, we could have the app generate its own private/public key pair, and deliver the public key to AM during registration. In this way, the private key used for authentication and digital signature never leaves the mobile device, which makes the security model simpler (for example, we don’t have to argue about the security of the secret key on the backend).Secondly, and more importantly, we can tighten up the linking between the digital signature and the transaction details. With the standard ForgeRock Authenticator app, AM sends the binding message (for example, the transaction details) to the mobile device, along with a random challenge. When the user acknowledges the message on the device, the app signs the challenge with the authenticator shared secret, and returns the signed challenge to AM.We could implement closer coupling if the challenge is the transaction message itself (or a hash of the same), rather than a random value sent along with the transaction message. In this case, the authenticator app receives the transaction message from AM, and returns a signed hash of the message. AM then verifies the incoming hash against the original binding message, and checks the signature of the hash against the user’s registered public key.Could we go even further? Yes!We could have AM sign the outbound request with its own key, so that the authenticator app knows that nothing bad has happened to the message before displaying it to the user for verification. This would require a mechanism for delivering trusted keys to the app—perhaps via fixed JWKS URI. We could also encrypt messages end-to-end by encrypting the verification message before sending to the authenticator app, and have the app decrypt the message on the device. Then we don’t have to argue about the privacy controls within each provider’s push notification infrastructure.

What if push is not an option?

Push is not for everyone.In any user community, there will be users who:

  • Don’t have a mobile device
  • Don’t want to use a mobile device
  • Don’t want to use your app
  • Have run out of battery
  • Have lost their mobile device
  • Are not always connected to a data network

There are alternatives to push notifications. For example, some users can be provisioned with a hardware based challenge response devices (such as, OATHOCRA) if you want to stay with open standards). These may be expensive and difficult to distribute, but would typically only be for a minority of users. Voice and SMS are also popular options, although it is much harder to demonstrate full dynamic linking for such channels.


As mentioned at the outset, there are several ways to implement dynamic linking under PSD2. We have looked at one way to support dynamic linking using tools that are available in the public domain.

The intention is to demonstrate a method to deliver authentication codes which are unambiguously linked to the transactions being secured, in line with the PSD2 technical security requirements. Full compliance is of course subject to local regulation — the idea here is to present the case for an open standards based approach to dynamic linking using CIBA as the delivery channel.

Helpful Links

AM 7.2 > Supported > Standards

1 Like