“CIBA”, a new authentication/authorization technology in 2019, explained by an implementer

Introduction

This article explains “OpenID Connect Client Initiated Backchannel Authentication Flow - Core 1.0”, a.k.a., “CIBA Core”.

CIBA Core is a new specification written by MODRNA Working Group of OpenID Foundation. The public review period for the specification started on Dec. 14, 2018 (announce) and it was approved on Feb. 4, 2019 (announce).

Simply put, the specification adds three authorization flows. What should be noted is that the traditional authorization code flow and implicit flow are categorized as “redirect flow”, while CIBA flows are categorized as “decoupled flow”.

In CIBA, (a) a device (“consumption device”) hosting a client application that calls APIs exposed by resource servers and (b) a device (“authentication device”) on which end-user authentication and consent confirmation are performed are decoupled. This enables to cover new use cases. The point is that the client application is not under the control of the end-user and the two devices can be physically separated.

Before the public review period started, I posted some issues for discussion from an implementer’s point of view and I think that the specification is now written well enough for implementers although it may still have room for improvement. As a matter of fact, I could implement and add CIBA features to Authlete. In this article, I explain technical aspects of CIBA Core with my experience.

1. CIBA Overview

In the traditional authorization code flow and implicit flow defined in RFC 6749, a client application starts the flow by sending an authorization request to the authorization endpoint of the authorization server via a web browser.

On the other hand, in CIBA flows, a client application directly sends a backchannel authentication request to the backchannel authentication endpoint which is a new endpoint defined by CIBA Core.

In both the traditional flows and CIBA flows, after receiving a request, the authorization server tells the end-user that the client application is requesting authorization and asks the end-user whether to approve or deny the request.

In traditional flows, this is achieved by sending back an HTML representing an authorization page to the web browser from the authorization endpoint. The authorization server receives the decision made by the end-user as an HTTP request generated by the HTML form.

On the other hand, in CIBA flows, the authorization server delegates the tasks of end-user authentication and consent confirmation to an authentication device of the end-user. A smartphone is a typical example of authentication devices. This process is performed on the background after a response is returned from the backchannel authentication endpoint to the client application.

What should be noted in end-user authentication and consent confirmation in CIBA flows is that the client application is not under the control of the end-user and it can be physically separated from the authentication device. For example, CIBA can support a use case where a client application is running on a computer in front of an operator working in a call center in Okinawa, while end-user authentication and consent confirmation are performed on a smartphone at the hand of the end-user who has made the call to the call center from Tokyo.

In the traditional flows, after getting user consent, the authorization server issues an authorization code in the authorization code flow or issues an access token directly in the implicit flow. In either case, because the authorization server cannot communicate with the client application directly, the token needs to be passed to the client application via the web browser. The trick used for the purpose is “HTTP redirect” as illustrated below. This is the reason the traditional flows are categorized as “redirect flow”.

Redirection in Authorization Code Flow
Redirection in Implicit Flow

On the other hand, in CIBA, there are three flows after consent confirmation. They are called POLL mode, PING mode and PUSH mode, respectively. In every flow, an ID token, an access token, and optionally a refresh token are issued.

In POLL mode, after getting a response from the backchannel authentication endpoint, the client repeats token requests (polling) to the token endpoint.

If the process on the authentication device has not finished yet when a token request arrives, the token endpoint returns 400 Bad Request with JSON including "error":"authorization_pending". The client repeats token requests while it receives the authorization_pending error. Note that the token endpoint may return "error":"slow_down" if the interval between token requests is too short.

The client stops token requests either when it receives tokens or when it receives an error other than authorization_pending and slow_down.

In PING mode, after the process on the authentication device is done, the authorization server sends a notification to the client notification endpoint which is configured for the client application. After receiving the notification, the client makes a token request to the token endpoint.

The client notification endpoint and the client application itself can be separated, but the implementation of the client notification endpoint must be able to notify the client application of the notification in some way or other.

The token endpoint returns an ID token, an access token, and optionally a refresh token.

In PUSH mode, after the process on the authentication device is done, the authorization server generates an ID token, an access token, and optionally a refresh token, and then delivers the tokens directly to the client notification endpoint.

