Contents

diyamantina/bearertokenauthmiddleware

Bearer-token middleware for Swift OpenAPI clients and Vapor-backed OpenAPI

Installation

.package(url: "https://github.com/diyamantina/BearerTokenAuthMiddleware", from: "2.0.0"),
// iOS / macOS app target: client product only.
.target(
    name: "MyApp",
    dependencies: [
        .product(name: "BearerTokenAuthMiddleware", package: "BearerTokenAuthMiddleware"),
    ]
),

// Vapor server target: server product.
.target(
    name: "MyApi",
    dependencies: [
        .product(name: "BearerTokenAuthServerMiddleware", package: "BearerTokenAuthMiddleware"),
        .product(name: "Vapor", package: "vapor"),
    ]
),

Client Quick Start

import BearerTokenAuthMiddleware
import OpenAPIAsyncHTTPClient

let auth = BearerTokenAuthenticationMiddleware(
    initialToken: nil,
    skipAuthorization: { operationID in
        ["login", "refreshToken", "getHealth"].contains(operationID)
    }
)

let client = Client(
    serverURL: serverURL,
    transport: AsyncHTTPClientTransport(),
    middlewares: [auth]
)

// After login or token refresh:
auth.updateToken(loginResponse.accessToken)

// On logout:
auth.updateToken(nil)

The token is stored behind an actor, so it can be updated at runtime without rebuilding the generated client.

Server Quick Start

import BearerTokenAuthServerMiddleware
import Vapor

app.middleware.use(BearerTokenAuthServerMiddleware(mode: .jwt))

That single line:

  • Exempts the default health, readiness, and metrics endpoints.
  • Requires a bearer token everywhere else.
  • Runs JWT shape validation by default.
  • Stores the extracted token in BearerTokenContext.token for downstream

generated handlers.

Generated handlers can read the token without seeing Vapor's Request:

import BearerTokenAuthServerMiddleware
import Vapor

extension ApiServer {
    public func someProtectedOperation(
        _ input: Operations.SomeOp.Input
    ) async throws -> Operations.SomeOp.Output {
        guard let token = BearerTokenContext.token else {
            throw Abort(.unauthorized)
        }

        // Verify against your session store, JWT verifier, or authorization layer.
    }
}

Server Configuration

public init(
    mode: AuthMode = .none,
    publicEndpoints: Set<String> = BearerTokenAuthServerMiddleware.defaultPublicEndpoints,
    publicPathPrefixes: Set<String> = [],
    validation: ValidationStrategy = .auto,
    propagateTokenOnPublicRoutes: Bool = false
)

| Parameter | Default | Purpose | |---|---|---| | mode | .none | .none passes through, .uuid expects UUID session tokens, .jwt expects JWT-shaped tokens. | | publicEndpoints | health, readiness, metrics paths | Exact paths that bypass enforcement. Pass ` to require auth everywhere. | | publicPathPrefixes | | Boundary-anchored public subtrees. ["/admin"] matches /admin and /admin/x, not /administrator. | | validation | .auto | Chooses .uuidShape for .uuid, .jwtShape for .jwt, and .none for .none. | | propagateTokenOnPublicRoutes | false | Keeps BearerTokenContext.token` empty on public routes unless explicitly opted in. |

Validation Strategies

The built-in validators are intentionally shape-only:

| Strategy | Behavior | |---|---| | .auto | Picks from mode. | | .none | Only checks that a token is present on protected routes. | | .jwtShape | Requires three non-empty Base64URL segments separated by dots. Does not verify signatures or claims. | | .uuidShape | Requires a canonical UUID string. | | .custom(Validator) | Runs your async validator. Use this for DB session lookup or real JWT verification. |

let strict = BearerTokenAuthServerMiddleware(
    mode: .jwt,
    validation: .custom { token in
        try await jwtSigners.verify(token, as: AccessTokenPayload.self)
    }
)

Error Model

Server-side failures use typed errors that conform to AbortError:

public enum BearerTokenAuthServerError: Error, AbortError {
    case missingToken
    case invalidToken
}

Both return HTTP 401 with the external reason "Unauthorized". Custom validators may throw any error, such as Abort(.forbidden), and that error passes through unchanged.

Task-Local Token

BearerTokenContext exposes the token to handlers that do not receive Vapor's Request:

public enum BearerTokenContext {
    @TaskLocal public static var token: String?
}

By default, public routes and .none mode do not propagate inbound tokens into this task-local. Set propagateTokenOnPublicRoutes: true only when handlers on public routes explicitly need to inspect an optional token.

Architecture

Client side:
  Generated Client
    -> BearerTokenAuthenticationMiddleware
    -> transport

Server side:
  Vapor Request
    -> BearerTokenAuthServerMiddleware
    -> generated OpenAPI handler
       reads BearerTokenContext.token

Companion Packages

captures client IP and user-agent context for generated OpenAPI handlers.

logs OpenAPI requests and responses with default credential-header redaction.

Platform Support

The package ships two products with different platform profiles:

| Product | macOS | iOS / tvOS / watchOS | Linux | |---|---|---|---| | BearerTokenAuthMiddleware | swift test | client build verification | swift test | | BearerTokenAuthServerMiddleware | swift test | Not supported; Vapor is server-only | swift test |

Consumers on Apple mobile platforms should depend on the client product only.

Documentation

DocC is enabled:

swift package --allow-writing-to-directory ./docs generate-documentation \
    --target BearerTokenAuthServerMiddleware \
    --output-path ./docs/server.doccarchive

Testing

The package ships with 78 tests across 14 suites covering:

  • Client-side header stamping and operation skipping.
  • Server modes, default public endpoints, and public prefix matching.
  • JWT-shape and UUID-shape validation.
  • Custom validators and typed error behavior.
  • Task-local propagation and public-route non-propagation.
  • Adversarial inputs, large tokens, repeated authorization headers, and

concurrent request isolation.

Run them with:

swift test

License

Apache 2.0. See LICENSE.

Package Metadata

Repository: diyamantina/bearertokenauthmiddleware

Default branch: main

README: README.md