···1+# Build a Bluesky Login Flow
2+3+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.
4+5+## Add the package to your app
6+7+1. In your app target's `Package.swift`, add the CoreATProtocol dependency:
8+9+ ```swift
10+ .package(url: "https://github.com/your-org/CoreATProtocol.git", from: "1.0.0")
11+ ```
12+13+2. List ``CoreATProtocol`` in the target's dependencies:
14+15+ ```swift
16+ .target(
17+ name: "App",
18+ dependencies: [
19+ .product(name: "CoreATProtocol", package: "CoreATProtocol")
20+ ]
21+ )
22+ ```
23+24+3. Import the module where you coordinate authentication:
25+26+ ```swift
27+ import CoreATProtocol
28+ ```
29+30+## Persist a DPoP key
31+32+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.
33+34+```swift
35+import CryptoKit
36+import JWTKit
37+38+final class DPoPKeyStore {
39+ private let keyTag = "com.example.app.dpop"
40+41+ func loadOrCreateKey() throws -> ES256PrivateKey {
42+ if let raw = try loadKeyData() {
43+ return try ES256PrivateKey(pem: raw)
44+ }
45+46+ let key = ES256PrivateKey()
47+ try persist(key.pemRepresentation)
48+ return key
49+ }
50+51+ private func loadKeyData() throws -> String? {
52+ // Read from the Keychain and return the PEM string if it exists.
53+ nil
54+ }
55+56+ private func persist(_ pem: String) throws {
57+ // Write the PEM string to the Keychain.
58+ }
59+}
60+```
61+62+## Expose a DPoP JWT generator
63+64+Wrap the signing key with ``DPoPJWTGenerator`` so the library can mint proofs on demand.
65+66+```swift
67+let keyStore = DPoPKeyStore()
68+let privateKey = try await keyStore.loadOrCreateKey()
69+let dpopGenerator = try await DPoPJWTGenerator(privateKey: privateKey)
70+let jwtGenerator = dpopGenerator.jwtGenerator()
71+```
72+73+Pass ``DPoPJWTGenerator.jwtGenerator()`` to ``LoginService`` and later to ``applyAuthenticationContext(login:generator:resourceNonce:)`` so API calls share the same key material.
74+75+## Configure login storage
76+77+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.
78+79+```swift
80+import OAuthenticator
81+82+struct BlueskyLoginStore {
83+ func makeStorage() -> LoginStorage {
84+ LoginStorage {
85+ try await loadLogin()
86+ } storeLogin: { login in
87+ try await persist(login)
88+ }
89+ }
90+91+ private func loadLogin() async throws -> Login? {
92+ // Decode and return the previously stored login if one exists.
93+ nil
94+ }
95+96+ private func persist(_ login: Login) async throws {
97+ // Save the login (for example, in the Keychain or the file system).
98+ }
99+}
100+```
101+102+## Perform the OAuth flow
103+104+1. Configure shared environment state early in your app lifecycle:
105+106+ ```swift
107+ await setup(
108+ hostURL: "https://bsky.social",
109+ accessJWT: nil,
110+ refreshJWT: nil,
111+ delegate: self
112+ )
113+ ```
114+115+2. Create the services needed for authentication:
116+117+ ```swift
118+ let loginStorage = BlueskyLoginStore().makeStorage()
119+ let loginService = LoginService(jwtGenerator: jwtGenerator, loginStorage: loginStorage)
120+ ```
121+122+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).
123+124+ ```swift
125+ let login = try await loginService.login(
126+ account: "did:plc:your-user",
127+ clientMetadataEndpoint: "https://example.com/.well-known/coreatprotocol-client.json"
128+ )
129+ ```
130+131+4. Share the authentication context with CoreATProtocol so the networking layer can add DPoP proofs automatically:
132+133+ ```swift
134+ await applyAuthenticationContext(login: login, generator: jwtGenerator)
135+ ```
136+137+5. When Bluesky returns a new DPoP nonce (`DPoP-Nonce` header), call ``updateResourceDPoPNonce(_:)`` with the latest value before the next request.
138+139+6. To sign the user out, call ``clearAuthenticationContext()`` and erase any stored login and keychain items.
140+141+## Make API requests
142+143+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.
144+145+```swift
146+var router = NetworkRouter<SomeEndpoint>(decoder: .atDecoder)
147+router.delegate = await APEnvironment.current.routerDelegate
148+```
149+150+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.
151+152+## Troubleshooting
153+154+- Ensure the DPoP key persists across app launches. If the key changes, all tokens issued by Bluesky become invalid and the user must reauthenticate.
155+- Always call ``applyAuthenticationContext(login:generator:resourceNonce:)`` after refreshing tokens via ``updateTokens(access:refresh:)`` or custom flows so the delegate has current credentials.
156+- If Bluesky rejects requests with `use_dpop_nonce`, update the cached value via ``updateResourceDPoPNonce(_:)`` and retry.
157+