Dissect the PKCE Authorization Code Grant Flow on iOS

Learn how to use Proof Key for Code Exchange (PKCE) authentication flow to access APIs with your Swift iOS apps. By Alessandro Di Nepi.

Login to leave a rating/review
Download materials
Save for later
Share

Learn how to use Proof Key for Code Exchange (PKCE) authentication flow to access APIs with your Swift iOS apps.

Proof Key for Code Exchange (PKCE) is an addition to the OAuth authorization framework that protects the authorization flow from security attacks. As data owners adopt the protocol, it’s mandatory for applications using their APIs to authorize access using this new protocol.

In this tutorial, you’ll build an app called MyGoogleInfo. The app authenticates users with Google using the OAuth authorization flow with PKCE, and it uses the Google API to retrieve users’ profile names and pictures.

Here’s what you’ll learn:

  • The OAuth 2.0 authorization code grant flow details and its vulnerability.
  • What the PKCE authorization flow is and how it strengthens the OAuth flow.
  • How to configure access to the Google API on the Google Cloud console.
  • How to implement the PKCE authorization flow in Swift to authenticate the user.
  • How to use the provided token to access the Google API.

If you have ever wondered how the authentication protocol works or if you’re thinking about using an API from one of the prominent providers for your next project, stay tuned. You’ll get all the details in this article.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Open MyGoogleInfo.xcodeproj in the starter folder. Build and run. The Login screen will look like this.

MyGoogleInfo Login screen.

The Login button doesn’t do anything yet. You’ll implement the PKCE flow with the Google OAuth service in PKCEAuthenticationService.

Once that’s done, when the user logs in, MyGoogleInfo presents the user’s profile information.

MyGoogleInfo Profile screen.

Introducing the OAuth 2.0 Authorization Framework

The OAuth 2 Authorization framework is the standard protocol used for client authentication. The main idea behind OAuth authorization is the separation of roles. Specifically, the standard defines a protocol to allow data owners to delegate clients to access their data, without giving them their credentials.

Here are some terms to know:

  • Resource Owner: This is the entity that owns the resources your app would like to access. Typically, this is you, holding your data.
  • Client: The application that wants to access the data on the resource server, such as MyGoogleInfo in this case.
  • Authorization server: The server in charge of authenticating the user and issuing the tokens to the client.
  • Resource Server: The server hosting the data to access. An access token protects the access to the resource server.

Authorization Code Grant Flow

This diagram represents the OAuth 2.0 Authorization code grant flow that mobile applications implement:

OAuth 2.0 Authorization Flow.

  1. The user starts the login flow by tapping the MyGoogleInfo Login button.
  2. Consequently, the app asks the authorization server to identify the user and ask their consent to access the data. The request includes a client_id so that the server can identify the app requesting the access.
  3. So, the authorization server redirects the user to its login screen (e.g. Google) and asks the user’s consent to give the app access to the API.
  4. The user logs in and approves the request.
  5. If the user approves the access, the authorization server returns a grant code to the client.
  6. The client requests a token to the authorization server, passing its client_id and the received grant code.
  7. In response, the authorization server emits a token after verifying the client_id and the grant code.
  8. Finally, the client accesses the data to the resource server, authenticating its requests with the token.

For all the details on this flow and the other ones defined in the standard, consult the RFC 6749: The OAuth 2.0 Authorization Framework (RFC 6749).

Attacking the Authorization Code Grant Flow

Although the authorization code grant flow is the way to go for mobile apps, it’s subject to client impersonation attacks. A malicious app can impersonate a legitimate client and receive a valid authentication token to access the user data.

For the flow diagram above, to receive a token the attacker should know these two parameters:

  • The app’s client_id.
  • The code received in the callback URL from the authorization token.

Under certain circumstances, a malicious app can recover both. The app’s client ID is usually hardcoded, for example, and an attacker could find it by reverse-engineering the app. Or, by registering the malicious app as a legitimate invoker of the callback URL, the attacker can also sniff the callback URL.

Once the attacker knows the client ID and the grant code, they can request a token to the token endpoint. From that point forward, they use the access token to retrieve data illegally.

Introducing PKCE

Proof Key for Code Exchange (PKCE) is an addition to the Authorization Code Grant flow to mitigate the attack depicted above. In practice, it adds a code to each request that’s dynamically generated by the client so an attacker can’t guess or intercept it.

