Skip to content

Commit

Permalink
Upgrade to latest SpeziAccount iteration
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Oct 23, 2023
1 parent ca7aa3c commit 57b82c0
Show file tree
Hide file tree
Showing 14 changed files with 160 additions and 38 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.8
// swift-tools-version:5.9

//
// This source file is part of the Stanford Spezi open-source project
Expand All @@ -15,7 +15,7 @@ let package = Package(
name: "SpeziFirebase",
defaultLocalization: "en",
platforms: [
.iOS(.v16)
.iOS(.v17),

Check failure on line 18 in Package.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Trailing Comma Violation: Collection literals should not have trailing commas (trailing_comma)
],
products: [
.library(name: "SpeziFirebaseAccount", targets: ["SpeziFirebaseAccount"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
// SPDX-License-Identifier: MIT
//

import AuthenticationServices
import FirebaseAuth
import OSLog
import SpeziAccount
import SwiftUI


protocol FirebaseAccountService: AnyActor, AccountService {
Expand All @@ -24,6 +26,8 @@ protocol FirebaseAccountService: AnyActor, AccountService {
/// - Parameter context: The global firebase context
func configure(with context: FirebaseContext) async

func inject(authorizationController: AuthorizationController) async

/// This method is called once the account for the given user was removed.
///
/// This allows for additional cleanup tasks to be performed.
Expand All @@ -38,6 +42,11 @@ protocol FirebaseAccountService: AnyActor, AccountService {
}


extension FirebaseAccountService {
func inject(authorizationController: AuthorizationController) async {}
}


// MARK: - Default Account Service Implementations
extension FirebaseAccountService {
func logout() async throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// SPDX-License-Identifier: MIT
//

import AuthenticationServices
import FirebaseAuth
import OSLog
import SpeziAccount
Expand All @@ -17,6 +18,7 @@ actor FirebaseEmailPasswordAccountService: UserIdPasswordAccountService, Firebas
static let logger = Logger(subsystem: "edu.stanford.spezi.firebase", category: "AccountService")

private static let supportedKeys = AccountKeyCollection {
\.accountId
\.userId
\.password
\.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
//

import AuthenticationServices
import CryptoKit
import FirebaseAuth
import OSLog
import SpeziAccount
Expand All @@ -33,6 +32,7 @@ actor FirebaseIdentityProviderAccountService: IdentityProvider, FirebaseAccountS
static let logger = Logger(subsystem: "edu.stanford.spezi.firebase", category: "IdentityProvider")

private static let supportedKeys = AccountKeyCollection {
\.accountId
\.userId
\.name
}
Expand All @@ -43,6 +43,8 @@ actor FirebaseIdentityProviderAccountService: IdentityProvider, FirebaseAccountS

let configuration: AccountServiceConfiguration

private var authorizationController: AuthorizationController?

@MainActor @AccountReference var account: Account // property wrappers cannot be non-isolated, so we isolate it to main actor
@MainActor private var lastNonce: String?

Expand All @@ -60,40 +62,23 @@ actor FirebaseIdentityProviderAccountService: IdentityProvider, FirebaseAccountS
}
}

// TODO move both somewhere else!
private static func randomNonceString(length: Int) -> String {
precondition(length > 0, "Nonce length must be non-zero")
let nonceCharacters = (0 ..< length).map { _ in
// ASCII alphabet goes from 32 (space) to 126 (~)
let num = Int.random(in: 32...126) // TODO something better? => crypto graphically!
guard let scalar = UnicodeScalar(num) else {
preconditionFailure("Failed to generate ASCII character for nonce!")
}
return Character(scalar)
}

return String(nonceCharacters)
}

private static func sha256(_ input: String) -> String {
SHA256.hash(data: Data(input.utf8))
.compactMap { byte in
String(format: "%02x", byte)
}
.joined()
}


func configure(with context: FirebaseContext) async {
self._context.inject(context)
await context.share(account: account)
}

func inject(authorizationController: AuthorizationController) {
self.authorizationController = authorizationController
}

func handleAccountRemoval(userId: String?) async {
// nothing we are doing here
}

func reauthenticateUser(userId: String, user: User) async {
// TODO how to check if token still valid?

Check failure on line 80 in Sources/SpeziFirebaseAccount/Account Services/FirebaseIdentityProviderAccountService.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (how to check if token still va...) (todo)

// TODO reauthenticate token https://firebase.google.com/docs/auth/ios/apple

Check failure on line 82 in Sources/SpeziFirebaseAccount/Account Services/FirebaseIdentityProviderAccountService.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (reauthenticate token https://f...) (todo)
}

Expand All @@ -103,7 +88,7 @@ actor FirebaseIdentityProviderAccountService: IdentityProvider, FirebaseAccountS
}

try await context.dispatchFirebaseAuthAction(on: self) {
let authResult = try await Auth.auth().signIn(with: credential)
let authResult = try await Auth.auth().signIn(with: credential) // TODO review error!

Check failure on line 91 in Sources/SpeziFirebaseAccount/Account Services/FirebaseIdentityProviderAccountService.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (review error!) (todo)
Self.logger.debug("signIn(with:) credential for user.")

return authResult
Expand All @@ -119,15 +104,65 @@ actor FirebaseIdentityProviderAccountService: IdentityProvider, FirebaseAccountS
}

try await context.dispatchFirebaseAuthAction(on: self) {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
await onAppleSignInRequest(request: request)

// TODO verify that controller is non-nil

Check failure on line 111 in Sources/SpeziFirebaseAccount/Account Services/FirebaseIdentityProviderAccountService.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (verify that controller is non-...) (todo)

guard let result = try await performRequest(request),
case let .appleID(credential) = result else {
return // TODO handle?
}

guard let _ = await lastNonce else {
fatalError("Invalid state: A login callback was received, but no login request was sent.")
}

guard let appleAuthCode = credential.authorizationCode else {
print("Unable to fetch authorization code")
return
}

guard let authCodeString = String(data: appleAuthCode, encoding: .utf8) else {
print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
return
}
// TODO token revocation!!! https://firebase.google.com/docs/auth/ios/apple

print("Revoking!")
do {
try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString) // TODO review error!
} catch {
// TODO token revocation fails on simulator https://github.com/firebase/firebase-tools/pull/6050
print(error)
throw error
}

print("Deleting")
try await currentUser.delete()
Self.logger.debug("delete() for user.")
}
}

private func performRequest(_ request: ASAuthorizationAppleIDRequest) async throws -> ASAuthorizationResult? {
guard let authorizationController else {
// TODO throw some error!
return nil
}

do {
return try await authorizationController.performRequest(request)
} catch {
try await onAppleSignInCompletion(result: .failure(error))
}

return nil
}

@MainActor
func onAppleSignInRequest(request: ASAuthorizationAppleIDRequest) {
let nonce = Self.randomNonceString(length: 32)
let nonce = CryptoUtils.randomNonceString(length: 32)
// we configured userId as `required` in the account service
var requestedScopes: [ASAuthorization.Scope] = [.email]

Expand All @@ -136,7 +171,7 @@ actor FirebaseIdentityProviderAccountService: IdentityProvider, FirebaseAccountS
requestedScopes.append(.fullName)
}

request.nonce = Self.sha256(nonce)
request.nonce = CryptoUtils.sha256(nonce)
request.requestedScopes = requestedScopes

self.lastNonce = nonce // save the nonce for later use to be passed to FirebaseAuth
Expand Down
35 changes: 35 additions & 0 deletions Sources/SpeziFirebaseAccount/Utils/CryptoUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import CryptoKit
import Foundation


enum CryptoUtils {
static func randomNonceString(length: Int) -> String {
precondition(length > 0, "Nonce length must be non-zero")
let nonceCharacters = (0 ..< length).map { _ in
// ASCII alphabet goes from 32 (space) to 126 (~)
let num = Int.random(in: 32...126) // .random(in:) is secure, see https://stackoverflow.com/a/76722233
guard let scalar = UnicodeScalar(num) else {
preconditionFailure("Failed to generate ASCII character for nonce!")
}
return Character(scalar)
}

return String(nonceCharacters)
}

static func sha256(_ input: String) -> String {
SHA256.hash(data: Data(input.utf8))
.compactMap { byte in
String(format: "%02x", byte)
}
.joined()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ actor FirebaseContext {
Self.logger.debug("Notifying SpeziAccount with updated user details.")

let builder = AccountDetails.Builder()
.set(\.accountId, value: user.uid)
.set(\.userId, value: email)
.set(\.isEmailVerified, value: user.isEmailVerified)

Expand Down
39 changes: 39 additions & 0 deletions Sources/SpeziFirebaseAccount/Views/FirebaseAccountModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import AuthenticationServices
import SpeziAccount
import SwiftUI


struct FirebaseAccountModifier: ViewModifier {
@EnvironmentObject private var account: Account

@Environment(\.authorizationController)
private var authorizationController

func body(content: Content) -> some View {
content
.task {
for service in account.registeredAccountServices {
guard let firebaseService = service as? any FirebaseAccountService else {
continue
}

await firebaseService.inject(authorizationController: authorizationController)
}
}
}
}


extension View {
public func firebaseAccount() -> some View {
modifier(FirebaseAccountModifier())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ struct FirebaseSignInWithAppleButton: View {
@State private var viewState: ViewState = .idle

var body: some View {
// TODO do we need to control the label?
SignInWithAppleButton(onRequest: { request in
accountService.onAppleSignInRequest(request: request)
}, onCompletion: { result in
Expand All @@ -44,7 +43,6 @@ struct FirebaseSignInWithAppleButton: View {
.frame(height: 55)
.signInWithAppleButtonStyle(colorScheme == .light ? .black : .white)
.viewStateAlert(state: $viewState)
// TODO should we prompt for existing credentials?
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct FirebaseAccountTestsView: View {
}
}
}
.firebaseAccount()
}


Expand Down
2 changes: 1 addition & 1 deletion Tests/UITests/TestAppUITests/FirebaseAccountTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class FirebaseAccountTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()

try disablePasswordAutofill()
// TODO try disablePasswordAutofill()

Check failure on line 22 in Tests/UITests/TestAppUITests/FirebaseAccountTests.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint / SwiftLint

Todo Violation: TODOs should be resolved (try disablePasswordAutofill()) (todo)

try await FirebaseClient.deleteAllAccounts()
try await Task.sleep(for: .seconds(0.5))
Expand Down
2 changes: 1 addition & 1 deletion Tests/UITests/TestAppUITests/FirebaseClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ struct FirestoreAccount: Decodable, Equatable {


enum FirebaseClient {
private static let projectId = "spezifirebaseuitests"
private static let projectId = "nams-e43ed"

// curl -H "Authorization: Bearer owner" -X DELETE http://localhost:9099/emulator/v1/projects/spezifirebaseuitests/accounts
static func deleteAllAccounts() async throws {
Expand Down
10 changes: 6 additions & 4 deletions Tests/UITests/UITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -435,7 +435,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -463,7 +463,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -499,7 +499,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -527,6 +527,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TestAppUITests/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.firebase.testappuitests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -548,6 +549,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TestAppUITests/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.firebase.testappuitests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
Loading

0 comments on commit 57b82c0

Please sign in to comment.