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.tokenfor 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.tokenCompanion 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.doccarchiveTesting
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 testLicense
Apache 2.0. See LICENSE.
Package Metadata
Repository: diyamantina/bearertokenauthmiddleware
Default branch: main
README: README.md