Skip to content

Commit 71db259

Browse files
committed
Merge branch 'remove-authentication-token' of https://github.com/firebase/FirebaseUI-iOS into ui-upgrades
2 parents d39e415 + cbed04a commit 71db259

File tree

65 files changed

+1890
-894
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1890
-894
lines changed

.github/workflows/swiftui-auth.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
unit-tests:
2727
name: Package Unit Tests
2828
runs-on: macos-15
29-
timeout-minutes: 20
29+
timeout-minutes: 15
3030
steps:
3131
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938
3232

@@ -129,7 +129,7 @@ jobs:
129129
ui-tests:
130130
name: UI Tests
131131
runs-on: macos-15
132-
timeout-minutes: 30
132+
timeout-minutes: 40
133133
steps:
134134
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938
135135

@@ -179,16 +179,16 @@ jobs:
179179
-enableCodeCoverage YES \
180180
-resultBundlePath FirebaseSwiftUIExampleUITests.xcresult | tee FirebaseSwiftUIExampleUITests.log | xcpretty --test --color --simple
181181
182-
- name: Upload test logs
182+
- name: Upload Firebase Emulator logs
183183
if: failure()
184184
uses: actions/upload-artifact@v4
185185
with:
186-
name: ui-tests-logs
187-
path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.log
186+
name: firebase-emulator-logs
187+
path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/firebase-debug.log
188188

