class AuthentificationViewModel: ObservableObject { @Published var userSession: FirebaseAuth.User? @Published var user: User? @Published var isBadCredentials: Bool = false @Published var isEmailNotVerified: Bool = false @Published var errorMessage = "" private var db = Firestore.firestore() private var currentNonce: String? init() { Task { await fetchUser() } verifySignInWithAppleAuthenticationState() } } extension AuthentificationViewModel { func fetchUser() async { guard let uuid = Auth.auth().currentUser?.uid else { return } guard let snaphsot = try? await Firestore.firestore().collection("users").document(uuid).getDocument() else { return } self.user = try? snaphsot.data(as: User.self) self.userSession = Auth.auth().currentUser } func signIn(withEmail email: String, password: String) async throws { do { let result = try await Auth.auth().signIn(withEmail: email, password: password) if result.user.isEmailVerified { self.userSession = result.user await fetchUser() } else { isEmailNotVerified = true } } catch { isBadCredentials = true print("Failed to sign in with error \(error.localizedDescription)") } } func signOut() { do { try Auth.auth().signOut() // signs out user on backend self.userSession = nil // wipes out user session and go back to LoginView self.user = nil // wipes out current user data model } catch { print("Failed to sign out with error \(error.localizedDescription)") } } func deleteAccount() async -> Bool { guard let user = userSession else { return false } guard let lastSignInDate = user.metadata.lastSignInDate else { return false } let needsReauth = !lastSignInDate.isWithinPast(minutes: 5) let needsTokenRevocation = user.providerData.contains { $0.providerID == "apple.com" } do { if needsReauth || needsTokenRevocation { let signInWithApple = SignInWithApple() let appleIDCredential = try await signInWithApple() guard let appleIDToken = appleIDCredential.identityToken else { print("Unable to fetdch identify token.") return false } guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else { print("Unable to serialise token string from data: \(appleIDToken.debugDescription)") return false } let nonce = randomNonceString() let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce) if needsReauth { try await user.reauthenticate(with: credential) } if needsTokenRevocation { guard let authorizationCode = appleIDCredential.authorizationCode else { return false } guard let authCodeString = String(data: authorizationCode, encoding: .utf8) else { return false } try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString) } } try await user.delete() signOut() errorMessage = "" return true } catch { print(error) errorMessage = error.localizedDescription return false } } func deleteAccountWithRevocationHelper() async -> Bool { do { try await TokenRevocationHelper().revokeToken() try await userSession?.delete() return true } catch { errorMessage = error.localizedDescription return false } } } class SignInWithApple: NSObject, ASAuthorizationControllerDelegate { private var continuation: CheckedContinuation? func callAsFunction() async throws -> ASAuthorizationAppleIDCredential { return try await withCheckedThrowingContinuation { continuation in self.continuation = continuation let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.performRequests() } } func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { if case let appleIDCredential as ASAuthorizationAppleIDCredential = authorization.credential { continuation?.resume(returning: appleIDCredential) } } func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { continuation?.resume(throwing: error) } } class TokenRevocationHelper: NSObject, ASAuthorizationControllerDelegate { private var continuation : CheckedContinuation? func revokeToken() async throws { try await withCheckedThrowingContinuation { continuation in self.continuation = continuation let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.performRequests() } } func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { if case let appleIDCredential as ASAuthorizationAppleIDCredential = authorization.credential { guard let authorizationCode = appleIDCredential.authorizationCode else { return } guard let authCodeString = String(data: authorizationCode, encoding: .utf8) else { return } Task { try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString) continuation?.resume() } } } func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { continuation?.resume(throwing: error) } } extension AuthentificationViewModel { func handleSignInWithAppleRequest(_ request: ASAuthorizationAppleIDRequest) { request.requestedScopes = [.fullName, .email] let nonce = randomNonceString() // prevents replay attack by generate a nonce currentNonce = nonce request.nonce = sha256(nonce) } func handleSignInWithAppleCompletion(_ result: Result) { if case .failure(let failure) = result { errorMessage = failure.localizedDescription } else if case .success(let authorization) = result { if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { guard let nonce = currentNonce else { fatalError("Invalid state: a login callback was received, but no login request was sent.") } guard let appleIDToken = appleIDCredential.identityToken else { print("Unable to fetdch identify token.") return } guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else { print("Unable to serialise token string from data: \(appleIDToken.debugDescription)") return } let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: idTokenString, rawNonce: nonce) Task { do { let result = try await Auth.auth().signIn(with: credential) self.userSession = result.user guard let email = appleIDCredential.email else { return } guard let fullName = appleIDCredential.fullName else { return } //TODO: create a function for create User document do { let user = User(id: result.user.uid, fullName: "\(fullName.givenName ?? "") \(fullName.familyName ?? "")", email: email) let encodedUser = try Firestore.Encoder().encode(user) try await Firestore.firestore().collection("users").document(user.id).setData(encodedUser) } catch { print("Failed to create user with error \(error.localizedDescription)") } } catch { print("Error authenticating: \(error.localizedDescription)") } } } } } func verifySignInWithAppleAuthenticationState() { let appleIDProvider = ASAuthorizationAppleIDProvider() let providerData = Auth.auth().currentUser?.providerData if let appleProviderData = providerData?.first(where: { $0.providerID == "apple.com" }) { Task { do { let credentialState = try await appleIDProvider.credentialState(forUserID: appleProviderData.uid) switch credentialState { case .authorized: break // The Apple ID credential is valid. case .revoked, .notFound: // The Apple ID credential is either revoked or was not found, so show the sign-in UI. self.signOut() default: break } } catch { } } } } } extension ASAuthorizationAppleIDCredential { func displayName() -> String { return [self.fullName?.givenName, self.fullName?.familyName] .compactMap( {$0}) .joined(separator: " ") } }