How to Add OAuth in iOS in Half an Hour
Published: 2023-03-08 01:00:42

Hello! [Our](https://www.kts.solutions/) iOS Developer Lena wrote an article about adding Oath to iOS in half 

In most cases, a mobile application should be able to authorize users for data access, and quite often this is done with third-party services. In these cases, _OAuth2.0 is used_. It is a protocol that allows to log into another service without the entering your login and password to the application and giving a restricted set of accesses to the service resources.

Mobile applications use _Authorization Code Flow with Proof Key for Code Exchange (PKCE)_. 

Today, we’ll consider implementation of the OAuth authorization with [AppAuth-iOS](https://github.com/openid/AppAuth-iOS). It is one of the most popular and easy-to-use libraries. The code from the article is available at [Github](https://github.com/elenakacharmina/ios-oauth-example).

In this article you will learn about:

- Basic configuration
- Authorization in the third-party service and saving the access token for further use
- Access token update
- Logout
- Conclusions

**Basic configuration**

We’ve chosen OAuth Github as a third-party service. The basic configuration is similar to that of the android application from the previous article: first, [let’s register the OAuth application in Github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app).

When registering, set CALLBACK_URL for your application in the service. This is the URL to redirect to after authorization, and to be intercepted by your application.

We’ll use ru.kts.oauth://github.com/callback as CALLBACK_URL. Don’t forget to use the _ru.kts.oauth_ custom scheme, so that only your application could intercept the redirect.

After registering the OAuth application in Github, you should have _client_id_ and _client_secret_ you have to generate. Save them.

Now you need to understand, what URL to go to in order to get authorized in the Github web-page, and what URL is to be used to exchange the code for the token. You can find the answer in the [Github OAuth documentation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps).

- URL for authorization: https://github.com/login/oauth/authorize
- URL for token exchange: https://github.com/login/oauth/access_token

To get authorized, you have to determine the scopes Github provides access to. Let’s say, in the application we need access to the user information and repositories: _user_, _repo_.

After configuring the OAuth Github application, we should have the following data we will add to the project as configuration properties:

```
struct AuthConfiguration {
    static let baseUrl = "https://github.com/"
    static let authUri = "login/oauth/authorize"
    static let tokenUri = "login/oauth/access_token"
    static let endSessionUri = "logout"
    static let scopes = ["user", "repo"]
    static let clientId = "..."
    static let clientSecret = "..."
    static let callbackUrl = "ru.kts.oauth://github.com/callback"
    static let logoutCallbackUrl = "ru.kts.oauth://github.com/logout_callback"
}
```

Please note that _callbackUrl_ and _logoutCallbackUrl_ have the _ru.kts.oauth_ custom scheme. For the application to be able to intercept the redirect, it’s necessary to write this scheme in _Info.plist (URL Schemes, URLIdentifier)_:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zapygmltkhgbjc2jgafy.jpg)

Connect AppAuth library via the cocoapods. When working with Github OAuth, there is one nuance — Github returns [response in the xml format](https://github.com/openid/AppAuth-iOS/pull/206), while OAuth specification requires response in the json format. You can’t add the custom request header to specify the response format in the AppAuth library, so let’s fork it and [add the headers](https://github.com/openid/AppAuth-iOS/compare/master...elenakacharmina:AppAuth-iOS:master) to the source code.

Connecting AppAuth library

Now, let’s execute authorization.

📱 **Authorization**

Our task is:

- to create the method, in which we will generate the authorization request and call the AppAuth library method
- to implement the redirect intercepting to complete the process

**1. Create the authorization session.** For a start, in AppDelegate announce the variable of the OIDExternalUserAgentSession type that will hold the current authorization session and will be used when intercepting the redirect to continue the authorization process (implementation is below):

```
class AppDelegate: UIResponder, UIApplicationDelegate {

    var currentAuthorizationFlow: OIDExternalUserAgentSession?

}
All classes / protocols of the AppAuth library have OID prefix.
```

**2. Create the authorization configuration.** The AppAuth library contains the **OIDAuthorizationService** entity that allows to execute OAuth requests.

Requests are presented by _OIDAuthorizationRequest_ (authorization request), _OIDTokenRequest_ (token update request), _OIDEndSessionRequest_ (logout request) entities. Every request requires the _OIDServiceConfiguration_ configuration — a set of endpoint uris in the initializer. We’ll create the common constant for all requests and put it in a new class of OAuthRepository:

```
class OAuthRepository {
    private let configuration = OIDServiceConfiguration.init(
        authorizationEndpoint: URL(string: AuthConfiguration.baseUrl + AuthConfiguration.authUri)!,
        tokenEndpoint: URL(string: AuthConfiguration.baseUrl + AuthConfiguration.tokenUri)!,
        issuer: nil,
        registrationEndpoint: nil,
        endSessionEndpoint: URL(string: AuthConfiguration.baseUrl + AuthConfiguration.endSessionUri)!)
}
```
In our case, _issuer_ (The OpenID Connect issuer) and _registrationEndpoint_ (registration uri) fields are not used.

**3. Create the authorization request.** The entire OAuth data logic will be located in OAuthRepository class; let’s place the login method there.

State the request:

```
 let request = OIDAuthorizationRequest(
            configuration: configuration,
            clientId: AuthConfiguration.clientId,
            clientSecret: AuthConfiguration.clientSecret,
            scopes: AuthConfiguration.scopes,
            redirectURL: URL(string: AuthConfiguration.callbackUrl)!,
            responseType: OIDResponseTypeCode,
            additionalParameters: nil)
```

- The _responseType_ field informs, in what form we want to get the response after authorization. Other possible values are _OIDResponseTypeToken_, _OIDResponseTypeIDToken_. Github always returns only the code, that’s why let’s use _OIDResponseTypeCode_
- _additionalParameters_ is a dictionary, whose keys will be added to the request URL as additional _query_ parameters. They can be required for some OAuth services, but we don’t use them in our example

**4. Execute the authorization request.** The next step is to call the AppAuth library method to execute the request.

If an authorized user doesn’t have to open pages in the application in SafariVC, you can get away with the standard [method](https://github.com/openid/AppAuth-iOS#authorizing-ios) from the AppAuth library documentation. Under the hood, the method uses _OIDExternalUserAgentIOS_ with a separate _ASWebAuthenticationSession_ session, which then doesn’t share cookies with other _SafariViewControllers_. That’s not what we want.

To share the session with SavariVC we implement custom _OIDExternalUserAgentIOSSafariViewController_ that implements the _OIDExternalUserAgent_ interface from the library. This allows to open pages in _SafariViewController_ without authorization.

[Click here](https://github.com/elenakacharmina/ios-oauth-example/blob/master/ios-oauth-example/Common/OIDExternalUserAgentIOSSafariViewController.swift) to see the sample implementation and add it to the project, and read more about UserAgent in [this article](https://backstage.forgerock.com/community/developer).

**5. Create OIDExternalUserAgentIOSSafariViewController**:

```
let agent = OIDExternalUserAgentIOSSafariViewController(presentingViewController: viewController)
```

Now let’s use the authorization method with the custom _externalUserAgent_:

```
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, externalUserAgent: agent) { [weak self] authState, error in
    // implementation below
}
```

Calling this method will open SafariVC with the authorization page, and after successful authorization, the service will redirect you to the url stated in the redirectURL field of the request. In our case, it’s _ru.kts.oauth://github.com/callback_.

The uri redirect parameters contain the _code_ we need. It’s necessary to intercept this redirect, and then to exchange code for the token.

**6. Now, let’s intercept the authorization redirect and get the token**. To intercept the redirect, let’s implement _scene(_, openURLContexts:)_ method in _sceneDelegate_, for ios versions lower than 13 — _application_(_:, open url: URL, options:) in AppDelegate:

```
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    if let authorizationFlow = appDelegate.currentAuthorizationFlow,
       let url = URLContexts.first?.url,
       authorizationFlow.resumeExternalUserAgentFlow(with: url) {
            appDelegate.currentAuthorizationFlow = nil
     }
}
```

Here we check the current authorization flow in the application and call the _resumeExternalUserAgentFlow_ method that verifies, if this uri coincides with the one stated in the request. If the method returns true, SafariVC hides and the process of exchange the code for the token starts. The code is exchanged for the token under AppAuth hood.

**7. Save the tokens**. After exchanging the code for the token, we get in the _OIDAuthState.authState_ method handler we used during the previous step. It’s necessary to check that there are no errors and to save the obtained token. For simplicity, let’s save access and refresh tokens in UserDefaults:

```
appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, externalUserAgent: agent) { [weak self] authState, error in
    if error == nil {
        let tokenModel = TokenModel(access: authState?.lastTokenResponse?.accessToken,
                                            refresh: authState?.lastTokenResponse?.refreshToken)
        self?.userDefaultsHelper.setToken(value: tokenModel)
    }
}
```

In a real-life project, that would not be safe, so it’s better to use keychain.

**7. Test the authorization**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oujldczyo8mxkciaijmk.jpg)

Run the code.

We have already implemented OAuth authorization and token receipt:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/65l18k71v2jrn6828yi7.jpg)

To validate the obtained token, we push a new screen, where we try to [get data](https://github.com/elenakacharmina/ios-oauth-example/blob/0da54ebf3fa6b0bb93903e73b6c16964954d96c5/ios-oauth-example/Repositories/DataRepository.swift#L17) from https://api.github.com/user with the token.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mkl0knr1ssmr00n9k0jz.jpg)

To make sure that the authorization session shares cookies with SafariVC, we open the Github user page in it by clicking:

📱**Token Update**

Refresh token is not used in Github OAuth, but in many other services the access expires. That’s why I provide the token update example.

**1. Generate the token update request.** For a start, it’s necessary to generate OIDTokenRequest:

```
func refreshToken() {
    guard let refreshToken = userDefaultsHelper.getToken()?.refresh else { return }
          let requestRefresh = OIDTokenRequest(
              configuration: configuration,
              grantType: OIDGrantTypeRefreshToken,
              authorizationCode: nil,
              redirectURL: nil,
              clientID: AuthConfiguration.clientId,
              clientSecret: AuthConfiguration.clientSecret,
              scope: nil,
              refreshToken: refreshToken,
              codeVerifier: nil,
              additionalParameters: nil)
```

In addition to the already known parameters we’ve seen during authorization, in the request it’s necessary to transfer the _OIDGrantTypeRefreshToken_ value and the saved refresh token in the grantType parameter. We transfer nil in the parameters we don’t use.

**2. Update the token.** Call the _OIDAuthorizationService_.perform method, if updated successfully, we save new values of access and refresh tokens:

```
func refreshToken() {
    guard let refreshToken = userDefaultsHelper.getToken()?.refresh else { return }
    let requestRefresh = OIDTokenRequest(
        configuration: configuration,
        grantType: OIDGrantTypeRefreshToken,
        authorizationCode: nil,
        redirectURL: nil,
        clientID: AuthConfiguration.clientId,
        clientSecret: AuthConfiguration.clientSecret,
        scope: nil,
        refreshToken: refreshToken,
        codeVerifier: nil,
        additionalParameters: nil)
        
    OIDAuthorizationService.perform(requestRefresh) { [weak self] tokenResponse, error in
        if error == nil {
            let tokenModel = TokenModel(access: tokenResponse?.accessToken,
                                        refresh: tokenResponse?.refreshToken)
            self?.userDefaultsHelper.setToken(value: tokenModel)
        }
    }
}
```

You can check this implementation, if you create the [Github Apps](https://docs.github.com/en/apps/creating-github-apps/creating-github-apps/creating-a-github-app) application instead of OAuth and replace clientId and clientSecret in AuthConfiguration.

📱**Logout**
When using OAuth authorization, it’s not enough to just clear the saved token from the storage. SafariVC cookies are not cleared, and when trying to get authorized again, a user will automatically log into the services under the previous account without the opportunity to enter another login / password.

That’s why, to implement logout, it’s also necessary to generate the request in _OIDEndSessionRequest_…

```
let request =  OIDEndSessionRequest(configuration: configuration,
                                    idTokenHint: accessToken,
                                    postLogoutRedirectURL: URL(string: AuthConfiguration.logoutCallbackUrl)!,
                                    additionalParameters: nil)
```

…and include it in the OIDAuthorizationService.present method:

```
let agent = OIDExternalUserAgentIOSSafariViewController(presentingViewController: viewController)

appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(request, externalUserAgent: agent) { [weak self] (response, error) in
  if let error = error {
       completion(.failure(CustomError.logoutError))
  } else {
       self?.userDefaultsHelper.clearToken()
    }
}
```
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72i9xhb99ujk1zkm2th9.jpg)

GitHub OAuth has no redirect after logout, that’s why users will have to close the window by themselves.

This causes an error in the callback, that’s why there is no error check in the final version, and the app just clears off the saves tokens:

```
let agent = OIDExternalUserAgentIOSSafariViewController(presentingViewController: viewController)

appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(request, externalUserAgent: agent) { [weak self] (response, error) in
    self?.userDefaultsHelper.clearToken()
}
```

📲** Conclusions**
The entier project code can be found in [github](https://github.com/elenakacharmina/ios-oauth-example).

We’ve implemented all methods required to support OAuth: authorization, token update and logout. Using the library helps easily integrate OAuth in the application. But some services — such as Github OAuth — have their own usage patterns that require custom solutions.

How do you implement OAuth in your applications? Have you had experience of working with AppAuth? What OAuth issues have you faced?