The following diagram depicts how PKCE strengthens the Authorization Code Grant flow in practice:

OAuth Authorization Flow with PKCE.

That is to say, PKCE introduces the following changes with respect to the plain flow:

  • [1] This is where the login flow begins.
  • [2] On each login request, the client generates a random code (code_verifier) and derives a code_challenge from it.
  • [3] When starting the flow, the client includes the code_challenge in the request to the authorization server. On receiving the authorization request, the authorization server saves this code for later verification.
  • [7] The client sends the code_verifier when requesting an access token.
  • [8] Therefore, the authorization server verifies that code_verifier matches code_challenge. If these two codes match, the server knows the client is legit and emits the token.

With reference to the previous attack scenario, even if the attacker can intercept the authorization grant code and the code code_challenge, it’s way more difficult — if not impossible — to intercept the code_verifier.

PKCE is secure, and it’s the best way to implement OAuth authorization flow in mobile apps.

You can find all the PKCE details at the RFC 7636 – Proof Key for Code Exchange by OAuth Public Clients.

Now, you’ll look at the code verifier/challenge generation and how to transmit the PKCE parameters with the HTTP requests.

Generating Code Verifier and Challenge

The standard itself specifies how to generate the code_verifier and code_challenge.

Open PKCECodeGenerator.swift and replace the body of generateCodeVerifier() with:

// 1
var buffer = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
// 2
return Data(buffer).base64URLEncodedString()

This generates the code_verifier as follows:

  1. Get a 32-byte random sequence.
  2. Pass the 32 bytes sequence to base64 URL encoder to generate a 43 octet URL safe string.

Now, replace the body of generateCodeChallenge(codeVerifier:) with:

guard let data = codeVerifier.data(using: .utf8) else { return nil }

let dataHash = SHA256.hash(data: data)
return Data(dataHash).base64URLEncodedString()

This derives the code_challenge as the SHA256 hash of the code verifier and then base64 URL encodes it.

Generating HTTP Requests

In addition, the standard specifies two different endpoints on the Authorization server for the two authorization phases.

Open PKCERequestBuilder.swift and note the properties for each of these endpoints at the top:

  • Authorization endpoint at /authorize is in charge of emitting the authorization code grant.
  • Token endpoint at /token-generation, to emit and refresh tokens.

According to the RFC, the client should communicate with these two endpoints with two different HTTP request types:

  • Using a GET with all the required parameters passed as URL parameters, for the authorization endpoint.
  • Sending a POST with the parameters passed in the request’s body, encoded as URL parameters, for the token endpoint.

PKCERequestBuilder already contains everything you need to generate the two requests.

  • createAuthorizationRequestURL(codeChallenge:) generates a URL with the required parameters.
  • createTokenExchangeURLRequest(code:codeVerifier:) generates a URLRequest for the token exchange.

Preparing Server Side (Google Cloud Platform)

Before proceeding with the client implementation, you have to set up the backend service.

This setup process allows you to register your application and its redirection URI used throughout the authorization flow and receive the clientID.

In this specific example, since Google already offers a service for user authentication with OAuth, you can use their service.

The service setup process consists of the following steps:

  • Creating a new project.
  • Enabling the specific APIs your app intend to use.
  • Generating the authorization credentials for the app (the client ID).

You’ll need a Google account to register an app.

Creating a New Project

First, open the Google API Console and click Create Project.

Project creation screen.

If you’ve previously created a project, you might need to click the name of the project in the blue bar to bring up a dialog with a New Project button.

Screenshot showing how to create a new project when there is an existing one

You might be asked to enroll in the Google Cloud Developer program. If you’re not already in, don’t worry — it’s as simple as accepting their terms and conditions.

Enter MyGoogleInfo in the project’s name. Then, Google assigns you a client ID that you’ll need once you generate the authorization requests from the app.

Click CREATE.

Project screen.

Enabling the Required API

Now, it’s time to tell Google what kind of API your app will use.

Declaring the required APIs is twofold.

First, it affects the kind of permission Google presents to the user during the authorization phase.

And, more important, it allows Google to enforce the data scope when your app requests data. Each token has a scope that defines which API the token grants access to.

For instance, in the case of MyGoogleInfo, you need to enable the Google People API to allow the app to query the user information.

From the project page, click ENABLE APIS AND SERVICES.

Enable APIs screen.

