Implementing OAuth with ASWebAuthenticationSession
Learn about what OAuth is and how to implement it using ASWebAuthenticationSession. By Felipe Laso-Marsetti.
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
Implementing OAuth with ASWebAuthenticationSession
30 mins
- Getting Started
- Understanding OAuth
- Authorization Versus Authentication
- OAuth Roles
- The OAuth Flow
- Creating a GitHub App
- User Authorization Settings
- Post Installation Options
- Permissions Settings
- Viewing Your Results
- Connecting GitHub App with ASWebAuthenticationSession
- Updating the Project with GitHub App Values
- Views Overview
- View Models Overview
- Understanding ASWebAuthenticationSession
- Adding ASWebAuthenticationSession
- Checking for Errors
- Setting the Presentation Context Provider
- Starting the Authentication Session
- Handling the Authorization Code
- Displaying the Results
- Understanding Tokens
- Handling Tokens
- How NetworkRequest Handles the Tokens
- Creating Ephemeral Sessions
- Where to Go From Here?
Do you need to authenticate your users against a third-party app? Perhaps your client has requested that you implement such a mechanism using the OAuth standard?
What if you’re working in an enterprise setting and your client uses Active Directory to manage users and Okta or Ping Federate to control how a third-party app interacts with protected resources?
In this tutorial, you’ll create a third-party GitHub app that you can authenticate via the OAuth standard using ASWebAuthenticationSession
and display a list of repositories owned by the user. In the process, you’ll learn about:
- OAuth
- Session tokens
- Ephemeral sessions
Getting Started
In a traditional client app that authenticates against a server, the server stores the username and password to authenticate a user and permit user access. This is fine when no third parties are involved.
But what happens when apps like GitHub want to add support for third-party apps?
That’s where things get tricky and a few shortcomings surface. Without OAuth, several issues can come up:
- To avoid constantly requesting the user’s credentials, the third-party app has to store and manage them somewhere.
- The password authentication method is more vulnerable.
- GitHub cannot restrict, revoke or limit access to third-party apps, as they have the user’s full credentials.
- If the third-party app is compromised, so is the user’s GitHub account.
How can GitHub (the server app) provide you (a third-party app) with access to its protected resources without giving full, unrestricted access? That’s where OAuth comes in.
Understanding OAuth
From the Internet Engineering Task Force’s (IETF) website, here’s the definition of the OAuth 2.0 standard:
“The OAuth 2.0 authorization framework enables a third-party app to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party app to obtain access on its own behalf.”
Your third-party app doesn’t store any user credentials or handle authenticating users directly. That’s something you’re hoping to have GitHub, with its robust infrastructure and security, do for you.
What you want your third-party app to do is allow users to log in with their own GitHub credentials to access specific resources within GitHub.
Another scenario is using GitHub to implement user authentication — without having to spend too much time on the implementation and security details — but not access any GitHub resources outside of validating the user’s identity.
This is why apps and websites often use the Sign in with buttons for a third-party service like Facebook, Apple or Google. They’re leaving it up to those parties to handle the server security and sign-in infrastructure.
Authorization Versus Authentication
OAuth helps by separating the authentication process from the authorization process.
But what exactly is the difference? Aren’t they the same? You might be surprised, after years of using the two terms interchangeably, that they are not.
- Authentication: Who you are.
- Authorization: Which permissions the service has given you.
In this tutorial, GitHub will give your app certain accesses and permissions: authorization. But it’s up to users to validate themselves and verify who they are against GitHub: authentication.
OAuth Roles
So far, you’ve read about what OAuth is and the difference between authentication and authorization. Behind the scenes, OAuth has a few more concepts that are important to learn about. The first is roles.
OAuth defines four roles:
- Resource Owner: Whoever can give access to a protected resource.
- Resource Server: The server that contains the protected resource.
- Client: An app that tries to access protected resources with authorization and on behalf of the resource owner.
- Authorization Server: Gives access tokens to the client upon successful authentication and authorization.
The OAuth Flow
The OAuth standard defines the following as the typical flow of a third-party app. This is what you’ll implement in this tutorial:
- Your app asks for an access token, a short-lived token used in requests against the resource owner. To do so, it must present the authorization server with a client ID and allow the user to authenticate with their credentials.
- The authorization server authenticates the client, then returns an access token and a refresh token for this user, to be used in your app only. This is assuming everything’s valid and went well.
- Your app, the client, makes a request to a protected resource from the resource owner. The client must then present the user’s access token or the request will fail.
- The resource owner validates your user’s access token. If it’s valid, then it returns the requested resource.
- Your app continues to make requests on behalf of the user until the access token expires. When this happens, the next request you make to the resource owner will result in an invalid token error.
- Your app can use the refresh token, often a longer-lived token, to request a new access token from the authorization server. The same scope and restrictions apply as before.
- If your refresh token is still valid, the authorization server will issue a new access token for your app. If the refresh token expired, the user must authenticate with their credentials again.
OAuth is heavily web-based, which means most implementations show some sort of web view to your users to let them enter their credentials. Behind the scenes, there are some redirects and callbacks that take place and make everything work. Don’t worry, this tutorial will help you handle all of that.
There’s been quite a bit of theory so far, but now you have a better understanding of OAuth and how it works.
Creating a GitHub App
You want your app to talk to GitHub. GitHub needs to know where the user is trying to access its resources from — your app — and which resources it should grant access to.
It’s time to create a GitHub app from your account. Here’s how to get started!
On your web browser, open and log in to GitHub. In the top-right corner, click your profile image. Then, click Settings:
Click Developer settings:
Click GitHub Apps. Click the New GitHub App button:
When creating a new app, there are quite a few options and settings. You’ll go through them now.
Name your app AuthHub-
Add the homepage URL for your app. This example uses https://www.raywenderlich.com/. If you’re creating your own app, it should link to your app’s homepage, where users can get more information.
User Authorization Settings
The next set of settings center around user authorization. The first option is the authorization callback URL. This is the page the authentication provider will redirect the users to when they successfully authenticate. The format you specify here is what your app will listen to, to take back control once a user authenticates.
Use the following for the app’s callback URL:
authhub://oauth-callback
This is just a callback you define. You can use a different value if you want, but note that it will change the setup process when working with ASWebAuthenticationSession. For now, it’s easiest to use the value above, so you can follow along.
The next option is a checkbox asking whether to let a user’s authorization tokens expire. Enable this checkbox because you want to acquire a refresh token and opt into expiring access tokens for a better security model.
Finally, there’s an option to request user authorization during installation, which can remain unchecked because you don’t need it for your app:
Post Installation Options
Moving down the page, there are post installation options and web hooks, which you can leave blank:
Be sure that the Active checkbox under Webhook is unchecked!
Permissions Settings
The next section relates to permissions. This is where you specify what information your app should be able to access.
In the Contents settings, change the access option to Read-only:
Finally, there’s a setting for where to install your app. For this example, select Only on this account:
Congratulations, you’ve configured your app. Now, click Create GitHub App and you’ll see your app’s details page.
Viewing Your Results
You’ve created an app in GitHub with its own app ID and client ID. Now you can set up a client — in this case, the AuthHub iOS app — to connect and use your GitHub app. If you had three different apps — perhaps iOS, Android and web — that connect to your GitHub app, you’d want to generate three different client secrets.
If you see a yellow banner at the top of the page telling you that you must generate a private key, click the link to do just that.
Next, click the Generate a new client secret button. Copy the alphanumeric value shown and paste it somewhere permanent because GitHub will never let you view this information again.
Be very careful about how you store your client secret and make sure you don’t share it.
You’re all done on the GitHub side. It’s time to write some Swift code!
Connecting GitHub App with ASWebAuthenticationSession
Download the project materials by clicking the Download Materials button at the top or bottom of the tutorial. Open the starter project in Xcode. Build and run.
Right now, tapping Sign In doesn’t do anything. You’ll need to implement the sign in feature in the project. Prior to doing that, you’ll explore the starter project.
You have a basic project with two screens: one to sign in and one to view your repositories. Also, you have models for User
, Repository
and NetworkRequest
.
The most interesting model here is NetworkRequest
. It acts as a wrapper around URLSession
.
Open NetworkRequest.swift. You’ll find enumerations that encapsulate the supported HTTP methods and errors for your network layer. The more interesting enum
here is RequestType
, which lists cases for the supported requests the app can make to GitHub. In the enumeration, you also have helper methods to construct a NetworkRequest
for a specific request type. Pretty handy!
For more information, check out GitHub’s documentation on its REST API.
Updating the Project with GitHub App Values
To let GitHub know about your app, you’ll add your GitHub app’s information to the project.
Open NetworkRequest.swift. Under // MARK: Private Constants
, you’ll find three static constants:
static let callbackURLScheme = "YOUR_CALLBACK_SCHEME_HERE"
static let clientID = "YOUR_CLIENT_ID_HERE"
static let clientSecret = "YOUR_CLIENT_SECRET_HERE"
Replace these values with the values from your newly created GitHub app.
The callback URL scheme doesn’t need to be the entire URL you entered when creating your GitHub app, it just needs to be the scheme. For this example, use authhub as the string for callbackURLScheme
.
Moving on, start(responseType:completionHandler:)
within NetworkRequest
is where the actual network request goes out. Here, you define some parameters for your URL request along with the authorization HTTP header, should your app have an access token available.
The GitHub API expects you to send the access token for requests that require authorization via the Authorization HTTP header. The value of this header will be in the format of:
Bearer YOUR_TOKEN_HERE
In addition, the method handles any errors with the completion handler and parses JSON data into a native Swift type specified in the parameters.
So far, so good!
Views Overview
Next, you have the views. This is a fairly simple app, so there are only two views necessary: SignInView.swift and RepositoriesView.swift. Not much to worry about here, the fun stuff happens in the view models.
Finally, you have the view models.
View Models Overview
Open RepositoriesViewModel.swift. This is where you’ll find code that requests a list of the logged-in user’s repositories from GitHub and provides them to the view to display in a list.
The other view model in the app is in SignInViewModel.swift. This is where you want to add your ASWebAuthenticationSession
. You’ll work on that now.
Understanding ASWebAuthenticationSession
ASWebAuthenticationSession
is an API that’s part of Apple’s Authentication Services framework and can be used to authenticate a user through a web service.
You create an instance of this class in order to acquire an out-of-the-box solution where you can point your app to an authentication page, allow the user to authenticate, and then receive a callback in your application with the user’s authentication token.
The cool thing about this API is that it’s going to adapt to the native platform it runs on. For iOS this mean an embedded, secure browser, and on macOS your default browser (if it supports web authentication sessions) or Safari.
With just a few parameters, you are up and running against your own (or third-party) authentication services, without having to implement everything from scratch using a web view.
Adding ASWebAuthenticationSession
The way ASWebAuthenticationSession
works is that it expects the authentication URL (with all of the required parameters from the authentication provider), your app’s callback URL scheme (in order to get back to your app upon successful login), and a completion handler for you to handle and manage the authentication token.
Open SignInViewModel.swift. Add the following code to signInTapped()
:
guard let signInURL =
NetworkRequest.RequestType.signIn.networkRequest()?.url
else {
print("Could not create the sign in URL .")
return
}
You need the URL used to sign in, so you use the RequestType
to acquire it. Should the process fail for some reason, an error prints to the console and the method returns without doing anything.
Next, in the same method, add the following:
let callbackURLScheme = NetworkRequest.callbackURLScheme
let authenticationSession = ASWebAuthenticationSession(
url: signInURL,
callbackURLScheme: callbackURLScheme) { [weak self] callbackURL, error in
// Code will be added here next! :)
}
Here, you first create a constant to store your callback URL scheme, then proceed to create a new ASWebAuthenticationSession
. The session initializer expects the sign-in URL and callback scheme as well as a completion handler as parameters.
The callback URL scheme is the one you just replaced inside NetworkRequest
, but what about the sign-in URL?
Open NetworkRequest.swift. Look at the .signIn
case inside url()
. Here, you see the host, path and parameters needed to make a successful sign-in request. Of note is client_id
, which you added in this file a little while ago.
Checking for Errors
Open SignInViewModel.swift, replace // Code will be added here next! :)
with:
// 1
guard
error == nil,
let callbackURL = callbackURL,
// 2
let queryItems = URLComponents(string: callbackURL.absoluteString)?
.queryItems,
// 3
let code = queryItems.first(where: { $0.name == "code" })?.value,
// 4
let networkRequest =
NetworkRequest.RequestType.codeExchange(code: code).networkRequest()
else {
// 5
print("An error occurred when attempting to sign in.")
return
}
This is quite a big guard statement, but necessary nonetheless. Here’s what’s going on:
- You check for errors and confirm there’s a valid callback URL.
- From there, you acquire the URL’s query items by extracting the components of the callback URL. The query items will help you check whether this response has the authorization code you need to exchange for tokens.
- When the callback URL loads, it includes the authorization code as a query parameter.
- Next, acquire a
NetworkRequest
for the code exchange. - Should any of these checks fail, you print an error and return from this method.
Build and run.
Tap the Sign In button. And check out the results… Nothing?!
Setting the Presentation Context Provider
There are two more things you need to do before your authentication session can work. The first is to set a presentation context provider.
To do this, you’ll first need to implement the necessary protocol. Do that now by adding the following extension at the end of SignInViewModel.swift:
extension SignInViewModel: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession)
-> ASPresentationAnchor {
let window = UIApplication.shared.windows.first { $0.isKeyWindow }
return window ?? ASPresentationAnchor()
}
}
Here, you implement ASWebAuthenticationPresentationContextProviding
to tell your authentication session how to present itself. Behind the scenes, ASWebAuthenticationSession
works with a browser, cookies, sessions and so on to show your users a login screen then redirect them to your app.
Now, add the following to the end of signInTapped()
:
authenticationSession.presentationContextProvider = self
You set the location for the authorization view. This takes care of the presentation portion.
Starting the Authentication Session
The second thing you need to do before this works is to start the authentication session.
In SignInViewModel
, add the following code to signInTapped()
:
if !authenticationSession.start() {
print("Failed to start ASWebAuthenticationSession")
}
This verifies whether the session was able to start. If not, then another error will print to the console.
Build and run.
Tap the Sign In button. You’ll may see an alert if you’re on an iOS version prior to 12.4 (this is part of the authentication session API, indicating that your app wants to use GitHub to sign in).
Tap Continue.
You’ll see a modal controller with the GitHub login page. Upon successful login, the modal dismisses and, once again, nothing happens.
But this is good! No, really, it is.
If you enter invalid credentials, you’ll see the authentication error within the GitHub page itself, but if the modal dismisses and no errors print in the console. That’s because GitHub responded to your app with the authorization code.
Handling the Authorization Code
At this point, you need to exchange the authorization code for the access and refresh tokens.
Open SignInViewModel.swift. Inside signInTapped()
, add the following code at the end of authenticationSession
‘s completion handler:
self?.isLoading = true
networkRequest.start(responseType: String.self) { result in
switch result {
case .success:
self?.getUser()
case .failure(let error):
print("Failed to exchange access code for tokens: \(error)")
}
}
While this takes place, you tell the view that something is loading, which replaces the Sign In button with an activity view. This prevents your users from going through this flow again while you’re in the middle of working on the existing session. You use the network request that you acquired earlier as part of the guard statement to perform the token exchange.
NetworkRequest
‘s start(responseType:completionHandler:)
also has a completion handler. Inside it, you check the request result for success or failure. If it succeeds, you proceed to call getUser()
. If it fails, you print an error to the console.
Displaying the Results
Before running the app again, add the following code to getUser()
:
isLoading = true
NetworkRequest
.RequestType
.getUser
.networkRequest()?
.start(responseType: User.self) { [weak self] result in
switch result {
case .success:
self?.isShowingRepositoriesView = true
case .failure(let error):
print("Failed to get user, or there is no valid/active session: \(error)")
}
self?.isLoading = false
}
This method is similar to what you did with NetworkRequest
, except this gets the signed-in user’s information. If the request succeeds, you’ll set a Boolean to true
that tells the view to show the repositories. Otherwise, you print an error to the console.
Regardless of the outcome, you tell the view that loading has finished.
Build and run. Then, sign in like before.
Two unexpected things happen:
- The modal just shows and dismisses without giving you a chance to input your GitHub credentials
- An error message displays in the Xcode console.
Because ASWebAuthenticationSession
works with web views, cookies and a web session behind the scenes, it has cached a state that results in getting an authorization code automatically. Note that this happens for an indeterminate amount of time, not forever, but is still not something you want.
You’ll handle this later in the Creating Ephemeral Sessions section. For now, focus on the error about a failure during the token exchange.
Before you can solve it, you need a bit of theory about the tokens themselves.
Understanding Tokens
Upon login, users can now acquire an authorization code. This, however, is not the final stopping point. This access code has a very short duration and can usually be used just once. Its purpose is for you to exchange it for an access token and a refresh token.
As you saw earlier, the access token is the one you’ll send with every request to authorize your requests. The refresh token is usually longer-lived. You can use it to acquire a new access token when it expires.
Handling Tokens
With the authorization code in hand, it’s time to exchange it for tokens. You’ll also add logic in your app to handle them properly.
Open NetworkRequest.swift.
Inside start(responseType:completionHandler:)
and right below the following code block:
guard
error == nil,
let data = data
else {
DispatchQueue.main.async {
let error = error ?? NetworkRequest.RequestError.otherError
completionHandler(.failure(error))
}
return
}
Find the line:
if let object = try? JSONDecoder().decode(responseType, from: data) {
Replace this line with the following code:
// 1
if T.self == String.self,
let responseString = String(data: data, encoding: .utf8) {
// 2
let components = responseString.components(separatedBy: "&")
var dictionary: [String: String] = [:]
// 3
for component in components {
let itemComponents = component.components(separatedBy: "=")
if let key = itemComponents.first,
let value = itemComponents.last {
dictionary[key] = value
}
}
// 4
DispatchQueue.main.async {
// 5
NetworkRequest.accessToken = dictionary["access_token"]
NetworkRequest.refreshToken = dictionary["refresh_token"]
completionHandler(.success((response, "Success" as! T)))
}
return
} else if let object = try? JSONDecoder().decode(T.self, from: data) {
Whereas the other GitHub API responses are JSON formatted, the token exchange response comes back as a string. Here’s how you handle that case:
- By adding this statement, you first see if there’s a string response as its contents.
- This response string, with your tokens, is made of key-value pairs separated by the ampersand, which is why you break that string into an array of key-value pairs.
- You loop through each of the components to acquire a Swift dictionary of the response.
- Right now, everything is running in a background thread due to
URLSession
‘s default threading model. Therefore, you make a call toDispatchQueue
to call the next code on the main thread. You need to do this because the completion handler will be updating the UI, which can only be done on the main thread. - The code within the block stores the access and refresh tokens within two helper properties of
NetworkRequest
. It then proceeds to call the completion handler, indicating that the process was successful.
How NetworkRequest Handles the Tokens
To see what NetworkRequest
does with the tokens, switch over to NetworkRequest+User.swift.
These are properties of type String
, but behind the scenes, they read and write the tokens to UserDefaults
so they can persist across app launches.
These tokens are sensitive data, so in your apps, you want to ensure you securely store these in the keychain. This is an interesting topic which you can learn more about here in How To Secure iOS User Data: Keychain Services and Biometrics with SwiftUI.
Build and run. Sign in. While the modal may flash on screen again, you’ll now see the list of your repositories afterward:
Fantastic!
Creating Ephemeral Sessions
The last topic to cover is ephemeral sessions, which are private authentication sessions. Ephemeral sessions don’t cache session-related data to disk but to RAM. Upon session invalidation, the ephemeral sessions’ data clears the session data. This conveniently gives users the added privacy and security.
Open. SignInViewModel.swift. In signInTapped
and below:
authenticationSession.presentationContextProvider = self
Add the following code:
authenticationSession.prefersEphemeralWebBrowserSession = true
This mitigates the issue encountered earlier, where attempting to sign in immediately after running your app a second or third time results in no prompt to enter the user credentials. An ephemeral session means ASWebAuthenticationSession
will not cache anything and will always ask the user for their credentials at the start of the session.
Build and run.
You’ll still be signed in since the app persists the access and refresh tokens. Tap Sign Out at the top to clear the tokens. Now, upon attempting to sign in, you’re asked to input your GitHub username and password. The app won’t cache anything from your previous login, as this is a private session.
Excellent! This might seem like a small issue, but security-wise, it’s of great benefit.
Where to Go From Here?
You can download the completed project by clicking the Download Materials button at the top or bottom of this tutorial.
Great work! In this tutorial, you got an introduction to OAuth and created your own third-party GitHub app using ASWebAuthenticationSession
to authenticate. Some additional features you could try are enhancing the way you store the tokens by putting them in the keychain and adding more GitHub API requests.
If you’re interested in reading more about OAuth, visit IETF’s page, which contains the whole standard. It’ a bit dense, but a good reference.
For ASWebAuthenticationSession
details and documentation, visit the Apple Developer Documentation.
For a comprehensive video course on networking, check out the Networking with URLSession.
If you have any questions or comments, don’t hesitate to reach out in the forum discussion below.
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more