How to Build an iOS App With OAuth2 Authentication Flow — GitHub Example(Part 1)
Step by step journey on how to build a flexible Swift app using a hexagonal architecture that uses an authentication method OAuth2.
Introduction
Let’s imagine that you are asked to create an app that is able to list all the Github repo of un user, public and private.
So you start taking a look at the Github API and you discover that the API that you need requires an access token.
Backend Configuration
To obtain the token you must create a Github OAuth App and use it to authenticate the user.
The app configuration requires an “Authorization callback URL”. This URL will be used by Github to notify the result of the authentication process. So, to be able to be notified inside the app, you need to specify a URL that has a schema that is unique for your app. I’ll use as URL it.iacopo.github://authentication
since the bundle Id of the app that I’ll create is it.iacopo.github
.
After the creation is done you’ll obtain a “Client ID” and a “Client Secret” that are used later to configure the iOS app.
iOS App
(In this article I will add for each step the commit reference inside square brackets of a sample project, so you can also follow the evolution of the codebase from there 🙂)
Coming back to the Github OAuth documentation, and specifically the Web application flow, we see that we need to handle the following steps:
- Users are redirected to request their GitHub identity
- Users are redirected back to your site by GitHub
- Your app accesses the API with the user’s access token
1. Request a user’s GitHub identity
To request a Github identity we need to show a web page where the user can login using her credentials and give access to the Github App we created before.
So we need a UIViewController with a button that presents a SFSafariViewController
initialized with that URL. I’m not a fan of the Storyboard approach to build the UI because it doesn’t allow you to use constructor injection to set the UIViewController dependencies so my first step is to delete the default Storyboard and its view controller. To properly remove it you need to delete these two lines from the info.plist
and delete the files Main.storyboard
and ViewController.swift
[bc48224]. Then I create a new view controller, called LoginViewController
[03054ed], with the following design
Now let me explain briefly the architecture of the project that I’ll follow.
App Architecture
The idea is to distinguish between UI components, Domain components, and Infrastructure components. The Domain components don’t depend on any other kind of components, while the UI and the Infrastructure components depend on Domain. This kind of architecture allows us to clearly separate responsibility and easily switch between implementations. Using a composition root and a constructor injection approach the project is also highly testable
Composition Root
All these components are created and wired up together by a Composition root that lives in Main. These components hold the long live dependency of the project and provide to each component the concrete implementation of its own dependencies. This object is created directly on the Scene Delegate and is used to instantiate the LoginViewController
created before, and set it as the rootViewController
[38f5225]
OAuthService
Now, coming back to OAuth flow, we need to generate the proper URL to open on the LoginViewController
when the user clicks on the button. To do so we will use a Domain object that provides this URL to the view controller called OAuthService
that will be injected as constructor parameter
This object depend on OAuthClient
, a protocol defined in the Domain layer and implemented on the in Infrastructure one. Let’s use for now a very basic implementation of the client that returns a hardcoded URL. The diagram now looks like this(I removed the <<create>>
relationship for clarity).
And this is the dependency wiring up happening on the AppDependencyContainer
At this point, we are able to open the web page where the user could authenticate itself but we didn’t handle yet the deep link that we will receive when the user successfully authenticates [f0bddd6]. So let’s move to the next step
2. Users are redirected back to your site by GitHub
Deeplink configuration
To be able to react to the URL you specify on the Github app creation we need to add a new entry on the URL Types
tab section of the info
tab in the iOS target like in the image below.
Now the app will react to the custom URL schema and we need to handle it properly. To be notified when such interaction happen while the app is in the foreground we need to override this method of the Scene delegate
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
We’ll create an enum to specify the kind of deeplink the app support and right now we have just the OAuth one
We also create a class that contains a map of which action to execute for each deeplink. This class is used on the callback method discussed before
The Github documentation says that the deeplink URL contains two params: code
and state
If the user accepts your request, GitHub redirects back to your site with a temporary
code
in a code parameter as well as the state you provided in the previous step in astate
parameter.
And the app should be able to exchange that code
for an access code. Let’s see how we can do it 💪🏻
Exchange code for an access token
I think that is the OAuthService
responsibility to be able to extract the code
and state
parameters from the deeplink and it can forward the exchange message to the OAUthClient
to obtain an access token. So I’ll add an additional method to the OAuthClient
protocol
and add a method to the OAuthService
to extract data from the URL and perform the code exchange
The last thing to do is to actually execute this method when a deeplink is received since right now the map that contains the callback to execute for each deeplink is empty. We can add the proper callback directly in the AppDependencyContainer
At this point[a5763f0] we are able to receive a deeplink and exchange it for an access token but the LoginViewController
, that start the authentication process, is not aware of the result. So we need a way fromOAuthService
to notify backLoginViewController
. To do so I decide to add a callback in OAuthService
var onAuthenticationResult: ((Result<TokenBag, Error>) -> Void)?
that will be called by OAuthService
inside exchangeCodeForToken
This callback will be wired up in viewDidLoad
of the LoginViewController
, using property injection
In this example, I just show an alert with the token received [f130a54].
Let’s try it in the simulator now, using as URL a static page with a link to the deeplink defined before. It works! 🎉
OAuth Client
The current implementation of the OAuthClient
return just static values
Now it’s time to integrate the app with the real API. To do this we’ll create an OAuthClient
implementation that is able to interact with the Github API. To be able to perform and describe network request we create as well two small abstractions called HTTPRequest
andHTTPClient
[021c8ea]. We can see the relationship between these objects in the updated architecture diagram
Moving to Github API documentation we see that to request a user identity we should use this URL
GET https://github.com/login/oauth/authorize
with the following parameters:
So we can think at the following struct to contains the needed parameters
Let’s look now at the API we should use to exchange the code for an access token. The URL is
POST https://github.com/login/oauth/access_token
with the following parameters:
based on that, the complete OAuthConfig
looks like this:
Using this information the RemoteOAuthClient
can create the proper Codable
request and response to interact with the API.
The last thing to do is to useRemoteOAuthClient
instead of LocalOAuthClient
. Thanks to the architecture used we can easily exchange it in the composition root
of course, you must insert here the “Client ID” and a “Client Secret” that you obtained in the first step of this tutorial and change as well the redirectUri
[e4c4463]
Conclusion
We saw so far how is possible to use the Github OAuth flow in an iOS app. As a disclaimer, I’d like to say that this implementation can not be used as a complete OAuth library since for example doesn’t handle the PKCE flow nor the possible expiration of the access token, but it gives you an idea of how it is possible to implement it from scratch 🏗
We saw as well how this kind of architecture lets you switch the implementation of a concrete class just in the composition root with no impact on the rest of the app.
If you have any questions, critics, suggestion feel free to leave your comment below and let me know 💪🏻
In the second part of the article, we’ll see how we can use the access token to make an authenticated request, how we can store it in the Keychain, how separate the presentation logic from the UI, and much more!