Skip to content

laconicman/RefreshTokenAuthMiddleware

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

RefreshTokenAuthMiddleware

Swift Compatibility Platform Compatibility

An auth middleware package for Swift OpenAPI Generator/Runtime for a common scenario dealing with long-living refresh token and short-living access token. The library is quite universal in and can cover most of such cases.

Features

  • πŸ“ Clear and compact
  • ⚑️ Prevents duplication of auth queries (concurrent 401s collapse to one refresh)
  • πŸ”§ Adopter-configurable AuthPolicy for refresh-failure, sign-out, sign-in-priority, and persistent-rejection situations
  • 🎯 Proactive refresh when your validatedAndFormattedAccessToken rejects the cached token, with a reactive 401 retry as a backstop
  • πŸͺͺ Replays single-pass request bodies (streaming uploads) up to a configurable cap so retries don't send empty bodies
  • πŸ—οΈ Pluggable AuthenticationProviding seam for token persistence (Keychain, file, anything) β€” InMemoryAuthenticationProvider ships for tests and ephemeral sessions
  • πŸ”€ signIn(credentials:) / signOut() / reset() for explicit session control (account switch, deliberate logout, lockout recovery)
  • πŸ›‚ Per-operation shouldAuthorize predicate for specs that mix authenticated and public ops on one client

Scope

  • Session layer only: attach + silent refresh. The interactive authorization_code + PKCE grant (browser consent) belongs outside a middleware β€” pass the obtained tokens into your SignInAndRefresh conformance instead.
  • Tokens are cached in actor memory (write-through). Persistence is the adopter's AuthenticationProviding conformance β€” provide() / update(...) / revoke(). The package does not ship a Keychain conformance; that may land in a 2.1.x companion module.
  • Single-pass bodies above maxRetryBufferBytes (default 1 MiB) are sent once and a 401 is surfaced to the caller instead of retried. Pass maxRetryBufferBytes: 0 to opt out of buffering entirely.

See Sources/RefreshTokenAuthMiddleware.docc/Design-2-0-0.md for the load-bearing design β€” state machine, seam shapes, error taxonomy, and migration notes.

Installation

Swift Package Manager

Add the following to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/laconicman/RefreshTokenAuthMiddleware.git", from: "1.0.0")
]

Then add the dependency to your target:

targets: [
    .target(
        name: "YourTarget",
        dependencies: [
            .product(name: "RefreshTokenAuthMiddleware", package: "RefreshTokenAuthMiddleware")
        ]
    )
]

Usage

For "Client" generated with Swift OpenAPI Generator, implement conformance to "SignInAndRefresh" protocol.

This will define the logic of specific authentication queries, their results, and token validation.

import Foundation
import HTTPTypes // Gonna need this to modify requests inside those funcs of `SignInAndRefresh` protocol.
import RefreshTokenAuthMiddleware

extension Client: SignInAndRefresh {
    typealias Token = String
    typealias RefreshToken = String
    func signIn(credentials: Credentials) async throws -> (accessToken: Token, refreshToken: RefreshToken) {
        // adjust to your API operation
        let response = try await authSignIn(body: .json(.init(login: credentials.username, pwd: credentials.password)))
        let auth = try response.ok.body.applicationJsonCharsetUtf8.auth
        return (auth.token, auth.refreshToken)
    }
    // 2.0.0: return the (possibly rotated) refresh token as the second element.
    // Return `nil` if your server does not rotate.
    func refreshTokenIfNeeded(with refreshToken: RefreshToken?) async throws
        -> (accessToken: Token, refreshToken: RefreshToken?) {
        guard let refreshToken else { throw AuthError.missingRefreshToken }
        // adjust to your API operation
        let refreshTokenResponse = try await authRefreshToken(body: .json(.init(token: refreshToken)))
        let auth = try refreshTokenResponse.ok.body.applicationJsonCharsetUtf8.auth
        return (auth.token, auth.refreshToken)  // or `nil` if no rotation
    }

    @Sendable func authorizeRequest(_ request: HTTPRequest, with accessToken: Token?) throws -> HTTPRequest {
       // Setup request according to the doc. Usually just a header.
        var authorizedRequest = request
        authorizedRequest.headerFields[.authorization] = "Bearer \(try validatedAndFormattedAccessToken(accessToken))"
        return authorizedRequest
    }
    // Throwing here is how the middleware learns the cached token is stale β€” it will
    // proactively refresh (or sign in) before sending the request. Wire your real
    // expiry check (e.g. a stored `expiresAt`) in place of the nil-only guard.
    @Sendable func validatedAndFormattedAccessToken(_ token: Token?) throws -> Token {
        guard let token else { throw AuthError.missingAccessToken }
        // if Date() >= expiresAt { throw AuthError.missingAccessToken }
        return token
    }

}

Pass RefreshTokenAuthMiddleware to your generated Client

In main it could look like this:

import OpenAPIRuntime
import OpenAPIURLSession
import Foundation

struct OuterClient: Sendable {
    let client: Client
    private let refreshTokenAuthMiddleware: RefreshTokenAuthMiddleware<Client>
    init?(credentials: Credentials) {
        guard let serverURL = try? Servers.Server1.url() else { return nil}
        let authManagementClient = Client(
            serverURL: serverURL,
            transport: URLSessionTransport()
        )
        // `InMemoryAuthenticationProvider` ships with the package β€” replace with a
        // Keychain-backed conformance in production to persist across launches.
        refreshTokenAuthMiddleware = RefreshTokenAuthMiddleware(
            authManagementClient: authManagementClient,
            credentials: credentials,
            authProvider: InMemoryAuthenticationProvider<String, String>()
        )
        self.client = Client(
            serverURL: serverURL,
            transport: URLSessionTransport(),
            middlewares: [refreshTokenAuthMiddleware]
        )
    }
}

let client = OuterClient(credentials: .init())
// Authorization is fully automatic by now. But if we do sign in there should be no extra re-auth request.
// let authorizationResponse = try await client?.client.signIn(credentials: .init())
// print(authorizationResponse ?? "No auth response")
let adminUsersResponse = try await client?.listGoods(body: .json(.init(limit: 10, offset: 0, page: 1, filter: "", order: .init(id: "asc"))))
print(adminUsersResponse ?? "No admin users response")

Contributing

Contributions and are welcome!

This is a helper package for the following

License

This project is licensed under the Apache License 2.0.

About

A flexible auth middleware for Swift OpenAPI Generator for a common scenario dealing with two tokens: long-living refresh token and short-living access token.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages