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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Dissect the PKCE Authorization Code Grant Flow on iOS
20 mins
- Getting Started
- Introducing the OAuth 2.0 Authorization Framework
- Authorization Code Grant Flow
- Attacking the Authorization Code Grant Flow
- Introducing PKCE
- Generating Code Verifier and Challenge
- Generating HTTP Requests
- Preparing Server Side (Google Cloud Platform)
- Creating a New Project
- Enabling the Required API
- Generating the Authorization Credentials
- Implementing PKCE Client in Swift
- Authenticating the User
- Parsing the Callback URL
- Getting the Access Token
- Storing the Token
- Refreshing the Token
- Where to Go From Here?
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 aURLRequest
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.
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.
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.
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.
Then, search for Google People API and click ENABLE.
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.
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.
Finally, click CREATE. You should have an OAuth client definition for iOS as in the picture below:
Replace REPLACE_WITH_CLIENTID_FROM_GOOGLE_APP
in the definition below with the Client ID from your Google app in PKCERequestBuilder
.
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:
- Generates the code verifier and derives the code challenge from it.
- Prepare the authorization endpoint URL with all the required parameters.
- Instantiate
ASWebAuthenticationSession
to perform the authentication, passingauthenticationURL
generated before. - In its completion handler,
ASWebAuthenticationSession
returns the parameters received from the server as callbackURL. - 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.
- 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.
Tap Continue and you’ll see the Google login screen.
Note the Google request to share the user’s profile information.
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.
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
.
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.