Skip to content

Commit 0148c4d

Browse files
authored
Merge pull request #1 from XcodesOrg/concurrency-shared-xcodeskit
XcodesLogin Kit v 1.0
2 parents e31b43c + 4de8e5c commit 0148c4d

22 files changed

Lines changed: 1785 additions & 86 deletions

.github/release-drafter.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
categories:
2+
- title: '🚀 Enhancements'
3+
labels:
4+
- 'enhancement'
5+
- title: '🐛 Bug Fixes'
6+
labels:
7+
- 'bugfix'
8+
- title: '🌎 Localization'
9+
labels:
10+
- 'localization'
11+
- title: '🧰 Maintenance'
12+
labels:
13+
- 'chore'
14+
- 'documentation'
15+
- 'dependencies'
16+
template: |
17+
## Changes
18+
19+
$CHANGES
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Release Drafter
2+
3+
on:
4+
# Allow running it manually in case we forget to label a PR before merging
5+
workflow_dispatch:
6+
push:
7+
branches:
8+
- main
9+
10+
jobs:
11+
update_release_draft:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: release-drafter/release-drafter@v6
15+
env:
16+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Package.resolved

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ let package = Package(
1111
.library(
1212
name: "XcodesLoginKit",
1313
targets: ["XcodesLoginKit"]),
14+
.library(
15+
name: "XcodesLoginKitSecurityKey",
16+
targets: ["XcodesLoginKitSecurityKey"]),
1417
],
1518
dependencies: [
1619
.package(url: "https://github.com/XcodesOrg/swift-srp", branch: "main"),
1720
.package(url: "https://github.com/XcodesOrg/AsyncHTTPNetworkService", branch: "main"),
18-
.package(url: "https://github.com/kinoroy/LibFido2Swift", from: "0.1.4")
21+
.package(url: "https://github.com/kinoroy/LibFido2Swift", from: "0.1.4"),
22+
.package(url: "https://github.com/jpsim/Yams", .upToNextMinor(from: "5.0.1")),
1923
],
2024
targets: [
2125
// Targets are the basic building blocks of a package, defining a module or a test suite.
@@ -25,13 +29,24 @@ let package = Package(
2529
dependencies: [
2630
.product(name: "SRP", package: "swift-srp"),
2731
.product(name: "AsyncNetworkService", package: "AsyncHTTPNetworkService"),
28-
.product(name: "LibFido2Swift", package: "libfido2swift")
32+
"Yams",
2933
],
3034
path: "./Sources")
3135
,
36+
.target(
37+
name: "XcodesLoginKitSecurityKey",
38+
dependencies: [
39+
"XcodesLoginKit",
40+
.product(name: "LibFido2Swift", package: "libfido2swift")
41+
],
42+
path: "./SourcesSecurityKey"
43+
),
3244
.testTarget(
3345
name: "XcodesLoginKitTests",
34-
dependencies: ["XcodesLoginKit"]
46+
dependencies: ["XcodesLoginKit"],
47+
resources: [
48+
.copy("Fixtures"),
49+
]
3550
),
3651
]
3752
)

README.md

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,130 @@
11
## XcodesLoginKit
22

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.
44

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

Comments
 (0)