Contents

swift-microservices/swift-authentication-hummingbird

Swift Authentication for Hummingbird tells a Hummingbird

πŸ“– Overview

A request arrives with Authorization: Bearer <token>. BearerAuthenticationMiddleware hands the token to an Authenticator<String, Identity>, such as JWTAuthenticator from swift-authentication-jwt. The authenticator returns an identity or throws. The middleware then sets that identity in two places, so every reader finds the same caller:

  • the request context's identity, which HummingbirdAuth's IsAuthenticatedMiddleware and route

handlers read;

  • the task's OperationContext, as a Principal<Identity, String> under

PrincipalKey<Identity, String>, added to whatever context is already bound. Everything downstream reads it there, including outgoing gRPC calls that present the same token onward through swift-authentication-grpc.

Key Features:

  • πŸšͺ Open routes stay open: a request with no token continues anonymously, which is what an

open route needs: signing in mints the first token and has no caller yet.

  • β›” Bad tokens stop early: a token that fails authentication ends the request with

401 Unauthorized before the route runs. The authenticator's error is mapped to HTTPError(.unauthorized), so the client gets the authentication status, not a server fault.

  • 🎯 Routes decide who must be signed in: requiring a caller is a route's decision, made with

IsAuthenticatedMiddleware after the bearer middleware.

  • 🧩 Your context, your identity: the middleware works with any AuthRequestContext, and the

token's identity is the context's Identity. HummingbirdAuth's BasicAuthRequestContext<Identity> is enough for most applications.

  • πŸ” Relays as the caller: the principal keeps the token as presented, so a Hummingbird gateway

can call a gRPC service as the same person.

🏁 Getting Started

Prerequisites

  • Swift version: Swift 6.3+
  • Platforms: macOS 15+ or Linux

To install/upgrade Swift, see https://www.swift.org/install/

Adding as a dependency

Add the package and its product to your Package.swift. The examples below also use JWTAuthenticator from swift-authentication-jwt:

dependencies: [
    .package(url: "https://github.com/swift-microservices/swift-authentication-hummingbird.git", from: "1.0.0"),
    .package(url: "https://github.com/swift-microservices/swift-authentication-jwt.git", from: "1.0.0"),
],
targets: [
    .executableTarget(
        name: "App",
        dependencies: [
            .product(name: "AuthenticationHummingbird", package: "swift-authentication-hummingbird"),
            .product(name: "AuthenticationJWT", package: "swift-authentication-jwt"),
        ]
    ),
]

Usage

Put the middleware on the group of routes that need a caller, with IsAuthenticatedMiddleware after it. Keep sign-in and refresh routes outside that group, so an expired token a client still attaches cannot block recovery:

import AuthenticationHummingbird
import AuthenticationJWT
import Hummingbird
import HummingbirdAuth
import JWTKit

// Your token's payload. The authenticator returns it once the signature and `verify` pass.
struct AppToken: JWTPayload {
    let sub: SubjectClaim
    let exp: ExpirationClaim

    func verify(using algorithm: some JWTAlgorithm) throws {
        try exp.verifyNotExpired()
    }
}

// Verify tokens against your issuer's Ed25519 public key (its JWK "x" value).
let publicKey = try EdDSA.PublicKey(x: "<your issuer's public key>", curve: .ed25519)
let keys = JWTKeyCollection()
await keys.add(eddsa: publicKey)

let router = Router(context: BasicAuthRequestContext<AppToken>.self)

// Open: no token needed, so a client with an expired one can still sign in.
router.post("/sign-in") { _, _ in "issue a token here" }

// Authenticated: the bearer middleware proves the token, IsAuthenticatedMiddleware requires one.
router.group("/account")
    .add(middleware: BearerAuthenticationMiddleware(authenticator: JWTAuthenticator<AppToken>(keys: keys)))
    .add(middleware: IsAuthenticatedMiddleware())
    .get("/") { _, context in
        let token = try context.requireIdentity()
        return "Signed in as \(token.sub.value)"
    }

let app = Application(router: router)
try await app.runService()

A request to /account with no token, or with one that fails verification, gets 401 Unauthorized and never reaches the handler.

Reading the principal downstream

The handler passes the verified identity explicitly to a user use case. Code that has no request context, such as an adapter relaying the call to another service, reads the principal from the task's OperationContext instead. Spell the lookup once:

import Authentication
import OperationContext

extension OperationContext {
    var caller: Principal<AppToken, String>? {
        self[PrincipalKey<AppToken, String>.self]
    }
}

// Anywhere below the middleware, in the same task:
if let caller = OperationContext.current.caller {
    print(caller.identity.sub.value)  // who the token proved
    print(caller.credential)          // the token as presented, to forward unchanged
}
Calling backend services

mTLS secures connections to backend services. Forward the original user JWT only on upstream user RPC descriptors, where the receiving service verifies it and the owning use case checks permissions. User database settings follow the user operation. swift-authentication-grpc's propagation interceptor reads the principal this middleware bound and presents its token on those descriptors.

πŸ“˜ Documentation

the full reference on Swift Package Index

where the proven identity goes, how a route asks for one, and how a presented token is handled

Related packages:

Authenticator, Principal and PrincipalKey this package binds

JWTAuthenticator and JWTIssuer

the same binding for gRPC, and propagation of the token onward

the same middleware for Vapor

🧰 Release Info

only a major release breaks it; CI checks every pull request for API breakage.

  • Release notes: GitHub Releases
  • Swift versions: CI tests Swift 6.3 and 6.4 on Linux, plus the 6.4 and main nightly toolchains.
  • Dependencies: Hummingbird 2.28, HummingbirdAuth 2.5, swift-authentication 1.0,

swift-operation-context 1.0.

πŸ› οΈ Support

If you have any questions or need help, feel free to reach out by opening an issue.

Pull requests are welcome. Keep a change focused, prove new behaviour with a test, and label the pull request with its semantic version impact.

Development

swift test
swift-format lint --strict --recursive Sources Tests    # what the soundness check runs

License

MIT. See LICENSE.

Package Metadata

Repository: swift-microservices/swift-authentication-hummingbird

Default branch: main

README: README.md