Skip to content

Passkey Support in AuthenticationExample Sample App #15211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: passkey-new
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1675,17 +1675,17 @@ extension Auth: AuthInterop {
@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
public func finalizePasskeySignIn(withPlatformCredential platformCredential: ASAuthorizationPlatformPublicKeyCredentialAssertion) async throws
-> AuthDataResult {
let credentialID = platformCredential.credentialID.base64EncodedString()
let clientDataJSON = platformCredential.rawClientDataJSON.base64EncodedString()
let credentialId = platformCredential.credentialID.base64EncodedString()
let clientDataJson = platformCredential.rawClientDataJSON.base64EncodedString()
let authenticatorData = platformCredential.rawAuthenticatorData.base64EncodedString()
let signature = platformCredential.signature.base64EncodedString()
let userID = platformCredential.userID.base64EncodedString()
let userId = platformCredential.userID.base64EncodedString()
let request = FinalizePasskeySignInRequest(
credentialID: credentialID,
clientDataJSON: clientDataJSON,
credentialID: credentialId,
clientDataJSON: clientDataJson,
authenticatorData: authenticatorData,
signature: signature,
userId: userID,
userId: userId,
requestConfiguration: requestConfiguration
)
let response = try await backend.call(with: request)
Expand Down
8 changes: 4 additions & 4 deletions FirebaseAuth/Sources/Swift/User/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1122,15 +1122,15 @@ extension User: NSSecureCoding {}
"Invalid platform credential: missing credentialID, clientDataJSON, or attestationObject."]
)
}
let credentialID = platformCredential.credentialID.base64EncodedString()
let clientDataJSON = platformCredential.rawClientDataJSON.base64EncodedString()
let credentialId = platformCredential.credentialID.base64EncodedString()
let clientDataJson = platformCredential.rawClientDataJSON.base64EncodedString()
let attestationObject = platformCredential.rawAttestationObject!.base64EncodedString()

let request = FinalizePasskeyEnrollmentRequest(
idToken: rawAccessToken(),
name: passkeyName ?? defaultPasskeyName,
credentialID: credentialID,
clientDataJSON: clientDataJSON,
credentialID: credentialId,
clientDataJSON: clientDataJson,
attestationObject: attestationObject,
requestConfiguration: auth!.requestConfiguration
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ enum AuthMenu: String {
case phoneEnroll
case totpEnroll
case multifactorUnenroll
case passkeySignUp
case passkeyEnroll
case passkeySignIn
case passkeyUnenroll

// More intuitively named getter for `rawValue`.
var id: String { rawValue }
Expand Down Expand Up @@ -139,6 +143,15 @@ enum AuthMenu: String {
return "TOTP Enroll"
case .multifactorUnenroll:
return "Multifactor unenroll"
// Passkey
case .passkeySignUp:
return "Sign Up with Passkey"
case .passkeyEnroll:
return "Enroll with Passkey"
case .passkeySignIn:
return "Sign In with Passkey"
case .passkeyUnenroll:
return "Unenroll Passkey"
}
}

Expand Down Expand Up @@ -220,6 +233,14 @@ enum AuthMenu: String {
self = .totpEnroll
case "Multifactor unenroll":
self = .multifactorUnenroll
case "Sign Up with Passkey":
self = .passkeySignUp
case "Enroll with Passkey":
self = .passkeyEnroll
case "Sign In with Passkey":
self = .passkeySignIn
case "Unenroll Passkey":
self = .passkeyUnenroll
default:
return nil
}
Expand Down Expand Up @@ -354,9 +375,20 @@ class AuthMenuData: DataSourceProvidable {
return Section(headerDescription: header, items: items)
}

static var passkeySection: Section {
let header = "Passkey"
let items: [Item] = [
Item(title: AuthMenu.passkeySignUp.name),
Item(title: AuthMenu.passkeyEnroll.name),
Item(title: AuthMenu.passkeySignIn.name),
Item(title: AuthMenu.passkeyUnenroll.name),
]
return Section(headerDescription: header, items: items)
}

static let sections: [Section] =
[settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection,
customAuthDomainSection, appSection, oobSection, multifactorSection]
customAuthDomainSection, appSection, oobSection, multifactorSection, passkeySection]

