OAuth2 made easy by TCA + Swift OpenAPI Generator

mein
Dev Genius
Published in
4 min readDec 26, 2023

--

Solve OAuth2’s complex flow and diversified spec by TCA + Swift OpenAPI Generator.

Although Swift OpenAPI Generator accepts securitySchemes in yaml definition, the authentication flow is not functioning yet. We need to implement it by ourselves.

Background Knowledge: OpenAPI + OAuth2

There are 4 kinds of flows in OpenAPI's OAuth2 definition.

  • authorizationCode – Authorization Code flow
  • implicit – Implicit flow
  • password – Resource Owner Password flow
  • clientCredentials – Client Credentials flow

We will focus onauthorizationCode because it is the most common flow.

Authorization code — The most common flow, mostly used for server-side and mobile web applications. This flow is similar to how users sign up into a web application using their Facebook or Google account.

https://swagger.io/docs/specification/authentication/oauth2/

The major two steps in authorizationCode are:

  • authorizationUrl : Pop-up a web browser for user to authenticate and authorize.
  • tokenUrl : Make a request to some specific API endpoint.

Implement the OAuth2 Flow

We can separate the flow into 3 parts:

  1. authorizationUrl : Use Swift’s build-in service ASWebAuthenticationSession
  2. tokenUrl : Define the request in the OpenAPI yaml format. Use Swift Open API Generator to make the request.
  3. flow coordinator: Use TCA nested reducers.

Target: A scaffold that applies to any OAuth2 service.

No need to code from scratch every time when connecting to a new OAuth2 service anymore.

BaseAsyncCallReducer

Both AuthorizationUrlReducer and TokenUrlReducer are making some async call. So we create a generic BaseAsyncCallReducer first.

Here is the unit test.

AuthorizationUrlReducer

There are two parts, AuthorizationUrland the nested reducer, BaseAsyncCallReducer.

AuthorizationUrl can be standalone. We can test it independently.

Here is the unit test.

Finally, wrap it up in a reducer AuthorizationUrlReducer.

TokenUrlReducer

We can think of it in a more generic format. It is a reducer who is communicating with the Swift OpenAPI Generator. There are two parts, OpenAPIClientRequest and the nested reducer, BaseAsyncCallReducer.

OpenAPIClientRequestProtocol can be standalone. We can test it independently.

The unit test is in my package. It is in one of the test targets. In order to run the Swift OpenAPI Generator properly, I need to configure it in the Package.swift file.

.testTarget(name: "OpenAPIClientRequestTests",
dependencies: [
.product(name: "OpenAPIRuntime",package: "swift-openapi-runtime"),
.product(name: "OpenAPIURLSession",package: "swift-openapi-urlsession"),
],
plugins: [
.plugin(name: "OpenAPIGenerator",package: "swift-openapi-generator")
]
)

We use Spotify ClientCredentials Auth Flow as an example because it does not depends on popping-up user login window.

Wrap it up in a reducer, OpenAPIClientRequestReducer

Package: AAA — authenticate-any-api

Combining all the elements, the outcome is a package, AAA — authenticate-any-api.

It provides OAuth2 Flows of Spotify, Twitter and GitHub as examples.

Though the detail configuration of each OAuth2 Flows are different, they all have same structure. We can see the unit test code look very similar.

func testReducer() async throws {
let authFlow = AAATwitter.authorization_code_PKCE(client_id: Credential.TwitterAuth.client_id, redirect_uri: Credential.TwitterAuth.redirect_uri, code_challenge: code_challenge, code_verifier: code_challenge, scope: [.follows_read,.offline_access,.tweet_read, .users_read], state: state)
let store : StoreOf<Reducer> = .init(initialState: State.init(authFlow: authFlow
, prefersEphemeralWebBrowserSession: prefersEphemeralWebBrowserSession
, debounceDuration: debounceDuration)) {
Reducer()
} withDependencies: { [self] in
$0.mainQueue = mainQueue.eraseToAnyScheduler()
}
await store.send(.auth).finish()
let access_token = store.withState { state in
return state.accessTokenResponse!.access_token
}
let client = OpenAPITwitterClient(accessToken: access_token)
let response = try! await client.findMyUser()
}
func testReducer() async throws {
let authFlow = AAASpotify.authorization_code(client_id: Credential.SpotifyAuth.client_id, client_secret: Credential.SpotifyAuth.client_secret, redirect_uri: Credential.SpotifyAuth.redirect_uri, scope: [.playlist_read_private, .user_read_playback_state, .user_read_private], state: nil)
let store = Store(initialState: State(authFlow: authFlow, prefersEphemeralWebBrowserSession: false, debounceDuration: debounceDuration)) {
Reducer()
}withDependencies: { [self] in
$0.mainQueue = mainQueue.eraseToAnyScheduler()
}
await store.send(.auth).finish()
let access_token = store.withState { state in
return state.accessTokenResponse!.access_token
}
let client = OpenAPISpotifyClient(accessToken: access_token)
let response = try! await client.get_hyphen_the_hyphen_users_hyphen_currently_hyphen_playing_hyphen_track(.init())
}

Notes

Swift OpenAPI Generator is young and iterates fast. We are looking forward it will one day support OAuth2 Flow.

While it is not supported yet, this AAA package provides some idea on how to solve OAuth2 Flow in a generic way.

--

--