Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions LoginKitExample/LoginKitExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

/* Begin PBXBuildFile section */
E85C97D82D4D7B190062D747 /* XcodesLoginKit in Frameworks */ = {isa = PBXBuildFile; productRef = E85C97D72D4D7B190062D747 /* XcodesLoginKit */; };
E8937CD82D4D71AD007C96DE /* XcodesLoginKit in Frameworks */ = {isa = PBXBuildFile; productRef = E8937CD72D4D71AD007C96DE /* XcodesLoginKit */; };
E8937CE02D4D7A34007C96DE /* XcodesLoginKit in Frameworks */ = {isa = PBXBuildFile; productRef = E8937CDF2D4D7A34007C96DE /* XcodesLoginKit */; };
E8937CD82D4D71AD007C96DE /* XcodesLoginKitSecurityKey in Frameworks */ = {isa = PBXBuildFile; productRef = E8937CD72D4D71AD007C96DE /* XcodesLoginKitSecurityKey */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -58,9 +57,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E8937CD82D4D71AD007C96DE /* XcodesLoginKit in Frameworks */,
E8937CD82D4D71AD007C96DE /* XcodesLoginKitSecurityKey in Frameworks */,
E85C97D82D4D7B190062D747 /* XcodesLoginKit in Frameworks */,
E8937CE02D4D7A34007C96DE /* XcodesLoginKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -121,8 +119,7 @@
);
name = LoginKitExample;
packageProductDependencies = (
E8937CD72D4D71AD007C96DE /* XcodesLoginKit */,
E8937CDF2D4D7A34007C96DE /* XcodesLoginKit */,
E8937CD72D4D71AD007C96DE /* XcodesLoginKitSecurityKey */,
E85C97D72D4D7B190062D747 /* XcodesLoginKit */,
);
productName = LoginKitExample;
Expand Down Expand Up @@ -577,13 +574,9 @@
isa = XCSwiftPackageProductDependency;
productName = XcodesLoginKit;
};
E8937CD72D4D71AD007C96DE /* XcodesLoginKit */ = {
E8937CD72D4D71AD007C96DE /* XcodesLoginKitSecurityKey */ = {
isa = XCSwiftPackageProductDependency;
productName = XcodesLoginKit;
};
E8937CDF2D4D7A34007C96DE /* XcodesLoginKit */ = {
isa = XCSwiftPackageProductDependency;
productName = XcodesLoginKit;
productName = XcodesLoginKitSecurityKey;
};
/* End XCSwiftPackageProductDependency section */
};
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions LoginKitExample/LoginKitExample/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import XcodesLoginKit
import XcodesLoginKitSecurityKey
import SwiftUI