Then, search for Google People API and click ENABLE.

Google People API screen.

Generating the Authorization Credentials

Finally, you need to create the authorization credentials before you can use the API.

Click Credentials in the sidebar.

If you see a prompt to configure a consent screen, select external user type and fill out the registration form for the required fields. Then, click Credentials in the sidebar again.

Click CREATE CREDENTIALS, then choose OAuth Client ID.

Add credentials menu.

These credentials let you specify which access level and to which API your users’ tokens have access.

Fill in the required fields as shown in the figure below.

Most importantly, the Bundle ID should have the same value as the one set in Xcode for your app. For example, in the example below, it’s com.alessandrodn.MyGoogleInfo. In your case, it’s your app bundle ID.

OAuth client ID screen.

Finally, click CREATE. You should have an OAuth client definition for iOS as in the picture below:

OAuth client screen.

Replace REPLACE_WITH_CLIENTID_FROM_GOOGLE_APP in the definition below with the Client ID from your Google app in PKCERequestBuilder.

PKCERequestBuilder for MyGoogleInfo.

It took a while to prepare, but you’re now ready to implement the PKCE client in Swift!

Implementing PKCE Client in Swift

After all that theory, it’s now time to get your hands dirty in Xcode :]

Authenticating the User

First, implement the first phase of the authorization flow, asking the authorization endpoint to verify the user identity.

Open PKCEAuthenticationService.swift. Add the following code to the end of startAuthentication():

// 1
let codeVerifier = PKCECodeGenerator.generateCodeVerifier()
guard
  let codeChallenge = PKCECodeGenerator.generateCodeChallenge(
    codeVerifier: codeVerifier
  ),
  // 2
  let authenticationURL = requestBuilder.createAuthorizationRequestURL(
    codeChallenge: codeChallenge
  )
else {
  print("[Error] Can't build authentication URL!")
  status = .error(error: .internalError)
  return
}
print("[Debug] Authentication with: \(authenticationURL.absoluteString)")
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
  print("[Error] Bundle Identifier is nil!")
  status = .error(error: .internalError)
  return
}
// 3
let session = ASWebAuthenticationSession(
  url: authenticationURL,
  callbackURLScheme: bundleIdentifier
) { callbackURL, error in
  // 4
  self.handleAuthenticationResponse(
    callbackURL: callbackURL,
    error: error,
    codeVerifier: codeVerifier
  )
}
// 5
session.presentationContextProvider = self
// 6
session.start()

The code above implements the first part of the authorization flow:

  1. Generates the code verifier and derives the code challenge from it.
  2. Prepare the authorization endpoint URL with all the required parameters.
  3. Instantiate ASWebAuthenticationSession to perform the authentication, passing authenticationURL generated before.
  4. In its completion handler, ASWebAuthenticationSession returns the parameters received from the server as callbackURL.
  5. Tell the browser instance that your class is its presentation context provider. So, iOS instantiates the system browser window on top of the app’s main window.
  6. Finally, start the session.

ASWebAuthenticationSession gives you back an optional callback URL and an optional error.

For now, handleAuthenticationResponse(callbackURL:error:codeVerifier:) parses the error and prints the callback URL.

Build and run. Tap the Login button, and you’ll see an alert saying MyGoogleInfo wants to use google.com to sign in.

Dialog asking permission to use google.com to sign in

Tap Continue and you’ll see the Google login screen.

Note the Google request to share the user’s profile information.

MyGoogleInfo login screen

Enter your credentials, authorize the app and check the logs.
Check the app’s log for the callback URL returned from Google with the authorization response parameters.

Received callback URL log.

Parsing the Callback URL

To proceed with the authorization flow, you now need to do two things.

First, in PKCEAuthenticationService.swift, add the function getToken(code:codeVerifier:) as follows.

private func getToken(code: String, codeVerifier: String) async {
  guard let tokenURLRequest = requestBuilder.createTokenExchangeURLRequest(
    code: code,
    codeVerifier: codeVerifier
  ) else {
    print("[Error] Can't build token exchange URL!")
    status = .error(error: .internalError)
    return
  }
  let tokenURLRequestBody = tokenURLRequest.httpBody ?? Data()
  print("[Debug] Get token parameters: \(String(data: tokenURLRequestBody, encoding: .utf8) ?? "")")
  //TODO: make request
}

