|
1 | 1 | ## XcodesLoginKit |
2 | 2 |
|
3 | | -An Swift API to log in to to Apple's developer website. |
| 3 | +A Swift API for signing in to Apple's developer services. |
4 | 4 |
|
5 | | -More to come. |
| 5 | +XcodesLoginKit handles Apple's SRP password login, federated Apple IDs, two-factor |
| 6 | +authentication, session validation, logout, and optional fastlane/Spaceship cookie import. |
| 7 | + |
| 8 | +### Getting started |
| 9 | + |
| 10 | +Add the package to your `Package.swift`: |
| 11 | + |
| 12 | +```swift |
| 13 | +.package(url: "https://github.com/XcodesOrg/XcodesLoginKit", branch: "main") |
| 14 | +``` |
| 15 | + |
| 16 | +Then add the product you need: |
| 17 | + |
| 18 | +```swift |
| 19 | +.product(name: "XcodesLoginKit", package: "XcodesLoginKit") |
| 20 | +``` |
| 21 | + |
| 22 | +Use `Client` when your app wants to drive its own sign-in UI: |
| 23 | + |
| 24 | +```swift |
| 25 | +import Foundation |
| 26 | +import XcodesLoginKit |
| 27 | + |
| 28 | +let client = Client() |
| 29 | + |
| 30 | +let state = try await client.authenticationState( |
| 31 | + accountName: "name@example.com", |
| 32 | + password: password |
| 33 | +) |
| 34 | + |
| 35 | +switch state { |
| 36 | +case .authenticated(let session): |
| 37 | + print("Signed in as \(session.user.fullName ?? "Apple Developer")") |
| 38 | + |
| 39 | +case .waitingForFederatedAuthentication(let federation): |
| 40 | + guard let idpURL = federation.idpURL else { |
| 41 | + throw AuthenticationError.federatedAuthenticationRequired |
| 42 | + } |
| 43 | + |
| 44 | + // Open idpURL in a browser. After the user signs in, collect the redirected URL. |
| 45 | + let callbackURLString = "... pasted or received callback URL ..." |
| 46 | + try await client.validateFederatedCallbackURLString(callbackURLString) |
| 47 | + |
| 48 | +case .waitingForSecondFactor(let option, let authOptions, let sessionData): |
| 49 | + switch option { |
| 50 | + case .codeSent: |
| 51 | + let code = "... code from trusted device ..." |
| 52 | + try await client.submitSecurityCode(.device(code: code), sessionData: sessionData) |
| 53 | + |
| 54 | + case .smsPendingChoice: |
| 55 | + guard let phoneNumber = authOptions.trustedPhoneNumbers?.first else { return } |
| 56 | + try await client.requestSMSSecurityCode( |
| 57 | + to: phoneNumber, |
| 58 | + authOptions: authOptions, |
| 59 | + sessionData: sessionData |
| 60 | + ) |
| 61 | + |
| 62 | + case .smsSent(let phoneNumber): |
| 63 | + let code = "... code from SMS ..." |
| 64 | + try await client.submitSecurityCode( |
| 65 | + .sms(code: code, phoneNumberId: phoneNumber.id), |
| 66 | + sessionData: sessionData |
| 67 | + ) |
| 68 | + |
| 69 | + case .securityKey: |
| 70 | + // Add the XcodesLoginKitSecurityKey product and call |
| 71 | + // submitSecurityKeyPinCode(_:sessionData:authOptions:). |
| 72 | + break |
| 73 | + } |
| 74 | + |
| 75 | +case .unauthenticated, .notAppleDeveloper: |
| 76 | + break |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +### Main flow |
| 81 | + |
| 82 | +1. Create a `Client`. Pass a custom `URLSession` if you want isolated cookie storage. |
| 83 | +2. Call `authenticationState(accountName:password:)`. |
| 84 | +3. Switch on `AuthenticationState`. |
| 85 | +4. Complete any required federated authentication or second-factor challenge. |
| 86 | +5. Call `validateSession()` later to check whether stored cookies are still valid. |
| 87 | +6. Call `signout()` to clear the client's cookies. |
| 88 | + |
| 89 | +### Federated Apple IDs |
| 90 | + |
| 91 | +Federated accounts return `.waitingForFederatedAuthentication`. Open `FederationResponse.idpURL` |
| 92 | +in a browser, let the user finish identity-provider sign-in, then pass the final callback URL to |
| 93 | +`validateFederatedCallbackURL(_:)` or `validateFederatedCallbackURLString(_:)`. |
| 94 | + |
| 95 | +### Reusing fastlane sessions |
| 96 | + |
| 97 | +If the user already has a fastlane session, load its cookies into a session and pass that session |
| 98 | +to `Client`: |
| 99 | + |
| 100 | +```swift |
| 101 | +let loader = FastlaneSessionLoader() |
| 102 | +let session = try loader.session( |
| 103 | + fastlaneUser: "name@example.com", |
| 104 | + environmentValue: { ProcessInfo.processInfo.environment[$0] } |
| 105 | +) |
| 106 | + |
| 107 | +let client = Client(urlSession: session) |
| 108 | +let state = try await client.validateSession() |
| 109 | +``` |
| 110 | + |
| 111 | +Use `FastlaneSessionLoader.Constants.fastlaneSessionEnvVarName` as `fastlaneUser` to load cookies |
| 112 | +from `FASTLANE_SESSION` instead of `~/.fastlane/spaceship/<apple-id>/cookie`. |
| 113 | + |
| 114 | +### Higher-level session management |
| 115 | + |
| 116 | +`AppleSessionService` wraps the low-level client with dependency-injected environment lookup, |
| 117 | +keychain storage, prompts, browser opening, validation, login, and logout. It is a good fit for |
| 118 | +command-line tools that want a "login if needed" workflow while still controlling where credentials |
| 119 | +are stored and how users are prompted. |
| 120 | + |
| 121 | +### Security keys |
| 122 | + |
| 123 | +Add the `XcodesLoginKitSecurityKey` product when you need FIDO2 security-key support: |
| 124 | + |
| 125 | +```swift |
| 126 | +.product(name: "XcodesLoginKitSecurityKey", package: "XcodesLoginKit") |
| 127 | +``` |
| 128 | + |
| 129 | +When the login state is `.waitingForSecondFactor(.securityKey, authOptions, sessionData)`, call |
| 130 | +`submitSecurityKeyPinCode(_:sessionData:authOptions:)`. |
0 commit comments