Skip to content

Commit 7f6be55

Browse files
feat: support apple sign in
1 parent 5ccebec commit 7f6be55

File tree

5 files changed

+157
-22
lines changed

5 files changed

+157
-22
lines changed

FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AccountService+Apple.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ extension AppleOperationReauthentication {
2020
}
2121

2222
do {
23-
// TODO: Implement Apple reauthentication
2423
let credential = try await appleProvider.createAuthCredential()
2524
try await user.reauthenticate(with: credential)
2625

@@ -40,7 +39,6 @@ class AppleDeleteUserOperation: AuthenticatedOperation,
4039
}
4140

4241
func callAsFunction(on user: User) async throws {
43-
// TODO: Implement delete user operation
4442
try await callAsFunction(on: user) {
4543
try await user.delete()
4644
}

FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift

Lines changed: 104 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,103 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import AuthenticationServices
16+
import CryptoKit
1517
import FirebaseAuth
1618
import FirebaseAuthSwiftUI
1719
import FirebaseCore
1820
import SwiftUI
1921

22+
// MARK: - Data Extensions
23+
24+
extension Data {
25+
var utf8String: String? {
26+
return String(data: self, encoding: .utf8)
27+
}
28+
}
29+
30+
extension ASAuthorizationAppleIDCredential {
31+
var authorizationCodeString: String? {
32+
return authorizationCode?.utf8String
33+
}
34+
35+
var idTokenString: String? {
36+
return identityToken?.utf8String
37+
}
38+
}
39+
40+
// MARK: - Authenticate With Apple Dialog
41+
42+
private func authenticateWithApple() async throws -> (ASAuthorizationAppleIDCredential, String) {
43+
return try await AuthenticateWithAppleDialog().authenticate()
44+
}
45+
46+
private class AuthenticateWithAppleDialog: NSObject {
47+
private var continuation: CheckedContinuation<(ASAuthorizationAppleIDCredential, String), Error>?
48+
private var currentNonce: String?
49+
50+
func authenticate() async throws -> (ASAuthorizationAppleIDCredential, String) {
51+
return try await withCheckedThrowingContinuation { continuation in
52+
self.continuation = continuation
53+
54+
let appleIDProvider = ASAuthorizationAppleIDProvider()
55+
let request = appleIDProvider.createRequest()
56+
request.requestedScopes = [.fullName, .email]
57+
58+
do {
59+
let nonce = try CryptoUtils.randomNonceString()
60+
currentNonce = nonce
61+
request.nonce = CryptoUtils.sha256(nonce)
62+
} catch {
63+
continuation.resume(throwing: AuthServiceError.signInFailed(underlying: error))
64+
return
65+
}
66+
67+
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
68+
authorizationController.delegate = self
69+
authorizationController.performRequests()
70+
}
71+
}
72+
}
73+
74+
extension AuthenticateWithAppleDialog: ASAuthorizationControllerDelegate {
75+
func authorizationController(
76+
controller: ASAuthorizationController,
77+
didCompleteWithAuthorization authorization: ASAuthorization
78+
) {
79+
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
80+
if let nonce = currentNonce {
81+
continuation?.resume(returning: (appleIDCredential, nonce))
82+
} else {
83+
continuation?.resume(
84+
throwing: AuthServiceError.signInFailed(
85+
underlying: NSError(
86+
domain: "AppleSignIn",
87+
code: -1,
88+
userInfo: [NSLocalizedDescriptionKey: "Missing nonce"]
89+
)
90+
)
91+
)
92+
}
93+
} else {
94+
continuation?.resume(
95+
throwing: AuthServiceError.invalidCredentials("Missing Apple ID credential")
96+
)
97+
}
98+
continuation = nil
99+
}
100+
101+
func authorizationController(
102+
controller: ASAuthorizationController,
103+
didCompleteWithError error: Error
104+
) {
105+
continuation?.resume(throwing: AuthServiceError.signInFailed(underlying: error))
106+
continuation = nil
107+
}
108+
}
109+
110+
// MARK: - Apple Provider Swift
111+
20112
public class AppleProviderSwift: AuthProviderSwift, DeleteUserSwift {
21113
public let scopes: [String]
22114
let providerId = "apple.com"
@@ -26,27 +118,22 @@ public class AppleProviderSwift: AuthProviderSwift, DeleteUserSwift {
26118
}
27119

28120
@MainActor public func createAuthCredential() async throws -> AuthCredential {
29-
// TODO: Implement Apple Sign In credential creation
30-
// This will need to use ASAuthorizationAppleIDProvider
31-
let provider = OAuthProvider(providerID: providerId)
32-
return try await withCheckedThrowingContinuation { continuation in
33-
provider.getCredentialWith(nil) { credential, error in
34-
if let error {
35-
continuation
36-
.resume(throwing: AuthServiceError.signInFailed(underlying: error))
37-
} else if let credential {
38-
continuation.resume(returning: credential)
39-
} else {
40-
continuation
41-
.resume(throwing: AuthServiceError
42-
.invalidCredentials("Apple did not provide a valid AuthCredential"))
43-
}
44-
}
121+
let (appleIDCredential, nonce) = try await authenticateWithApple()
122+
123+
guard let idTokenString = appleIDCredential.idTokenString else {
124+
throw AuthServiceError.invalidCredentials("Unable to fetch identity token from Apple")
45125
}
126+
127+
let credential = OAuthProvider.appleCredential(
128+
withIDToken: idTokenString,
129+
rawNonce: nonce,
130+
fullName: appleIDCredential.fullName
131+
)
132+
133+
return credential
46134
}
47135

48136
public func deleteUser(user: User) async throws {
49-
// TODO: Implement delete user functionality
50137
let operation = AppleDeleteUserOperation(appleProvider: self)
51138
try await operation(on: user)
52139
}

FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AuthService+Apple.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import FirebaseAuthSwiftUI
2424
public extension AuthService {
2525
@discardableResult
2626
func withAppleSignIn(_ provider: AppleProviderSwift? = nil) -> AuthService {
27-
// TODO: Register Apple provider with authentication service
2827
registerProvider(providerWithButton: AppleProviderAuthUI(provider: provider ??
2928
AppleProviderSwift()))
3029
return self
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
import CryptoKit
17+
18+
/// Set of utility APIs for generating cryptographical artifacts.
19+
enum CryptoUtils {
20+
enum NonceGenerationError: Error {
21+
case generationFailure(status: OSStatus)
22+
}
23+
24+
static func randomNonceString(length: Int = 32) throws -> String {
25+
precondition(length > 0)
26+
var randomBytes = [UInt8](repeating: 0, count: length)
27+
let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
28+
if errorCode != errSecSuccess {
29+
throw NonceGenerationError.generationFailure(status: errorCode)
30+
}
31+
32+
let charset: [Character] =
33+
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
34+
35+
let nonce = randomBytes.map { byte in
36+
// Pick a random character from the set, wrapping around if needed.
37+
charset[Int(byte) % charset.count]
38+
}
39+
40+
return String(nonce)
41+
}
42+
43+
static func sha256(_ input: String) -> String {
44+
let inputData = Data(input.utf8)
45+
let hashedData = SHA256.hash(data: inputData)
46+
let hashString = hashedData.compactMap {
47+
String(format: "%02x", $0)
48+
}.joined()
49+
50+
return hashString
51+
}
52+
}
53+

FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,11 @@ public struct SignInWithAppleButton {
2828
extension SignInWithAppleButton: View {
2929
public var body: some View {
3030
Button(action: {
31-
// TODO: Implement sign in with Apple action
3231
Task {
3332
try await authService.signIn(provider)
3433
}
3534
}) {
3635
HStack {
37-
// TODO: Add Apple logo image
3836
Image(systemName: "apple.logo")
3937
.resizable()
4038
.renderingMode(.template)

0 commit comments

Comments
 (0)