Skip to content

Commit 8b320b7

Browse files
authored
[Auth] Revoke SiwA token when unlinking Apple provider (#13621)
1 parent 85a6cc2 commit 8b320b7

File tree

2 files changed

+79
-47
lines changed

2 files changed

+79
-47
lines changed

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Utility/Extensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public extension UIViewController {
7777
}
7878
}
7979

80-
func displayError(_ error: (any Error)?, from function: StaticString = #function) {
80+
@MainActor func displayError(_ error: (any Error)?, from function: StaticString = #function) {
8181
guard let error = error else { return }
8282
print("ⓧ Error in \(function): \(error.localizedDescription)")
8383
let message = "\(error.localizedDescription)\n\n Occurred in \(function)"

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
7777
// If the item's affiliated provider is currently linked with the user,
7878
// unlink the provider from the user's account.
7979
if item.isChecked {
80-
unlinkFromProvider(provider.id)
80+
Task { await unlinkFromProvider(provider.id) }
8181
return
8282
}
8383

@@ -86,7 +86,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
8686
performGoogleAccountLink()
8787

8888
case .apple:
89-
performAppleAccountLink()
89+
Task { await performAppleAccountLink() }
9090

9191
case .facebook:
9292
performFacebookAccountLink()
@@ -124,15 +124,53 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
124124
}
125125
}
126126

127+
/// Used for Sign in with Apple token revocation flow.
128+
private var continuation: CheckedContinuation<ASAuthorizationAppleIDCredential, Error>?
129+
127130
/// Wrapper method that uses Firebase's `unlink(fromProvider:)` API to unlink a user from an auth
128131
/// provider.
129132
/// This method will update the UI upon the unlinking's completion.
130133
/// - Parameter providerID: The string id of the auth provider.
131-
private func unlinkFromProvider(_ providerID: String) {
132-
user.unlink(fromProvider: providerID) { user, error in
133-
guard error == nil else { return self.displayError(error) }
134-
print("Unlinked user from auth provider: \(providerID)")
135-
self.updateUI()
134+
private func unlinkFromProvider(_ providerID: String) async {
135+
if providerID == AuthProviderID.apple.rawValue {
136+
// Needs SiwA token revocation.
137+
do {
138+
let needsTokenRevocation = user.providerData
139+
.contains { $0.providerID == AuthProviderID.apple.rawValue }
140+
if needsTokenRevocation {
141+
let appleIDCredential = try await signInWithApple()
142+
143+
guard let appleIDToken = appleIDCredential.identityToken else {
144+
print("Unable to fetch identify token.")
145+
return
146+
}
147+
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
148+
print("Unable to serialise token string from data: \(appleIDToken.debugDescription)")
149+
return
150+
}
151+
152+
let nonce = try CryptoUtils.randomNonceString()
153+
let credential = OAuthProvider.credential(providerID: .apple,
154+
idToken: idTokenString,
155+
rawNonce: nonce)
156+
157+
try await user.reauthenticate(with: credential)
158+
if
159+
let authorizationCode = appleIDCredential.authorizationCode,
160+
let authCodeString = String(data: authorizationCode, encoding: .utf8) {
161+
try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString)
162+
}
163+
}
164+
} catch {
165+
displayError(error)
166+
}
167+
}
168+
169+
do {
170+
_ = try await user.unlink(fromProvider: providerID)
171+
updateUI()
172+
} catch {
173+
displayError(error)
136174
}
137175
}
138176

@@ -179,27 +217,26 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
179217

180218
// MARK: - Sign in with Apple Account Linking 🔥
181219

