Co-authored with Emelia Smith
At Germ, we recently shipped our E2EE atproto messenger into public beta with an AT Protocol Client for Swift built on libraries from Matt Massicotte and CJ Riley. Along the way, we contributed to these libraries where we could, and gained a lot of experience about the existing libraries. They worked for our public beta, and we learned a lot.
After reflecting on that experience and thinking about what we could do differently, we're now building our next Swift stack for AT Protocol in the open.
This stack will include an OAuth Client and an AT Protocol Client with a particular focus on our future needs: longer sessions, multi-platform support, multi-account support, did:web resolution, better security and privacy, lower maintenance burden for lexicons in Swift types, and cryptographic validation of the records we retrieve from PDSes.
For longer sessions, we're planning on adopting the Client Assertion Backend (CAB) proposal from Bluesky, along with using the platform attestation APIs for asserting the client to the CAB server.
We're building cross-platform with Swift, including for Android. This means we'll have pluggable platform-specific implementations, such as for presenting Web UI during user authentication with OAuth, and making sure our cryptography and networking libraries are portable.
Why do we need something new?
We feel that we've reached the limits of how much we can improve our existing stack, without doing very significant changes upstream. While we have already contributed substantially upstream, our needs are changing. We don't wish to place the burden on open-source developers to adopt the interfaces we need, nor to implement it for us. We want to contribute back to the community.
Proposed Architecture
We're proposing a modular architecture which features:
A low-level OAuth package, inspired by the oauth4webapi project which provides the generic OAuth 2.1 building blocks but doesn't provide an out-of-the-box OAuth Client.
An AT Protocol OAuth Client, with runtime specific implementations of dependencies for different platforms:
This design is similar to the official typescript SDKs where
@atproto/oauth-clientis used by both@atproto/oauth-client-nodeand@atproto/oauth-client-browser, and both receive runtime implementations for certain things.The AT Protocol OAuth Client is provided with Handle and DID Document Resolvers via an ergonomic interface. For our implementation of both of these resolvers, we plan to use a service like slingshot, as to avoid working with JSON-LD and DNS resolution in Swift, both of which have been pain points in the past.
An AT Protocol Client for interacting with XRPC APIs, inspired by the new Lex Client package. This package will be fairly low-level, providing the way through which to make requests to the PDS and other services.
An AT Protocol Lexicon library, potentially with support for taking the lexicon JSON files and generating Swift code in the future.
As a diagram, the proposed architecture looks something like this:
This architecture was designed by Emelia Smith, who has been helping Germ with our OAuth and AT Protocol implementations.
The proposed architecture in more detail:
The OAuth Client will take care of making sure we follow AT Protocol's OAuth profile (née specification). That means you won't need to worry about PAR, PKCE, DPoP, and all those complicated parts of the OAuth profile.
When you use the OAuth Client to authorize or restore a session, you receive back an OAuth Session, which follows a "agent" interface – essentially an "agent" provides a method for making HTTP requests, and agents can also be unauthenticated too.
When authenticated, the OAuth Session agent takes care of handling token refreshes and sending requests to the authenticated accounts' PDS. The session is serializable to an archive, and manages state like the issuing authorization server, the authorized identity, access token, refresh token, and DPoP private key.
The OAuth Session agent is also responsible for retrying on expected failures, such as missing DPoP Nonce or expired access tokens (provided we can refresh the access token successfully).
The OAuth Client requires that you supply the following dependencies:
A resolver for handle and DID document resolution & retrieval
A HTTP Client for making requests
A User Authenticator for performing the OAuth Authorization Code grant flow
The User Authenticator is used for showing the Web UI during the OAuth Authorization Code grant flow using either ASWebAuthentication (iOS) or AuthTabIntent (android).
We'll provide a generic implementation of each of these dependencies, but you can always bring your own implementation that suits you.
Persistence of OAuth Sessions is left to users of the OAuth Client, as each application may have its own way to persist that data securely. Some common implementations can be built using the secure storage APIs on each platform, e.g. keychain on iOS.
The AT Protocol Client delegates request handling to an Agent, which may be authenticated. The proposed AT Protocol Client API is very low level:
Making XRPC requests
CRUD operations for records
Blob operations
Service proxying & labeler support (via the HTTP Headers used), for calling AppView XRPC APIs.
Verification of records via the com.atproto.sync.getRecord endpoint.
Swift Patterns
Thread safety with Swift concurrency, using actors to protect mutable session state, and propagating saves out of the actor with AsyncStream
We make quite a lot of usage of protocols for defining interfaces. This
Helps to constrain the public API we expose to the client
Allows for substitution of implementations. For example, in place of the AsyncStream save propagation of our provided OAuth Session implementation, you can provide an implementation of the Actor protocol
OAuthSessionCapabilitiesthat uses SwiftData ModelActor for persistence.
Germ Plans
We've started to building this, but we want your feedback on the overall design. It's not quite ready yet for general usage, but you're welcome to be an early adopter before we write the documentation (no support provided).
We intend to defer handle and DID resolution to a caching service like slingshot by Microcosm.
We intend to implement mobile confidential client authentication with the client assertion backend pattern, where the backend checks the instance of the client application with platform attestations ( App Attest or Play Integrity API)
We want to explore a codegen approach to deriving swift types from lexicons:
We look forward to your feedback. Find us or hello@germnetwork.com.