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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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.