Skip to content

Commit

Permalink
AppleAuthentication update.
Browse files Browse the repository at this point in the history
  • Loading branch information
borut-t committed Jan 22, 2025
1 parent 56872e6 commit a108b50
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 28 deletions.
17 changes: 10 additions & 7 deletions Sources/Apple/AppleAuthenticator+Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ public extension AppleAuthenticator {
}
}

struct Email: Codable {
public let address: String
public let isPrivate: Bool
public let isVerified: Bool
}

enum Error: Swift.Error {
case system(_ error: Swift.Error)
case cancelled
Expand All @@ -39,12 +45,9 @@ public extension AppleAuthenticator {
case missingExpiration
case missingEmail
}
}

public extension AppleAuthenticator.Response {
struct Email {
public let address: String
public let isPrivate: Bool
public let isVerified: Bool

struct UserData: Codable {
let name: PersonNameComponents?
let email: Email
}
}
68 changes: 47 additions & 21 deletions Sources/Apple/AppleAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ public final class AppleAuthenticator: NSObject {
private let storageUserIdKey = "signIn.userId"
private let storageAuthenticatedKey = "authenticated"
private let provider: ASAuthorizationAppleIDProvider
private let keychainService: KeychainService = .init(service: "povioKit.auth")
private let keychainServiceDataKey: String = "user.data"
private var continuation: CheckedContinuation<Response, Swift.Error>?

public init(storage: UserDefaults? = nil) {
self.provider = .init()
self.storage = storage ?? .init(suiteName: "povioKit.auth.apple") ?? .standard
Expand All @@ -37,15 +39,15 @@ extension AppleAuthenticator: Authenticator {
public func signIn(from presentingViewController: UIViewController) async throws -> Response {
try await appleSignIn(on: presentingViewController, with: nil)
}

/// SignIn user with `nonce` value
///
/// Nonce is usually needed when doing auth with an external auth provider (e.g. firebase).
/// Will asynchronously return the `Response` object on success or `Error` on error.
public func signIn(from presentingViewController: UIViewController, with nonce: Nonce) async throws -> Response {
try await appleSignIn(on: presentingViewController, with: nonce)
}

/// Clears the signIn footprint and logs out the user immediatelly.
public func signOut() {
storage.removeObject(forKey: storageUserIdKey)
Expand All @@ -57,11 +59,15 @@ extension AppleAuthenticator: Authenticator {
public var isAuthenticated: Authenticated {
storage.string(forKey: storageUserIdKey) != nil && storage.bool(forKey: storageAuthenticatedKey)
}

/// Boolean if given `url` should be handled.
///
/// Call this from UIApplicationDelegate’s `application:openURL:options:` method.
public func canOpenUrl(_ url: URL, application: UIApplication, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool {
public func canOpenUrl(
_ url: URL,
application: UIApplication,
options: [UIApplication.OpenURLOptionsKey : Any]
) -> Bool {
false
}

Expand All @@ -76,7 +82,10 @@ extension AppleAuthenticator: Authenticator {

// MARK: - ASAuthorizationControllerDelegate
extension AppleAuthenticator: ASAuthorizationControllerDelegate {
public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
public func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
switch authorization.credential {
case let credential as ASAuthorizationAppleIDCredential:
guard let authCodeData = credential.authorizationCode,
Expand All @@ -93,38 +102,53 @@ extension AppleAuthenticator: ASAuthorizationControllerDelegate {

// parse email and related metadata
let jwt = try? JWTDecoder(token: identityTokenString)
let email: Response.Email? = (credential.email ?? jwt?.string(for: "email")).map {
var email: Email? = (credential.email ?? jwt?.string(for: "email")).map {
let isEmailPrivate = jwt?.bool(for: "is_private_email") ?? false
let isEmailVerified = jwt?.bool(for: "email_verified") ?? false
return .init(address: $0, isPrivate: isEmailPrivate, isVerified: isEmailVerified)
}

// load email from keychain on subsequent logins
let existingUserData: UserData? = keychainService.read(UserData.self, for: keychainServiceDataKey)
if email == nil, let existingUserData {
email = existingUserData.email
}

// do not continue if `email` is missing
guard let email else {
guard let email, !email.address.isEmpty else {
rejectSignIn(with: .missingEmail)
return
}

// save user data for the future logins
let updatedUserData: UserData = .init(name: credential.fullName, email: email)
try? keychainService.save(updatedUserData, for: keychainServiceDataKey)

// do not continue if `expiresAt` is missing
guard let expiresAt = jwt?.expiresAt else {
rejectSignIn(with: .missingExpiration)
return
}

let response = Response(userId: credential.user,
token: identityTokenString,
authCode: authCode,
nameComponents: credential.fullName,
email: email,
expiresAt: expiresAt)
let response = Response(
userId: credential.user,
token: identityTokenString,
authCode: authCode,
nameComponents: updatedUserData.name,
email: updatedUserData.email,
expiresAt: expiresAt
)

continuation?.resume(with: .success(response))
case _:
rejectSignIn(with: .unhandledAuthorization)
}
}

public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Swift.Error) {
public func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: Swift.Error
) {
switch error {
case let err as ASAuthorizationError where err.code == .canceled:
rejectSignIn(with: .cancelled)
Expand All @@ -139,7 +163,7 @@ private extension AppleAuthenticator {
func appleSignIn(on presentingViewController: UIViewController, with nonce: Nonce?) async throws -> Response {
let request = provider.createRequest()
request.requestedScopes = [.fullName, .email]

switch nonce {
case .random(let length):
guard length > 0 else {
Expand All @@ -151,7 +175,7 @@ private extension AppleAuthenticator {
case .none:
break
}

return try await withCheckedThrowingContinuation { continuation in
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
Expand All @@ -162,10 +186,12 @@ private extension AppleAuthenticator {
}

func setupCredentialsRevokeListener() {
NotificationCenter.default.addObserver(self,
selector: #selector(appleCredentialRevoked),
name: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(appleCredentialRevoked),
name: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
object: nil
)
}

func rejectSignIn(with error: Error) {
Expand Down

0 comments on commit a108b50

Please sign in to comment.