OAuth2 made easy by TCA + Swift OpenAPI Generator
Solve OAuth2’s complex flow and diversified spec by TCA + Swift OpenAPI Generator.
Background Knowledge: OpenAPI + OAuth2
There are 4 kinds of flows in OpenAPI's OAuth2 definition.
authorizationCode
– Authorization Code flowimplicit
– Implicit flowpassword
– Resource Owner Password flowclientCredentials
– Client Credentials flow
We will focus onauthorizationCode
because it is the most common flow.
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:
authorizationUrl
: Use Swift’s build-in service ASWebAuthenticationSessiontokenUrl
: Define the request in the OpenAPI yaml format. Use Swift Open API Generator to make the request.- 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, AuthorizationUrl
and 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.