Skip to content

Instantly share code, notes, and snippets.

@henrik-dmg
Last active May 4, 2024 19:01
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save henrik-dmg/8094d2c0566c6f210cc5ed56c9b1c8e7 to your computer and use it in GitHub Desktop.
Save henrik-dmg/8094d2c0566c6f210cc5ed56c9b1c8e7 to your computer and use it in GitHub Desktop.
A SwiftUI wrapper around the new Sign in with Apple button that returns OAuthCredentials directly to login with Firebase
import AuthenticationServices
import CryptoKit
import FirebaseAuth
import Foundation
import SwiftUI
// Adapted from https://firebase.google.com/docs/auth/ios/apple?authuser=0
@available(iOS 14.0, OSX 10.16, tvOS 14.0, *)
@available(watchOS, unavailable)
struct SignInWithAppleFirebaseButton: View {
public typealias Body = SignInWithAppleButton
private let label: SignInWithAppleButton.Label
private let currentNonce: String
private let completion: (Result<OAuthCredential, Error>) -> Void
public init(_ label: SignInWithAppleButton.Label = .signIn, completion: @escaping (Result<OAuthCredential, Error>) -> Void) {
self.label = label
self.currentNonce = SignInWithAppleFirebaseButton.randomNonceString()
self.completion = completion
}
public var body: SignInWithAppleButton {
SignInWithAppleButton(label) { request in
request.requestedScopes = [.fullName, .email]
request.nonce = makeHashedNonce(currentNonce)
} onCompletion: { result in
switch result {
case .success(let authResult):
guard let appleCredential = authResult.credential as? ASAuthorizationAppleIDCredential else {
completion(.failure(AuthenticationError.incompatibleCredentials))
return
}
guard let appleIDToken = appleCredential.identityToken else {
completion(.failure(AuthenticationError.missingIdentityToken))
return
}
guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
completion(.failure(AuthenticationError.invalidData(appleIDToken)))
return
}
// Initialize a Firebase credential.
let credential = OAuthProvider.credential(
withProviderID: "apple.com",
idToken: idTokenString,
rawNonce: currentNonce)
// Sign in with Firebase.
completion(.success(credential))
case .failure(let error):
completion(.failure(error))
}
}
}
private func makeHashedNonce(_ input: String) -> String {
let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap {
return String(format: "%02x", $0)
}.joined()
return hashString
}
// Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
private static func randomNonceString(length: Int = 32) -> String {
precondition(length > 0)
let charset: Array<Character> =
Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
var result = ""
var remainingLength = length
while remainingLength > 0 {
let randoms: [UInt8] = (0 ..< 16).map { _ in
var random: UInt8 = 0
let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random)
if errorCode != errSecSuccess {
fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)")
}
return random
}
randoms.forEach { random in
if remainingLength == 0 {
return
}
if random < charset.count {
result.append(charset[Int(random)])
remainingLength -= 1
}
}
}
return result
}
}
final class AuthenticationError: NSError {
static let incompatibleCredentials = AuthenticationError(
domain: "com.henrikpanhans.SignInWithAppleFirebaseButton",
code: 1,
description: "Incompatible credentials returned by Apple")
static let missingIdentityToken = AuthenticationError(
domain: "com.henrikpanhans.SignInWithAppleFirebaseButton",
code: 2,
description: "Unable to fetch identity token")
static func invalidData(_ data: Data) -> AuthenticationError {
AuthenticationError(
domain: "com.henrikpanhans.SignInWithAppleFirebaseButton",
code: 3,
description: "Unable to serialize token string from data: \(data.debugDescription)")
}
}
extension NSError {
convenience init(domain: String, code: Int, description: String) {
let dictionary = [NSLocalizedDescriptionKey: description]
self.init(domain: domain, code: code, userInfo: dictionary)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment