this repo has no description
at oauth 157 lines 5.2 kB view raw view rendered
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