createTokenExchangeURLRequest() generates the HTTP request, given the grant code and code_verifier.

Note: The function getToken(code:codeVerifier:) is async, as it’ll return immediately and complete the network call in the background. Since you invoke it from a synchronous context, you use a Task.

Then, replace the implementation of handleAuthenticationResponse(callbackURL:error:codeVerifier:) with the following.

if let error = error {
  print("[Error] Authentication failed with: \(error.localizedDescription)")
  status = .error(error: .authenticationFailed)
  return
}
guard let code = extractCodeFromCallbackURL(callbackURL) else {
  status = .error(error: .authenticationFailed)
  return
}
Task {
  await getToken(code: code, codeVerifier: codeVerifier)
}

The code above extracts the code parameter value in the callback URL and passes it to getToken(code:codeVerifier:).

Build and run, then log in with your credentials. Verify the log now contains the parameters for the credential exchange.

Get Token Parameters log.

Getting the Access Token

Finally, you’re ready to get the token.

Replace the //TODO: make request comment in getToken(code:codeVerifier:) with the following:

do {
  // 1
  let (data, response) = try await URLSession.shared.data(for: tokenURLRequest)
  // 2
  guard let response = response as? HTTPURLResponse else {
    print("[Error] HTTP response parsing failed!")
    status = .error(error: .tokenExchangeFailed)
    return
  }
  guard response.isOk else {
    let body = String(data: data, encoding: .utf8) ?? "EMPTY"
    print("[Error] Get token failed with status: \(response.statusCode), body: \(body)")
    status = .error(error: .tokenExchangeFailed)
    return
  }
  print("[Debug] Get token response: \(String(data: data, encoding: .utf8) ?? "EMPTY")")
  // 3
  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromSnakeCase
  let token = try decoder.decode(GoogleToken.self, from: data)
  // TODO: Store the token in the Keychain
  // 4
  status = .authenticated(token: token)
} catch {
  print("[Error] Get token failed with: \(error.localizedDescription)")
  status = .error(error: .tokenExchangeFailed)
}

The function getToken(code:codeVerifier:) performs the following actions:

  1. Use the tokenURLRequest to start the token exchange session with the token endpoint. As a result, it receives back a URLResponse and an optional Data.
  2. Parse the server response status.
  3. If there are no errors, decode the result as a GoogleToken.
  4. Finally, set the status to authenticated, including the access token as a parameter.

Now you’re ready to start querying data. :]

Once you get the token, you can start using it to access the API.

The code in ViewModel listens to the authentication service status and passes the token to the GoogleProfileInfoService. Then, the profile info service uses the token to access your profile information.

Build and run. Log in one last time. Finally, you can see your Google profile information.

MyGoogleInfo showing user profile.

You can also see in the logs the token response from the server:

Get token response log.

Storing the Token

So far, you didn’t save the access token in persistent storage. In other words, every time the app starts, the user needs to log in again.

To make the user experience flawless, the app should do two more things.
First, it should save both the access and the refresh tokens in persistent storage, as soon as they’re received from the server.
Second, it should restore the token from the persistent storage when the app starts.

Since the tokens contain credential access, you should avoid UserDefaults and use the Apple keychain.

Refreshing the Token

The access token and the refresh token have a limited timeframe. In other words, they have a time expiration date enforced on the server.

Once the access token expires, your API calls will fail with error 401. In these cases, you need to trigger the token refresh flow with the token endpoint. The HTTP request body contains the client ID and the refresh token encoded as URL parameters.

For a reference, createRefreshTokenURLRequest(refreshToken:) in the final project generates the URLRequest for the token refresh.

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

You dug into the details of the OAuth Authorization flow with PKCE, and now you’re ready to implement the authentication service for your next app. You can also experiment with things like token management and better error handling.

For all the details on how to store and retrieve the token in the keychain, check out How To Secure iOS User Data: Keychain Services and Biometrics with SwiftUI.

On the other hand, if you want to adopt one of the SDKs available for OAuth, you now know how PKCE works under the hood to totally control the package behavior.

For reference, here are some third-party SDKs that implement OAuth with PKCE.

  • AppAuth is an open-source SDK from the OpenID consortium; it supports native apps (iOS and Android) and all the other Apple OSs and Native JS.
  • Auth0 offers a complete solution for OpenID authentication and, as a part of it, they provide an SDK that supports both iOS and macOS.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!