···11+# Build a Bluesky Login Flow
22+33+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.
44+55+## Add the package to your app
66+77+1. In your app target's `Package.swift`, add the CoreATProtocol dependency:
88+99+ ```swift
1010+ .package(url: "https://github.com/your-org/CoreATProtocol.git", from: "1.0.0")
1111+ ```
1212+1313+2. List ``CoreATProtocol`` in the target's dependencies:
1414+1515+ ```swift
1616+ .target(
1717+ name: "App",
1818+ dependencies: [
1919+ .product(name: "CoreATProtocol", package: "CoreATProtocol")
2020+ ]
2121+ )
2222+ ```
2323+2424+3. Import the module where you coordinate authentication:
2525+2626+ ```swift
2727+ import CoreATProtocol
2828+ ```
2929+3030+## Persist a DPoP key
3131+3232+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.
3333+3434+```swift
3535+import CryptoKit
3636+import JWTKit
3737+3838+final class DPoPKeyStore {
3939+ private let keyTag = "com.example.app.dpop"
4040+4141+ func loadOrCreateKey() throws -> ES256PrivateKey {
4242+ if let raw = try loadKeyData() {
4343+ return try ES256PrivateKey(pem: raw)
4444+ }
4545+4646+ let key = ES256PrivateKey()
4747+ try persist(key.pemRepresentation)
4848+ return key
4949+ }
5050+5151+ private func loadKeyData() throws -> String? {
5252+ // Read from the Keychain and return the PEM string if it exists.
5353+ nil
5454+ }
5555+5656+ private func persist(_ pem: String) throws {
5757+ // Write the PEM string to the Keychain.
5858+ }
5959+}
6060+```
6161+6262+## Expose a DPoP JWT generator
6363+6464+Wrap the signing key with ``DPoPJWTGenerator`` so the library can mint proofs on demand.
6565+6666+```swift
6767+let keyStore = DPoPKeyStore()
6868+let privateKey = try await keyStore.loadOrCreateKey()
6969+let dpopGenerator = try await DPoPJWTGenerator(privateKey: privateKey)
7070+let jwtGenerator = dpopGenerator.jwtGenerator()
7171+```
7272+7373+Pass ``DPoPJWTGenerator.jwtGenerator()`` to ``LoginService`` and later to ``applyAuthenticationContext(login:generator:resourceNonce:)`` so API calls share the same key material.
7474+7575+## Configure login storage
7676+7777+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.
7878+7979+```swift
8080+import OAuthenticator
8181+8282+struct BlueskyLoginStore {
8383+ func makeStorage() -> LoginStorage {
8484+ LoginStorage {
8585+ try await loadLogin()
8686+ } storeLogin: { login in
8787+ try await persist(login)
8888+ }
8989+ }
9090+9191+ private func loadLogin() async throws -> Login? {
9292+ // Decode and return the previously stored login if one exists.
9393+ nil
9494+ }
9595+9696+ private func persist(_ login: Login) async throws {
9797+ // Save the login (for example, in the Keychain or the file system).
9898+ }
9999+}
100100+```
101101+102102+## Perform the OAuth flow
103103+104104+1. Configure shared environment state early in your app lifecycle:
105105+106106+ ```swift
107107+ await setup(
108108+ hostURL: "https://bsky.social",
109109+ accessJWT: nil,
110110+ refreshJWT: nil,
111111+ delegate: self
112112+ )
113113+ ```
114114+115115+2. Create the services needed for authentication:
116116+117117+ ```swift
118118+ let loginStorage = BlueskyLoginStore().makeStorage()
119119+ let loginService = LoginService(jwtGenerator: jwtGenerator, loginStorage: loginStorage)
120120+ ```
121121+122122+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).
123123+124124+ ```swift
125125+ let login = try await loginService.login(
126126+ account: "did:plc:your-user",
127127+ clientMetadataEndpoint: "https://example.com/.well-known/coreatprotocol-client.json"
128128+ )
129129+ ```
130130+131131+4. Share the authentication context with CoreATProtocol so the networking layer can add DPoP proofs automatically:
132132+133133+ ```swift
134134+ await applyAuthenticationContext(login: login, generator: jwtGenerator)
135135+ ```
136136+137137+5. When Bluesky returns a new DPoP nonce (`DPoP-Nonce` header), call ``updateResourceDPoPNonce(_:)`` with the latest value before the next request.
138138+139139+6. To sign the user out, call ``clearAuthenticationContext()`` and erase any stored login and keychain items.
140140+141141+## Make API requests
142142+143143+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.
144144+145145+```swift
146146+var router = NetworkRouter<SomeEndpoint>(decoder: .atDecoder)
147147+router.delegate = await APEnvironment.current.routerDelegate
148148+```
149149+150150+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.
151151+152152+## Troubleshooting
153153+154154+- Ensure the DPoP key persists across app launches. If the key changes, all tokens issued by Bluesky become invalid and the user must reauthenticate.
155155+- Always call ``applyAuthenticationContext(login:generator:resourceNonce:)`` after refreshing tokens via ``updateTokens(access:refresh:)`` or custom flows so the delegate has current credentials.
156156+- If Bluesky rejects requests with `use_dpop_nonce`, update the cached value via ``updateResourceDPoPNonce(_:)`` and retry.
157157+