As the same as in PING mode, the client notification endpoint and the client application itself can be separated, but the implementation of the client notification endpoint must be able to pass the delivered tokens to the client application in some way or other.

2. CIBA Requests and Responses

This chapter briefly lists up request parameters and response parameters in CIBA flows. Refer to the original text of the specification for detailed usage of the parameters.

Both backchannel authentication requests and token requests must include additional request parameters according to the client authentication method configured for the client application.

Backchannel authentication requests may use a special request parameter named request. See “3.1. Request Object” for details.

In POLL mode and PING mode, token requests are made. The content of token requests is common in both the modes.

POST {TokenEndpoint} HTTP/1.1
Host: {AuthorizationServer}
Content-Type: application/x-www-form-urlencoded
grant_type={GrantType}& // Mandatory
auth_req_id={AuthenticationRequestID} // Mandatory
  • The value of grant_type is urn:openid:params:grant-type:ciba.
  • The value of auth_req_id is the “authentication request ID” issued from the backchannel authentication endpoint.

In POLL mode and PING mode, token requests are made. The content of token requests is common in both the modes.

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Contro: no-store
{
"access_token": "{AccessToken}",
"token_type": "{TokenType}",
"refresh_token": "{RefreshToken}", // Optional
"expires_in": {AccessTokenDurationInSeconds},
"id_token": "{IDToken}"
}
(the same diagram as shown in 1.4)
POST {BackchannelAuthenticationEndpoint} HTTP/1.1
Host: {AuthorizationServer}
Content-Type: application/x-www-form-urlencoded
scope={Scopes}& // Mandatory
acr_values={AuthenticationContextClassReferences}& // Optional
(login_hint_token|id_token_hint|login_hint)={Hint}& // Mandatory
binding_message={Message}& // Optional
user_code={UserCode}& // Conditional
requested_expiry={AuthenticationRequestIdDuration} // Optional
  • As a notification is not sent in POLL mode, client_notification_token does not have to be included.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"auth_req_id": "{AuthenticationRequestID}",
"expires_in": {AccessTokenDurationInSeconds},
"interval": {MinimumPollingIntervalInSeconds}
}
(the same diagram as shown in 1.5)
POST {BackchannelAuthenticationEndpoint} HTTP/1.1
Host: {AuthorizationServer}
Content-Type: application/x-www-form-urlencoded
scope={Scopes}& // Mandatory
client_notification_token={ClientNotificationToken}& // Mandatory
acr_values={AuthenticationContextClassReferences}& // Optional
(login_hint_token|id_token_hint|login_hint)={Hint}& // Mandatory
binding_message={Message}& // Optional
user_code={UserCode}& // Conditional
requested_expiry={AuthenticationRequestIdDuration} // Optional
  • As a notification is sent in PING mode, client_notification_token is mandatory.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"auth_req_id": "{AuthenticationRequestID}",
"expires_in": {AccessTokenDurationInSeconds},
"interval": {MinimumPollingIntervalInSeconds}
}
POST {ClientNotificationEndpoint} HTTP/1.1
Host: {ServerHostingClientNotificationEndpoint}
Authorization: Bearer {ClientNotificationToken}
Content-Type: application/json
{
"auth_req_id": "{AuthenticationRequestID}"
}
(the same diagram as shown in 1.6)
POST {BackchannelAuthenticationEndpoint} HTTP/1.1
Host: {AuthorizationServer}
Content-Type: application/x-www-form-urlencoded
scope={Scopes}& // Mandatory
client_notification_token={ClientNotificationToken}& // Mandatory
acr_values={AuthenticationContextClassReferences}& // Optional
(login_hint_token|id_token_hint|login_hint)={Hint}& // Mandatory
binding_message={Message}& // Optional
user_code={UserCode}& // Conditional
requested_expiry={AuthenticationRequestIdDuration} // Optional
  • As a notification is sent in PUSH mode, client_notification_token is mandatory.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"auth_req_id": "{AuthenticationRequestID}",
"expires_in": {AccessTokenDurationInSeconds}
}
  • As a token request is not made in PUSH mode, interval is not included in backchannel authentication responses.
