Contents

swift-microservices/swift-authentication-grpc

Binding who is calling on gRPC: a bearer token or the peer's certificate on the way in, and the

Binding a caller from a token

let authenticator = JWTAuthenticator<AppToken>(keys: keys)

GRPCServer(
    transport: transport,
    services: [service],
    interceptorPipeline: [
        .apply(BearerAuthenticationInterceptor(authenticator: authenticator), to: .services([Service.descriptor]))
    ]
)

A call with no token continues anonymously, which is what an open RPC needs: signing in mints the first token and has no caller yet. A token the authenticator declines continues unbound. A token it refuses fails the call as unauthenticated, because absent and invalid are not the same thing.

Requiring a caller is the handler's decision:

guard let caller = ServiceContext.current?[PrincipalKey<AppToken, String>.self]?.identity else {
    throw RPCError(code: .unauthenticated, message: "Sign in to continue.")
}

Binding a peer from its certificate

import AuthenticationGRPCNIOTransport

CertificateAuthenticationInterceptor(authenticator: SPIFFEAuthenticator(trustDomain: "example"))

The transport verified the certificate at the handshake; the authenticator reads who it names. A connection with no client certificate, or one the authenticator declines, continues unbound: an unlisted peer is a valid one this service simply does not admit. The principal is bound under PrincipalKey<SPIFFEID, Certificate>, separately from any bearer principal, because a service relaying a person's call arrives with its own certificate and the person's token.

Calling onward as the same caller

GRPCClient(transport: transport, interceptorPipeline: [
    .apply(BearerPropagationInterceptor<AppToken>(), to: .services([UpstreamService.descriptor]))
])

The propagation interceptor reads the bearer principal and puts its token back on the outgoing call, so one token identifies the caller at every service in the chain. Apply it to the upstream services that take a token, so a public service is dialled with nothing. Calls made outside a caller's request, startup work, a workflow activity, go out unauthenticated rather than failing; a process identifies itself on such calls with its certificate, not a token.

Testing a handler

Authenticator is a one-method protocol, so a handler test conforms a dictionary to it and sends Bearer admin-token without minting a key. The interceptors themselves are tested the same way here, by calling intercept directly with a constructed request and context.

Requirements

Swift 6.3, macOS 15 or Linux.

Development

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

Contributing

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

License

MIT. See LICENSE.

Package Metadata

Repository: swift-microservices/swift-authentication-grpc

Default branch: main

README: README.md