Generic Swift OAuth 2.0 in Async Way

mein
4 min readOct 16, 2022

--

[Updated] I write a new article, OAuth2 made easy by TCA + Swift OpenAPI Generator. It introduces a package to do the OAuth2 Flow.

Network code snippet is a good place to practice writing generic Swift code. At the same time, we could also practice the async way, too.

Among all network code snippets, code for OAuth 2.0 is slightly more complex. But as long as you managed it, you will have a very handy tool. And it is simpler than you have thought about. You don’t have to rely on third party library for OAuth 2.0.

In Swift, we could write OAuth 2.0 in different ways:

  • use call back function
  • use Combine framework
  • use async/await

This article will focus on the async/await way.

The overall OAuth 2.0 flow is like this:

this diagram is drawn with Heptabase

There are two asynchronous calls:

  1. Web authentication via ASWebAuthenticationSession: This needs to be wrapped into an async call since the result of ASWebAuthenticationSession is in the traditional call back style.
  2. URLSession request: This already has the built-in async way.

So there is only one prerequisite: how to convert a callback based function to async/await?

Referring to Wrapping existing asynchronous code in async/await in Swift, the key word is withCheckedThrowingContinuation:

func someFunction(_ completion: @escaping (Result<T, Error>) -> Void) {
//some code which call back the completion closure
}
func someFunction() async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
someFunction { result in
switch result {
case .success(let result):
continuation.resume(returning: result)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}

Apply to ASWebAuthenticationSession,

Notice the ASWebAuthenticationSession needs to run in the main thread, so I wrapped it in the DispatchQueue.main.async, otherwise it will got warning,

-[UIWindow init] must be used from main thread only

We could integrate everything in an async flow. The outlines of code are like this:

func requestUserInfo<T>(    webAuthAsyncWrapper: @escaping () async throws->URL,    parseUrlToGetToken: @escaping (URL) async throws->String,    requestUserInfoByToken: @escaping (String) async throws->T) async throws->T{    let urlWithTokenInfo = try await webAuthAsyncWrapper()    let token = try await parseUrlToGetToken(urlWithTokenInfo)    let userInfo : T = try await requestUserInfoByToken(token)    return userInfo
}

The only generic type needed to consider is the return type. It is in general decoded from the Json data.

I will use Twitter and GitHub as examples to show for different OAuth 2.0 services, these closures would be different:

  • webAuthAsyncWrapper

Different services not only differ in the url end point, but also have various query parameters. We need to construct the url for the ASWebAuthenticationSession respectively.

+-----------+---------------------+------------------------+
| | Twitter | GitHub |
+-----------+---------------------+------------------------|
| scheme | https | https |
+-----------+---------------------+------------------------|
| host | twitter.com | github.com |
+-----------+---------------------+------------------------|
| path | /i/oauth2/authorize | /login/oauth/authorize |
+-----------+---------------------+------------------------|
| queryItems| client_id | client_id |
| | redirect_uri | redirect_uri |
| | response_type | response_type |
| | scope | scope |
| | state | |
| | code_challenge | |
| |code_challenge_method| |
+-----------+---------------------+------------------------|
  • parseUrlToGetToken

Though the urls returned from each OAuth 2.0 services are different, most likely the way to parse the urls to get the token is the same.

As an example, these are the urls returned from Twitter and GitHub

We only need the ‘code’ part in the urls.

let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let token = components.queryItems?.first(where: { $0.name == "code" })!.value
  • requestUserInfoByToken

This is using the token to construct an URLRequest and using the URLSession to get the Json data. Then decode it.

This is very common network code. The basic format is like this,

func decode<T:Decodable>(request : URLRequest)async throws->T{
let (data, response) = try await URLSession.shared.data(for: request)
let result : T = try JSONDecoder().decode(T.self, from: data)
return result
}

The key points is to construct the URLRequest and the Json structure correctly. Most likely you could find some sample code for it.

Here are the sample code for the URLRequest and the Json structure I referred to for Twitter and GitHub.

And the my sample code is here.

Conclusion

The ASWebAuthenticationSession is handy. There is no need to configure URL Types anymore. The ASWebAuthenticationSession will check whether the returned scheme is the same as the input parameter.

But it still uses the old way of call back function. So we need to integrate the ASWebAuthenticationSession into our async/await flow.

The async/await flow could apply in general to different kinds of OAuth 2.0 services.

At the end, not too much generic code is required in this practice. It shows that if we break down the overall process into step by step, we may find things become much easier than we had thought.

--

--