POST {ClientNotificationEndpoint} HTTP/1.1
Host: {ServerHostingClientNotificationEndpoint}
Authorization: Bearer {ClientNotificationToken}
Content-Type: application/json
{
"auth_req_id": "{AuthenticationRequestID}",
"access_token": "{AccessToken}",
"token_type": "{TokenType}",
"refresh_token": "{RefreshToken}", // Optional
"expires_in": {AccessTokenDurationInSeconds},
"id_token": "{IDToken}"
}
  • In Push mode, a notification includes an ID token, an access token and optionally a refresh token.
  • The ID token in a notification in PUSH mode includes a urn:openid:params:jwt:claim:auth_req_id claim whose value is the authentication request ID (auth_req_id). In addition, if a refresh token is issued together, the ID token includes a urn:openid:params:jwt:claim:rt_hash claim whose value is the hash value of the refresh token. The way to calculate the hash value is the same as the one for access tokens described in “OpenID Connect Core 1.0, 3.2.2.9. Access Token Validation”.

3. CIBA Implementer’s Impressions

Here I pick up some topics that CIBA implementers will be interested in.

What I found burdensome when I read the draft of CIBA Core for the first time was that the specification allows to pack request parameters of a backchannel authentication request into a JWT and use it as a value of the request request parameter, which is similar to the request object of OIDC Core (6. Passing Request Parameters as JWTs).

In OIDC, when a request object is passed, the server decrypts it if it is encrypted and then verifies the signature. First, as both decryption and verification require a key respectively, the server implementation has to look up the keys in registered JWK Sets. This is tiresome. Next, decrypters and verifiers have to be generated in different ways depending on the algorithms. This is tiresome, too. Furthermore, after signature verification, some claims in the payload (such as aud, iss, exp, iat, nbf and jti) must be validated. This is tiresome, too.

However, because the draft of CIBA Core didn’t mention anything about encryption (especially, there were no metadata related to encryption), I posted an issue to ask about encryption of backchannel authentication request JWT.

  • [Issue 105] CIBA: encryption of backchannel authentication request

As a result, the following sentence was added to the specification.

Note that encrypted JWT authentication requests are not supported.

Thanks to this, CIBA implementers don’t have to support encryption of backchannel authentication request JWT.

What I was interested in next was how to handle other request parameters when the request request parameter is used. In OIDC, it does not matter whether request parameters are placed inside the request object or outside it, or even both inside and outside the request object. An exception for compatibility with RFC 6749 is that mandatory parameters such as client_id and response_type must be put outside the JWT even if they are also included in the request object. FAPI also imposes additional requirements such as “shall send all parameters inside the authorization request’s signed request object”. So, that is, the request object in OIDC (and FAPI) has some burdensome rules for implementers.

However, as the backchannel authentication endpoint of CIBA is a new one, it is possible to avoid introducing such known problems. So, I proposed a rule to prohibit use of other request parameters when the request request parameter is used.

  • [Issue 117] CIBA: other request parameters when “request” is present

This proposal was incorporated and the draft was modified accordingly. However, it was still ambiguous, so Joseph Heenan suggested a refined description.

  • [Issue 128] ambiguities in 7.1.1 signed authentication request

The final specification is as follows:

  1. Other request parameters must not be used when the request request parameter is used.
  2. But, as an exception, request parameters related to client authentication must be put outside the JWT. (e.g. client_id, client_secret, client_assertion, client_assertion_type)

The following is the part in the specification which describes the rule.

The signed authentication request JWT is passed as an application/x-www-form-urlencoded HTTP request parameter with the name request. Authentication request parameters MUST NOT be present outside of the JWT, in particular they MUST NOT appear as HTTP request parameters. Additional HTTP request parameters as required by the given client authentication method, however, MUST be included as application/x-www-form-urlencoded parameters (e.g. Mutual TLS client authentication uses client_id while JWT assertion based client authentication uses client_assertion and client_assertion_type).

Thanks to this, implementations for the request request parameter of a backchannel authentication request can become simpler than those for OIDC Core.

In addition, an interesting point is that algorithms for JWT signature are limited to asymmetric ones. As a result, symmetric algorithms such as HS256, HS384 and HS512 are excluded. In the context of OIDC, computation of symmetric keys requires special treatment (OIDC Core 10.1. Signing) unless the implementation stores client secrets in JWK Sets with other keys. Therefore, limiting algorithms to asymmetric ones makes CIBA implementations a bit simpler.

