Pablo Cibraro

My notes about software development, security and other stuff.

The state of OAuth 2.0 for Mobile Apps

Overview

Mobile applications implemented these days for a single native platform such as iOS or Android, or multiple platforms with frameworks like Xamarin, React Native or Flutter, all share the same minimum authentication requirements, which include authenticating users, and also passing authentication data in the form of tokens to external web APIs.

In OAuth 2.0, these requirements are addressed with ID tokens, refresh tokens and access tokens. The purpose of each token will be discussed in following sections.

OAuth 2.0 Tokens

ID Tokens

An ID token represents user identity information. An authorization server returns ID tokens after the user is successfully authenticated (using his username and password for example). In the case of Azure AD, ID Tokens are represented as JSON Web Tokens or JWT.

JWTs contain three parts, a header with metadata with information about the algorithm and key used to sign the token, a payload with attributes and a signature to make the payload has not been changed in transit.

The payload for an ID token looks as follow,

 {
   "sub": "MCX7TR7RTB5L3YRYR4FIAKX2IE",
   "aud": "dj0yJmk9NDdXZzBEcmJ6UjJxJmQ9WVdrOVlVWktjR0ZLT...",
   "email_verified": true,
   "iss": "https://azure...",
   "name": "Sample user",
   "exp": 1440569876,
   "locale": "en-US",
   "given_name": "sample",
   "iat": 1440566276,
   "family_name": "user",
   "email": "sample.user@mydomain.com"
}

ID tokens are only valid for user authentication in the mobile application. The aud attribute in the payload specifies the intended audience for the token. This token should not be used to try accessing external web apis.

Refresh token

ID tokens have an expiration, which is represented by the exp attribute in the case of JWT. Once the ID token is expired, user should re-authenticate again in theory by providing his authentication credentials (e.g. username and password). The context of the previous sentence implies there are two workarounds for this scenario.

Silent authentication with the use of a hidden iframe. This reuses the existing authentication session on the server side. An additional parameter prompt=none is passed to the Authorization Server so the existing session is reused and the user is not prompted for his credentials.

A refresh token is used instead. Refresh tokens can be renewed indefinitely and does not require an user session to be active on the server side, which make them ideal for scenarios with mobile applications.

For getting a refresh token with Open ID Connect an additional scope “offline_access” must be passed to the Authorization Server. When this happens, the server will return an ID Token and a Refresh token as well.

For mobile applications, that means the user can be prompted for credentials once, and a refresh token can be used afterwards for renewing ID and Access Tokens until the user explicitly signs out from the application.

Access Tokens

An access token represents a permission to invoke an external web API. It could be an opaque string that only the web API understands and can validate or a something more self-descripting like JWT. It only targets a single API, and JWT is used to represent it, this target is set in the aud attribute. The Web API can validate the aud attribute to make sure the token was issued for it by the authorization server, and not any other web api. In that way, the attack surface is reduced once an attacker gains access to a token.

In the case of Azure AD, an access token can contain one or more scopes, which represent permissions granted by the user on behalf of the client application (for example, an scope for reading the user’s calendar) or roles, which are mapped by Azure AD from AD groups associated to the user. Roles and Scopes can be both used by the Web API to perform authorization.

{
  "aud": "api://myapi",
  "iss": "https://sts.windows.net/tenantid/",
  "iat": 1607435426,
  "nbf": 1607435426,
  "exp": 1607439326,
  "acr": "1",
  "aio": "ATQAy/8RAAAA3L27q98dITMwGbNIBJxxVh7ulCE4rnwBfBT4YsnUUTrcVdn8LivEpzRYI4wg8v2W",
  "amr": [
    "pwd"
  ],
  "appid": "appid",
  "appidacr": "0",
  "family_name": "Test",
  "given_name": "User",
  "name": "Test User",
  "rh": "0.AS0AOme64oK3RE-wtZPakCWCANV2XzQLasFLglhirklcxF8tAOQ.",
  "roles": [
    "Admin"
  ],
  "scp": "read contacts",
  "sub": "jsMTcd0qUKGJRZZ-MBNA3Gaa-iIRoFTEcKV0Ir8sQzQ",
  "tid": "x",
  "unique_name": "test.user@mydomain.com",
  "upn": "test@mydomain.com",
  "uti": "H-XYPjZKVkun3ERouO-XAA",
  "ver": "1.0"
}

OAuth 2.0 Flows

A flow in OAuth represents a process with multiple steps/interactions for obtaining a token. The specification formalizes various flows that can be used in different contexts or scenarios. To understand where a given flow can be used, the specification also formalizes the types of applications, trusted or untrusted. A trusted app is one that runs in a controlled environment, such a web application hosted in a web server owned by the organization. Different security policies and a firewall can be put in place for this. An untrusted app is everything else that runs out of the boundaries of the organization, which could be web apps hosted by a vendor, native desktop apps running in personal laptops and mobile apps.

In this context, mobile applications are consider untrusted applications that also suffer from another main problem, client secrets can not be securely stored in these. The application can be decompiled, which will reveal the client secret that is unique for all the users and devices (This is what happened to Twitter in the past).

To mitigate this issue, the Implicit Flow was considered for many years as the adequate flow for mobile applications. This is not longer true, as the Implicit Flow itself suffers from another issue. It relies on a custom callback from the Authorization Server to retrieve the access token. This callback can be potentially captured in mobile devices by malicious applications or in the case of a web browser, it’s kept in the browser history.

A new extension for the Code Authorization Flow has been introduced, which makes client secrets optional, and the perfect match for Mobile applications and SPA running in a browser. This extension is called Code Authorization Flow with PKCE.

Implicit Grant Flow will be deprecated in the version of OAuth (2.1)

Code Authorization Flow with PKCE

This Code Authorization Flow makes use of a Proof Key for Code Exchange (PKCE). It introduces a secret (Code verifier) created by the client application (mobile app or spa) that can be verified by the Authorization Server.

In addition, this code verifier is transformed by the client in something called Code Challenge and sent to the Authorization Server via HTTPS to obtain an authorization code.

If a malicious attacker intercepts that Authorization Code, it can not use it for exchanging an Access Token without the Code Verifier.

How it works.

The user clicks Login within the application.

The client application creates a cryptographically-random code_verifier and from this generates a code_challenge.

The client application redirects the user to the Authorization Server (/authorize endpoint) along with the code_challenge.

The Authorization Server redirects the user to the login and authorization prompt.

The user authenticates using one of the configured login options and may see a consent page listing the permissions the Authorization Server will give to the application.

The Authorization Server stores the code_challenge and redirects the user back to the application with an authorization code, which is good for one use.

The client application sends this code and the code_verifier (created in step 2) to the Authorization Server (/oauth/token endpoint).

The Authorization Server verifies the code_challenge and code_verifier.

The Authorization Server responds with an ID Token or Access Token (and optionally, a Refresh Token).