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?
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:
- Use the
tokenURLRequest
to start the token exchange session with the token endpoint. As a result, it receives back aURLResponse
and an optionalData
. - Parse the server response status.
- If there are no errors, decode the result as a
GoogleToken
. - 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.
You can also see in the logs the token response from the server:
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!