this repo has no description
1# Build a Bluesky Login Flow
2
3Learn 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
71. 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
132. 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
243. Import the module where you coordinate authentication:
25
26 ```swift
27 import CoreATProtocol
28 ```
29
30## Persist a DPoP key
31
32Bluesky 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
35import CryptoKit
36import JWTKit
37
38final 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
64Wrap the signing key with ``DPoPJWTGenerator`` so the library can mint proofs on demand.
65
66```swift
67let keyStore = DPoPKeyStore()
68let privateKey = try await keyStore.loadOrCreateKey()
69let dpopGenerator = try await DPoPJWTGenerator(privateKey: privateKey)
70let jwtGenerator = dpopGenerator.jwtGenerator()
71```
72
73Pass ``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
77Provide 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
80import OAuthenticator
81
82struct 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
1041. 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
1152. Create the services needed for authentication:
116
117 ```swift
118 let loginStorage = BlueskyLoginStore().makeStorage()
119 let loginService = LoginService(jwtGenerator: jwtGenerator, loginStorage: loginStorage)
120 ```
121
1223. 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
1314. 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
1375. When Bluesky returns a new DPoP nonce (`DPoP-Nonce` header), call ``updateResourceDPoPNonce(_:)`` with the latest value before the next request.
138
1396. To sign the user out, call ``clearAuthenticationContext()`` and erase any stored login and keychain items.
140
141## Make API requests
142
143Attach 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
146var router = NetworkRouter<SomeEndpoint>(decoder: .atDecoder)
147router.delegate = await APEnvironment.current.routerDelegate
148```
149
150With 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