189189
- name: Upload test results
190190
if: failure()
191191
uses: actions/upload-artifact@v4
192192
with:
193-
name: ui-tests-results
193+
name: FirebaseSwiftUIExampleUITests.xcresult
194194
path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// AccountService+Apple.swift
3+
// FirebaseUI
4+
//
5+
// Created by Russell Wheatley on 21/10/2025.
6+
//
7+
8+
@preconcurrency import FirebaseAuth
9+
import FirebaseAuthSwiftUI
10+
import Observation
11+
12+
protocol AppleOperationReauthentication {
13+
var appleProvider: AppleProviderSwift { get }
14+
}
15+
16+
extension AppleOperationReauthentication {
17+
@MainActor func reauthenticate() async throws {
18+
guard let user = Auth.auth().currentUser else {
19+
throw AuthServiceError.reauthenticationRequired("No user currently signed-in")
20+
}
21+
22+
do {
23+
let credential = try await appleProvider.createAuthCredential()
24+
try await user.reauthenticate(with: credential)
25+
} catch {
26+
throw AuthServiceError.signInFailed(underlying: error)
27+
}
28+
}
29+
}
30+
31+
@MainActor
32+
class AppleDeleteUserOperation: AuthenticatedOperation,
33+
@preconcurrency AppleOperationReauthentication {
34+
let appleProvider: AppleProviderSwift
35+
init(appleProvider: AppleProviderSwift) {
36+
self.appleProvider = appleProvider
37+
}
38+
39+
func callAsFunction(on user: User) async throws {
40+
try await callAsFunction(on: user) {
41+
try await user.delete()
42+
}
43+
}
44+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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 AuthenticationServices
16+
import CryptoKit
17+
import FirebaseAuth
18+
import FirebaseAuthSwiftUI
19+
import FirebaseCore
20+
import SwiftUI
21+
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(scopes: [ASAuthorization.Scope]) async throws -> (
43+
ASAuthorizationAppleIDCredential,
44+
String
45+
) {
46+
return try await AuthenticateWithAppleDialog(scopes: scopes).authenticate()
47+
}
48+
49+
private class AuthenticateWithAppleDialog: NSObject {
50+
private var continuation: CheckedContinuation<(ASAuthorizationAppleIDCredential, String), Error>?
51+
private var currentNonce: String?
52+
private let scopes: [ASAuthorization.Scope]
53+
54+
init(scopes: [ASAuthorization.Scope]) {
55+
self.scopes = scopes
56+
super.init()
57+
}
58+
59+
func authenticate() async throws -> (ASAuthorizationAppleIDCredential, String) {
60+
return try await withCheckedThrowingContinuation { continuation in
61+
self.continuation = continuation
62+
63+
let appleIDProvider = ASAuthorizationAppleIDProvider()
64+
let request = appleIDProvider.createRequest()
65+
request.requestedScopes = scopes
66+
67+
do {
68+
let nonce = try CryptoUtils.randomNonceString()
69+
currentNonce = nonce
70+
request.nonce = CryptoUtils.sha256(nonce)
71+
} catch {
72+
continuation.resume(throwing: AuthServiceError.signInFailed(underlying: error))
73+
return
74+
}
75+
76+
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
77+
authorizationController.delegate = self
78+
authorizationController.performRequests()
79+
}
80+
}
81+
}
82+
83+
extension AuthenticateWithAppleDialog: ASAuthorizationControllerDelegate {
84+
func authorizationController(controller _: ASAuthorizationController,
85+
didCompleteWithAuthorization authorization: ASAuthorization) {
86+
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
87+
if let nonce = currentNonce {
88+
continuation?.resume(returning: (appleIDCredential, nonce))
89+
} else {
90+
continuation?.resume(
91+
throwing: AuthServiceError.signInFailed(
92+
underlying: NSError(
93+
domain: "AppleSignIn",
94+
code: -1,
95+
userInfo: [NSLocalizedDescriptionKey: "Missing nonce"]
96+
)
97+
)
98+
)
99+
}
100+
} else {
101+
continuation?.resume(
102+
throwing: AuthServiceError.invalidCredentials("Missing Apple ID credential")
103+
)
104+
}
105+
continuation = nil
106+
}
107+
108+
func authorizationController(controller _: ASAuthorizationController,
109+
didCompleteWithError error: Error) {
110+
continuation?.resume(throwing: AuthServiceError.signInFailed(underlying: error))
111+
continuation = nil
112+
}
113+
}
114+
115+
// MARK: - Apple Provider Swift
116+
117+
public class AppleProviderSwift: AuthProviderSwift, DeleteUserSwift {
118+
public let scopes: [ASAuthorization.Scope]
119+
let providerId = "apple.com"
120+
121+
public init(scopes: [ASAuthorization.Scope] = [.fullName, .email]) {
122+
self.scopes = scopes
123+
}
124+
125+
@MainActor public func createAuthCredential() async throws -> AuthCredential {
126+
let (appleIDCredential, nonce) = try await authenticateWithApple(scopes: scopes)
127+
128+
guard let idTokenString = appleIDCredential.idTokenString else {
129+
throw AuthServiceError.invalidCredentials("Unable to fetch identity token from Apple")
130+
}
131+
132+
let credential = OAuthProvider.appleCredential(
133+
withIDToken: idTokenString,
134+
rawNonce: nonce,
135+
fullName: appleIDCredential.fullName
136+
)
137+
138+
return credential
139+
}
140+
141+
public func deleteUser(user: User) async throws {
142+
let operation = AppleDeleteUserOperation(appleProvider: self)
143+
try await operation(on: user)
144+
}
145+
}
146+
147+
public class AppleProviderAuthUI: AuthProviderUI {
148+
public var provider: AuthProviderSwift
149+
150+
public init(provider: AuthProviderSwift) {
151+
self.provider = provider
152+
}
153+
154+
public let id: String = "apple.com"
155+
156+
@MainActor public func authButton() -> AnyView {
157+
AnyView(SignInWithAppleButton(provider: provider))
158+
}
159+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
//
16+
// AuthService+Apple.swift
17+
// FirebaseUI
18+
//
19+
// Created by Russell Wheatley on 21/10/2025.
20+
//
21+
22+
import FirebaseAuthSwiftUI
23+
24+
public extension AuthService {
25+
@discardableResult
26+
func withAppleSignIn(_ provider: AppleProviderSwift? = nil) -> AuthService {
27+
registerProvider(providerWithButton: AppleProviderAuthUI(provider: provider ??
28+
AppleProviderSwift()))
29+
return self
30+
}
31+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 CryptoKit
16+
import Foundation
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+
}
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 FirebaseAuthSwiftUI
16+
import SwiftUI
17+
18+
/// A button for signing in with Apple
19+
@MainActor
20+
public struct SignInWithAppleButton {
21+
@Environment(AuthService.self) private var authService
22+
let provider: AuthProviderSwift
23+
public init(provider: AuthProviderSwift) {
24+
self.provider = provider
25+
}
26+
}
27+
28+
extension SignInWithAppleButton: View {
29+
public var body: some View {
30+
Button(action: {
31+
Task {
32+
try? await authService.signIn(provider)
33+
}
34+
}) {
35+
HStack {
36+
Image(systemName: "apple.logo")
37+
.resizable()
38+
.renderingMode(.template)
39+
.scaledToFit()
40+
.frame(width: 24, height: 24)
41+
.foregroundColor(.white)
42+
Text("Sign in with Apple")
43+
.fontWeight(.semibold)
44+
.foregroundColor(.white)
45+
}
46+
.frame(maxWidth: .infinity, alignment: .leading)
47+
.padding()
48+
.background(Color.black)
49+
.cornerRadius(8)
50+
}
51+
.accessibilityIdentifier("sign-in-with-apple-button")
52+
}
53+
}

0 commit comments

Comments
 (0)