However, above all, what made me relieved most is that a backchannel authentication request does not have the request_uri request parameter (cf. OIDC Core, 6.2.2. Request using the “request_uri” Request Parameter). If the request parameter existed, implementation work would have become harder.

The following is the second paragraph in “CIBA Core, 7.1. Authentication Request”.

The Client MUST authenticate to the Backchannel Authentication Endpoint using the authentication method registered for its client_id, such as the authentication methods from Section 9 of [OpenID.Core] or authentication methods defined by extension in other specifications.

That is, client authentication is required at the backchannel authentication endpoint. Consequently, it is only confidential clients that may use CIBA. Public clients are not allowed to use CIBA.

CIBA Core mentions “OIDC Core, 9. Client Authentication”. Client authentication methods listed in the section are as follows (except none):

  • client_secret_basic
  • client_secret_post
  • client_secret_jwt
  • private_key_jwt

For FAPI, client authentication methods defined in “OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens” should be considered, too.

  • tls_client_auth
  • self_signed_tls_client_auth

Regarding client authentication methods supported by an authorization server, there exists metadata per endpoint. For example, regarding client authentication methods supported at the token endpoint, token_endpoint_auth_methods_supported is the metadata (RFC 8414). Likewise, introspection_endpoint_auth_methods_supported is metadata for the introspection endpoint (RFC 7662) and revocation_endpoint_auth_methods_supported is metadata for the revocation endpoint (RFC 7009).

So, I suggested a new metadata backchannel_authentication_endpoint_auth_methods_supported that represents client authentication methods supported at the backchannel authentication endpoint.

  • [Issue 102] CIBA: Metadata for client auth at backchannel endpoint

However, the metadata was not added to the specification. Why? Let me excerpt a comment by Brian Campbell, an editor of the specification.

There’s some historical weirdness to how client authentication has come to be represented (and named) in metadata. Client registration metadata has only a token_endpoint_auth_method, which despite the name has become the de facto place in the client data model to say how the client will authenticate to the AS when making any direct client -> AS call. The parameter name is a bit unfortunate but I believe it makes sense to have a single (backchannel) authentication method per client. RFC 8414 took a different direction and has revocation_endpoint_auth_methods_supported and introspection_endpoint_auth_methods_supported etc., which I believe was a mistake. I don't see a clear use case for needing or allowing a different set of auth methods for the different endpoints. And having a bunch of _supported parameters for different endpoints seems likely to clutter up the metadata document with a bunch of redundant info.

I’d propose that some text be added into CIBA core that says that, for the backchannel authentication endpoint, the AS supports the same client authentication methods as indicated with token_endpoint_auth_methods_supported. And state that, for a client, the token_endpoint_auth_method is the authentication method registered for its client_id regardless of the endpoint being called.

I agree. From an implementer’s point of view, it is burdensome to prepare metadata for supported client authentication methods per endpoint. To be honest, rather, it is difficult to find a use case where it is really necessary to use a different client authentication method per endpoint. Probably, it is enough to have one metadata for client authentication for all endpoints.

Consequently, it was decided not to add a new metadata that represents client authentication methods supported at the backchannel authentication endpoint. Instead, it was decided to reuse token_endpoint_auth_methods_supported (server side) and token_endpoint_auth_method (client side) for the purpose. Likewise, token_endpoint_auth_signing_alg_values_supported (server side) and token_endpoint_auth_signing_alg (client side) are also reused for JWT signature algorithms of JWT-based client authentication methods (client_secret_jwt and private_key_jwt) at the backchannel authentication endpoint.

By the way, the client_id request parameter is not mandatory in a backchannel authentication request. This is different from the requirement in RFC 6749. If so, how do implementations of the backchannel authentication endpoint find a client identifier?

First, in the case where the client authentication method is client_secret_basic or client_secret_post, it is easy. It’s because a backchannel authentication request directly includes the value of a client identifier.

Next, in the case where the client authentication method is client_secret_jwt or private_key_jwt, a client identifier is embedded as the value of the iss claim in the JWT specified by the client_assertion request parameter.

Finally, in the case where the client authentication is tls_client_auth or self_signed_tls_client_auth, however, the client authentication methods don’t include information about a client identifier. Therefore, the client_id request parameter is necessary when these client authentication methods are used.

The following sentence added to “CIBA Core, 7.1. Authentication Request” was written with the things mentioned above in mind.

When applicable, additional parameters required by the given client authentication method are also included (e.g. JWT assertion based client authentication uses client_assertion and client_assertion_type while Mutual TLS client authentication uses client_id).

A backchannel authentication request may have an acr_values request parameter. This request parameter exists in OIDC Core, too.

The acr_values request parameter includes authentication context class references in preference order one of which the client wants to be satisfied on end-user authentication. In OIDC, an authorization server tries its best to satisfy one of the authentication context classes listed in the acr_values request parameter. However, even if none of them can be satisfied, it is not regarded as an error. It is because authentication context classes are regarded as optional when they are specified by the acr_values request parameter.

In order to make it essential, in other words, if a client wants the authorization server to return an error when none of authentication context classes is satisfied, the client must use other means than acr_values. The means is more complicated than what most people may imagine. A client has to write {“essential":true} at a deep position inside JSON specified by the claims request parameter. The following is an example to mark urn:mace:incommon:iap:silver as essential.

{
"id_token":
{
"acr":
{
"essential": true,
"values": ["urn:mace:incommon:iap:silver"]
}
}
}

You may wonder who does such complicated thing, but “5.2.3. Public Client” of “Financial-grade API — Part 2: Read and Write API Security Profile” has a requirement saying “shall request user authentication at LoA 3 or greater by requesting the acr claim as an essential claim as defined in section 5.5.1.1 of [OIDC];”, so this mechanism must be supported for FAPI Part 2.

Therefore, I posted an issue to ask whether there was a plan to provide means to mark ACRs as essential. Because it is hard to support the claims request parameter (OIDC Core, 5.5. Requesting Claims using the “claims” Request Parameter), I wanted to clarify whether claims needs supporting before starting CIBA implementation.

  • [Issue 103] CIBA: Means to require “acr” as “essential”

The conclusion is that CIBA Core does not provide any means to mark ACRs as essential. As indicated by Brian’s comment, “I think that something complicated like the “claims” request parameter from OIDC Core would be overkill in CIBA.”, the working group tries to keep CIBA Core as simple as possible.

OIDC Core provides means for a client to specify custom claims it wants to be embedded in an ID token. It is the claims request parameter. I posted an issue to ask (1) whether the backchannel authentication request provides the means and (2) whether profile, email, address and phone scopes should be interpreted in the same way as defined in “OIDC Core, 5.4. Requesting Claims using Scope Values”.

  • [Issue 106] CIBA: Means to request claims to be embedded in the issued ID token

The conclusion is that CIBA Core does not provide any means to specify custom scopes and that profile, email, address and phone scopes are interpreted in the same way as defined in OIDC Core.

Those who have experience in implementing a code to process the claims request parameter know how burdensome it is and will be glad to know they don’t have to support the request parameter at the backchannel authentication endpoint.

A backchannel authentication request must include a hint with which the authorization server can identify the target end-user. There exist three request parameters to pass a hint to the authorization server. They are login_hint_token, id_token_hint and login_hint. A backchannel authentication request must include one and only one of the request parameters.

id_token_hint and login_hint can be found in OIDC Core, too. On the other hand, login_hint_token is a new one added by CIBA Core, but its format is not described in CIBA Core at all.

By the way, soon after starting to implement validation for the request parameters which will include the following steps:

  1. When login_hint_token is included, id_token_hint and login_hint must not be included.
  2. When id_token_hint is included, login_hint_token and login_hint must not be included.
  3. When login_hint is included, login_hint_token and id_token_hint must not be included.
  4. One and only one of login_hint_token, id_token_hint and login_hint must be included.

, you will notice that the validation code is of course implementable but it won’t become beautiful.

To achieve a mechanism to require one among multiple options, it is better to have the following request parameters than to define a request parameter per hint type.

  • hint_type — type of the hint
  • hint — value of the hint

For both client-side and server-side implementations, this style is easier to cope with. So, I proposed it.

[Issue 123] CIBA: hint and hint_type

However, the proposal was not adopted. Advantages of this style were admitted to some extent but it was too late to warrant the change at the timing (2 weeks before the start of the public review period).

I didn’t persist as I knew the working group wanted to start the public review period as soon as possible. However, it doesn’t necessarily have to prohibit our product (Authlete) from adopting the style internally. Therefore, the response (BackchannelAuthenticationResponse) returned from Authlete’s /api/backchannel/authentication API (which is used to parse a backchannel authentication request) does not have response parameters such as loginHintToken, idTokenHint and loginHit, but instead it has hintType and hint response parameters.

In addition, I posted other issues from an implementer’s point of view mainly to clarify details necessary for implementations. For example, about the maximum length and valid characters of client_notification_token (Issue 104), whether lack of the urn:openid:params:jwt:claim:rt_hash claim in other modes is intentional although it is included in an ID token issued in PUSH mode (Issue 127). Look over the issue list if you are interested, and you may find something else interesting.

The main reason that CIBA Core was put into the public review period in the middle of Dec., 2018 was not because the working group didn’t have anything more to discuss but because it needed to be aligned to the schedule of financial policies in Europe. However, it is worth releasing the first version of CIBA Core Implementer’s Draft at this timing because differences between CIBA Core and the original specification used as the base are considerably big.

Regarding FAPI, discussion continued actively even after the release of the first version of Implementer’s Draft, and the second version was released at the end of Oct., 2018. Likewise, I guess the second version of CIBA Core Implementer’s Draft will be released sooner or later. As a matter of fact, new issues that may affect the specification have been posted during the public review period. Those who work in this field have to follow discussion in MODRNA Working Group continuously.

Also, FAPI Working Group has resumed discussion about FAPI-CIBA profile after the start of the public review period of CIBA Core. Constraints and additional requirements specific to FAPI are being discussed and the FAPI-CIBA profile also will start its public review period without big delay.

4. Authlete’s CIBA Implementation

To support CIBA, 4 APIs have been added to Authlete.

  1. /api/backchannel/authentication API — parses a backchannel authentication request. (spec: request, response)
  2. /api/backchannel/authentication/issue API — issues an auth_req_id. (spec: request, response)
  3. /api/backchannel/authentication/fail API — generates an error response returned from the backchannel authentication endpoint. (spec: request, response)
  4. /api/backchannel/authentication/complete API — completes the process after end-user authentication and consent confirmation. (spec: request, response)

In addition, the implementation of /api/auth/token API has been updated.

By combining the APIs as illustrated below, you can implement CIBA flows.

By design, Authlete does not manage user data and does not perform end-user authentication (“New Architecture of OAuth 2.0 and OpenID Connect Implementation”). Therefore, the authorization server itself and communication between the server and authentication devices need to be implemented by Authlete users (i.e. by our customers).

java-oauth-server is an open-source sample implementation of an authorization server which uses Authlete as a backend service. The latest implementation of java-oauth-server supports CIBA. Note that, however, the shared Authlete server (api.authlete.com) is old (version 1.1) and does not support FAPI and CIBA. In order to try CIBA, you need to have access to an Authlete server which supports CIBA (version 2.1+) (and access to corresponding web consoles). Please contact sales@authlete.com for details.

Also, we are preparing a website that hosts simulators of consumption device and authentication device. You can use the simulators while developing a CIBA-ready authorization server. In fact, we used the simulators while adding CIBA support to java-oauth-server. I hope we will be able to announce the simulators soon.

Finally

The second Implementer’s Draft of FAPI was approved on Oct. 24, 2018 (announce). It states FAPI consists of Part 1, Part 2 and CIBA. UK Open Banking has decided to adopt CIBA. The financial industry in Australia also is showing big interest in CIBA. “Decoupled Flow” is gaining momentum.

New use cases enabled by CIBA will yield new business opportunities. I wish you can make the most of them. We, Authlete, Inc., also is thinking of exploring new markets this year with the (probably) first commercial CIBA implementation! (new year’s resolution)

Happy New Year 2019!

Takahiko Kawasaki, a programmer, co-founder and representative director of Authlete, Inc.

Co-founder and representative director of Authlete, Inc., working as a software engineer since 1997. https://www.authlete.com/

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store