static var authLinkSections: [Section] {
let allItems = [providerSection, emailPasswordSection, otherSection].flatMap { $0.items }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ extension User: DataSourceProvidable {
return Section(headerDescription: "Info", items: items)
}

private var passkeysSection: Section {
let passkeys = enrolledPasskeys ?? []
guard !passkeys.isEmpty else {
return Section(
headerDescription: "Passkeys",
items: [Item(title: "None", detailTitle: "No passkeys enrolled")]
)
}
let items: [Item] = passkeys.map { info in
Item(title: info.name, detailTitle: info.credentialID)
}
return Section(headerDescription: "Passkeys", items: items)
}

private var metaDataSection: Section {
let metadataRows = [
Item(title: metadata.lastSignInDate?.description, detailTitle: "Last Sign-in Date"),
Expand Down Expand Up @@ -62,7 +76,7 @@ extension User: DataSourceProvidable {
}

var sections: [Section] {
[infoSection, metaDataSection, otherSection, actionSection]
[infoSection, passkeysSection, metaDataSection, otherSection, actionSection]
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,18 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {

case .multifactorUnenroll:
mfaUnenroll()

case .passkeySignUp:
passkeySignUp()

case .passkeyEnroll:
Task { await passkeyEnroll() }

case .passkeySignIn:
Task { await passkeySignIn() }

case .passkeyUnenroll:
Task { await passkeyUnenroll() }
}
}

Expand Down Expand Up @@ -922,6 +934,87 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
}
}

// MARK: - Passkey

private func passkeySignUp() {
guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
print("OS version is not supported for this action.")
return
}
Task {
do {
_ = try await AppManager.shared.auth().signInAnonymously()
print("sign-in anonymously succeeded.")
if let uid = AppManager.shared.auth().currentUser?.uid {
print("User ID: \(uid)")
}
// Continue to enroll a passkey.
await passkeyEnroll()
} catch {
print("sign-in anonymously failed: \(error.localizedDescription)")
self.showAlert(for: "Anonymous Sign-In Failed")
}
}
}

private func passkeyEnroll() async {
guard let user = AppManager.shared.auth().currentUser else {
showAlert(for: "Please sign in first.")
return
}
let passkeyName = await showTextInputPrompt(with: "Passkey name")
guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
showAlert(for: "Not Supported", message: "This OS version does not support passkeys.")
return
}

do {
let request = try await user.startPasskeyEnrollment(withName: passkeyName)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
print("Started passkey enrollment (challenge created).")
} catch {
showAlert(for: "Passkey enrollment failed", message: error.localizedDescription)
print("startPasskeyEnrollment failed: \(error.localizedDescription)")
}
}

private func passkeySignIn() async {
guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
print("OS version is not supported for this action.")
return
}
do {
let request = try await AppManager.shared.auth().startPasskeySignIn()
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
print("Started passkey sign in (challenge created).")
} catch {
print("Passkey sign-in failed with error: \(error)")
}
}

private func passkeyUnenroll() async {
guard let user = AppManager.shared.auth().currentUser else {
showAlert(for: "Please sign in first.")
return
}
guard let credentialId = await showTextInputPrompt(with: "Credential Id") else {
print("Passkey unenrollment cancelled: no credential id entered.")
return
}
do {
let _ = try await user.unenrollPasskey(withCredentialID: credentialId)
} catch {
showAlert(for: "Passkey unenrollment failed", message: error.localizedDescription)
print("unenrollPasskey failed: \(error.localizedDescription)")
}
}

// MARK: - Private Helpers

private func showTextInputPrompt(with message: String, completion: ((String) -> Void)? = nil) {
Expand Down Expand Up @@ -1027,6 +1120,43 @@ extension AuthViewController: ASAuthorizationControllerDelegate,

func authorizationController(controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization) {
if #available(iOS 16.0, macOS 12.0, tvOS 16.0, *),
let regCred = authorization.credential
as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
Task { @MainActor [weak self] in
guard let self else { return }
do {
guard let user = AppManager.shared.auth().currentUser else {
self.showAlert(for: "Finalize failed", message: "No signed-in user.")
return
}
_ = try await user.finalizePasskeyEnrollment(withPlatformCredential: regCred)
self.showAlert(for: "Passkey Enrollment", message: "Succeeded")
print("Passkey Enrollment succeeded.")
} catch {
self.showAlert(for: "Passkey Enrollment failed", message: error.localizedDescription)
print("Finalize enrollment failed: \(error.localizedDescription)")
}
}
return
}
if let assertion = authorization
.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
Task { @MainActor [weak self] in
guard let self else { return }
do {
let _ = try await AppManager.shared.auth()
.finalizePasskeySignIn(withPlatformCredential: assertion)
self.showAlert(for: "Passkey Sign-In", message: "Succeeded")
print("Passkey sign-in succeeded.")
self.transitionToUserViewController()
} catch {
self.showAlert(for: "Passkey Sign-In failed", message: error.localizedDescription)
print("Finalize passkey sign-in failed: \(error.localizedDescription)")
}
}
return
}
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
else {
print("Unable to retrieve AppleIDCredential")
Expand Down Expand Up @@ -1074,10 +1204,10 @@ extension AuthViewController: ASAuthorizationControllerDelegate,

func authorizationController(controller: ASAuthorizationController,
didCompleteWithError error: any Error) {
// Ensure that you have:
print("Apple authorization failed: \(error)")
// for Sign In with Apple, ensure that you have:
// - enabled `Sign in with Apple` on the Firebase console
// - added the `Sign in with Apple` capability for this project
print("Sign in with Apple failed: \(error)")
}

// MARK: ASAuthorizationControllerPresentationContextProviding
Expand Down
Loading