@Observable
Expand All @@ -23,8 +24,8 @@ class AppState {

Task {
do {
let autheticationState = try await client.srpLogin(accountName: username, password: password)
handleAuthenticationFlowCompletion(autheticationState)
let authenticationState = try await client.authenticationState(accountName: username, password: password)
handleAuthenticationFlowCompletion(authenticationState)
isProcessingAuthRequest = false
}
catch {
Expand All @@ -39,14 +40,16 @@ class AppState {
switch authenticationState {
case .unauthenticated:
authError = AuthenticationError.notAuthorized
case .waitingForFederatedAuthentication:
authError = AuthenticationError.federatedAuthenticationRequired
case let .waitingForSecondFactor(twoFactorOption, authOptionsResponse, appleSessionData):
self.presentedSheet = .twoFactor(.init(
option: twoFactorOption,
authOptions: authOptionsResponse,
sessionData: AppleSessionData(serviceKey: appleSessionData.serviceKey, sessionID: appleSessionData.sessionID, scnt: appleSessionData.scnt)
))
case .authenticated(let appleSession):
print("SUCCESSFULLY LOGGED IN - WELCOME: \(appleSession.user.fullName)")
print("SUCCESSFULLY LOGGED IN - WELCOME: \(appleSession.user.fullName ?? "Apple Developer")")
self.presentedSheet = nil
break
case .notAppleDeveloper:
Expand Down Expand Up @@ -114,9 +117,7 @@ class AppState {
}

func cancelSecurityKeyAssertationRequest() {
Task {
await client.cancelSecurityKeyAssertationRequest()
}
client.cancelSecurityKeyAssertationRequest()
}
}
enum XcodesSheet: Identifiable {
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,38 @@ case .unauthenticated, .notAppleDeveloper:
}
```

`Client` resolves Apple's public sign-in widget key in this order:

1. An explicit key supplied by the application, when present.
2. A key previously saved in XcodesLoginKit's in-memory or on-disk cache.
3. The `widgetKey` in App Store Connect's unauthenticated `/logout` redirect.
4. The legacy App Store Connect Olympus configuration endpoint as a final fallback.

The sign-out lookup uses a separate cookie-free session and does not follow the redirect. Following
that redirect would perform a real sign-out, so the authentication session is never used for this
request.

Supply an explicit key without changing the library:

```swift
let client = Client(serviceKeyProvider: .fixed("current-public-widget-key"))
```

For dynamic configuration, supply an asynchronous, `Sendable` loader:

```swift
let client = Client(
serviceKeyProvider: AppleServiceKeyProvider {
try await configuration.appleServiceKey()
}
)
```

Successful automatic lookups are cached on a best-effort basis. Cache read or write failures do not
block authentication. If every network source fails, the client throws
`AuthenticationError.serviceKeyResolutionFailed(attempts:)`, whose localized description includes
the source-specific failures and HTTP status codes when available.

### Main flow

1. Create a `Client`. Pass a custom `URLSession` if you want isolated cookie storage.
Expand Down
78 changes: 78 additions & 0 deletions Sources/XcodesLoginKit/AppleServiceKeyProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/// Supplies an explicit Apple widget key for Apple ID authentication requests.
///
/// When a provider is present, XcodesLoginKit tries it before consulting its cache or Apple's
/// current App Store Connect key sources. The loader is asynchronous so applications can source
/// the value from their own configuration service.
public struct AppleServiceKeyProvider: Sendable {
public typealias Loader = @Sendable () async throws -> String

private let load: Loader

/// Creates a provider backed by an asynchronous loader.
public init(load: @escaping Loader) {
self.load = load
}

/// Loads the service key.
public func serviceKey() async throws -> String {
try await load()
}

/// Returns a provider that always supplies the given service key.
public static func fixed(_ serviceKey: String) -> Self {
Self { serviceKey }
}
}

/// Sources XcodesLoginKit can use when resolving Apple's public sign-in service key.
public enum AppleServiceKeySource: String, Equatable, Sendable {
/// A key supplied explicitly by the application.
case supplied
/// A key read from XcodesLoginKit's best-effort cache.
case cache
/// A key read from App Store Connect's unauthenticated sign-out redirect.
case appStoreConnectSignOut
/// A key read from App Store Connect's legacy Olympus configuration endpoint.
case olympus
}

/// Why an Apple service-key source did not produce a usable key.
public enum AppleServiceKeyFailure: Swift.Error, Equatable, Sendable {
/// The source could not be reached.
case network(description: String)
/// The response was not an HTTP response.
case invalidResponse
/// The source returned an HTTP error.
case httpStatus(code: Int, bodyPreview: String?)
/// The App Store Connect sign-out response did not contain a redirect.
case missingRedirect
/// The sign-out redirect could not be parsed.
case invalidRedirect
/// The source returned a response without a service key.
case missingKey

/// Whether retrying this failure later may succeed without an application update.
public var isRetryable: Bool {
switch self {
case .network:
return true
case let .httpStatus(code, _):
return code == 429 || code >= 500
case .invalidResponse, .missingRedirect, .invalidRedirect, .missingKey:
return false
}
}
}

/// A failed attempt to resolve Apple's public sign-in service key.
public struct AppleServiceKeyAttempt: Equatable, Sendable {
/// The source that was attempted.
public let source: AppleServiceKeySource
/// The reason the source did not produce a usable key.
public let failure: AppleServiceKeyFailure

public init(source: AppleServiceKeySource, failure: AppleServiceKeyFailure) {
self.source = source
self.failure = failure
}
}
Loading
Loading