# Build a Bluesky Login Flow Learn how an iOS app can depend on ``CoreATProtocol`` and guide a user through the AT Protocol OAuth flow using Bluesky as the authorization server. ## Add the package to your app 1. In your app target's `Package.swift`, add the CoreATProtocol dependency: ```swift .package(url: "https://github.com/your-org/CoreATProtocol.git", from: "1.0.0") ``` 2. List ``CoreATProtocol`` in the target's dependencies: ```swift .target( name: "App", dependencies: [ .product(name: "CoreATProtocol", package: "CoreATProtocol") ] ) ``` 3. Import the module where you coordinate authentication: ```swift import CoreATProtocol ``` ## Persist a DPoP key Bluesky issues DPoP-bound access tokens, so the app must generate and persist a single ES256 key pair. The example below stores the private key in the Keychain and recreates it when needed. ```swift import CryptoKit import JWTKit final class DPoPKeyStore { private let keyTag = "com.example.app.dpop" func loadOrCreateKey() throws -> ES256PrivateKey { if let raw = try loadKeyData() { return try ES256PrivateKey(pem: raw) } let key = ES256PrivateKey() try persist(key.pemRepresentation) return key } private func loadKeyData() throws -> String? { // Read from the Keychain and return the PEM string if it exists. nil } private func persist(_ pem: String) throws { // Write the PEM string to the Keychain. } } ``` ## Expose a DPoP JWT generator Wrap the signing key with ``DPoPJWTGenerator`` so the library can mint proofs on demand. ```swift let keyStore = DPoPKeyStore() let privateKey = try await keyStore.loadOrCreateKey() let dpopGenerator = try await DPoPJWTGenerator(privateKey: privateKey) let jwtGenerator = dpopGenerator.jwtGenerator() ``` Pass ``DPoPJWTGenerator.jwtGenerator()`` to ``LoginService`` and later to ``applyAuthenticationContext(login:generator:resourceNonce:)`` so API calls share the same key material. ## Configure login storage Provide a ``LoginStorage`` implementation that reads and writes the user’s Bluesky session securely. The storage runs on the calling actor, so use async APIs. ```swift import OAuthenticator struct BlueskyLoginStore { func makeStorage() -> LoginStorage { LoginStorage { try await loadLogin() } storeLogin: { login in try await persist(login) } } private func loadLogin() async throws -> Login? { // Decode and return the previously stored login if one exists. nil } private func persist(_ login: Login) async throws { // Save the login (for example, in the Keychain or the file system). } } ``` ## Perform the OAuth flow 1. Configure shared environment state early in your app lifecycle: ```swift await setup( hostURL: "https://bsky.social", accessJWT: nil, refreshJWT: nil, delegate: self ) ``` 2. Create the services needed for authentication: ```swift let loginStorage = BlueskyLoginStore().makeStorage() let loginService = LoginService(jwtGenerator: jwtGenerator, loginStorage: loginStorage) ``` 3. Start the Bluesky OAuth flow. Use the client metadata URL registered with the Authorization Server (for example, the one served from your app’s hosted metadata file). ```swift let login = try await loginService.login( account: "did:plc:your-user", clientMetadataEndpoint: "https://example.com/.well-known/coreatprotocol-client.json" ) ``` 4. Share the authentication context with CoreATProtocol so the networking layer can add DPoP proofs automatically: ```swift await applyAuthenticationContext(login: login, generator: jwtGenerator) ``` 5. When Bluesky returns a new DPoP nonce (`DPoP-Nonce` header), call ``updateResourceDPoPNonce(_:)`` with the latest value before the next request. 6. To sign the user out, call ``clearAuthenticationContext()`` and erase any stored login and keychain items. ## Make API requests Attach the package’s router delegate to your networking stack (for example, the client that wraps ``URLSession``) so that access tokens and DPoP proofs are injected into outgoing requests. ```swift var router = NetworkRouter(decoder: .atDecoder) router.delegate = await APEnvironment.current.routerDelegate ``` With the context applied, subsequent calls through ``APRouterDelegate`` will refresh DPoP proofs, hash access tokens into the `ath` claim, and keep the nonce in sync with the server. ## Troubleshooting - Ensure the DPoP key persists across app launches. If the key changes, all tokens issued by Bluesky become invalid and the user must reauthenticate. - Always call ``applyAuthenticationContext(login:generator:resourceNonce:)`` after refreshing tokens via ``updateTokens(access:refresh:)`` or custom flows so the delegate has current credentials. - If Bluesky rejects requests with `use_dpop_nonce`, update the cached value via ``updateResourceDPoPNonce(_:)`` and retry.