182-
// For Sign in with Apple
183-
var currentNonce: String?
184-
185220
/// This method will initate the Sign In with Apple flow.
186221
/// See this class's conformance to `ASAuthorizationControllerDelegate` below for
187222
/// context on how the linking is made.
188-
private func performAppleAccountLink() {
223+
private func performAppleAccountLink() async {
189224
do {
190-
let nonce = try CryptoUtils.randomNonceString()
191-
currentNonce = nonce
192-
let appleIDProvider = ASAuthorizationAppleIDProvider()
193-
let request = appleIDProvider.createRequest()
194-
request.requestedScopes = [.fullName, .email]
195-
request.nonce = CryptoUtils.sha256(nonce)
225+
let appleIDCredential = try await signInWithApple()
196226

197-
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
198-
authorizationController.delegate = self
199-
authorizationController.presentationContextProvider = self
200-
authorizationController.performRequests()
227+
guard let appleIDToken = appleIDCredential.identityToken else {
228+
fatalError("Unable to fetch identify token.")
229+
}
230+
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
231+
fatalError("Unable to serialise token string from data: \(appleIDToken.debugDescription)")
232+
}
233+
234+
let nonce = try CryptoUtils.randomNonceString()
235+
let credential = OAuthProvider.credential(providerID: .apple,
236+
idToken: idTokenString,
237+
rawNonce: nonce)
238+
linkAccount(authCredential: credential)
201239
} catch {
202-
// In the unlikely case that nonce generation fails, show error view.
203240
displayError(error)
204241
}
205242
}
@@ -448,7 +485,7 @@ class AccountLinkingViewController: UIViewController, DataSourceProviderDelegate
448485
dataSourceProvider.delegate = self
449486
}
450487

451-
private func updateUI() {
488+
@MainActor private func updateUI() {
452489
configureDataSourceProvider()
453490
animateUpdates(for: tableView)
454491
}
@@ -488,39 +525,34 @@ extension AccountLinkingViewController: ASAuthorizationControllerDelegate,
488525
ASAuthorizationControllerPresentationContextProviding {
489526
// MARK: ASAuthorizationControllerDelegate
490527

491-
func authorizationController(controller: ASAuthorizationController,
492-
didCompleteWithAuthorization authorization: ASAuthorization) {
493-
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
494-
else {
495-
print("Unable to retrieve AppleIDCredential")
496-
return
497-
}
528+
func signInWithApple() async throws -> ASAuthorizationAppleIDCredential {
529+
return try await withCheckedThrowingContinuation { continuation in
530+
self.continuation = continuation
531+
let appleIDProvider = ASAuthorizationAppleIDProvider()
532+
let request = appleIDProvider.createRequest()
533+
request.requestedScopes = [.fullName, .email]
498534

499-
guard let nonce = currentNonce else {
500-
fatalError("Invalid state: A login callback was received, but no login request was sent.")
501-
}
502-
guard let appleIDToken = appleIDCredential.identityToken else {
503-
print("Unable to fetch identity token")
504-
return
505-
}
506-
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
507-
print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
508-
return
535+
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
536+
authorizationController.delegate = self
537+
authorizationController.performRequests()
509538
}
539+
}
510540

511-
let credential = OAuthProvider.credential(providerID: .apple,
512-
idToken: idTokenString,
513-
rawNonce: nonce)
514-
// Once we have created the above `credential`, we can link accounts to it.
515-
linkAccount(authCredential: credential)
541+
func authorizationController(controller: ASAuthorizationController,
542+
didCompleteWithAuthorization authorization: ASAuthorization) {
543+
if case let appleIDCredential as ASAuthorizationAppleIDCredential = authorization.credential {
544+
continuation?.resume(returning: appleIDCredential)
545+
} else {
546+
fatalError("Unexpected authorization credential type.")
547+
}
516548
}
517549

518550
func authorizationController(controller: ASAuthorizationController,
519551
didCompleteWithError error: any Error) {
520552
// Ensure that you have:
521553
// - enabled `Sign in with Apple` on the Firebase console
522554
// - added the `Sign in with Apple` capability for this project
523-
print("Sign in with Apple errored: \(error)")
555+
continuation?.resume(throwing: error)
524556
}
525557

526558
// MARK: ASAuthorizationControllerPresentationContextProviding

0 commit comments

Comments
 (0)