Skip to content

Passkey Support in AuthenticationExample App #15196

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

Closed
wants to merge 8 commits into from
Closed
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
2 changes: 2 additions & 0 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,8 @@ extension Auth: AuthInterop {
refreshToken: response.refreshToken,
anonymous: false
)
try await user.reload()
try await updateCurrentUser(user)
return AuthDataResult(withUser: user, additionalUserInfo: nil)
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation

/// The GCIP endpoint for finalizePasskeySignIn rpc
private let finalizePasskeySignInEndPoint = "accounts/passkeySignIn:finalize"
Expand Down Expand Up @@ -50,19 +51,21 @@ class FinalizePasskeySignInRequest: IdentityToolkitRequest, AuthRPCRequest {
}

var unencodedHTTPRequestBody: [String: AnyHashable]? {
let assertion: [String: AnyHashable] = [
"clientDataJSON": clientDataJSON,
"authenticatorData": authenticatorData,
"signature": signature,
"userHandle": userId,
]
let authResponse: [String: AnyHashable] = [
"id": credentialID,
"response": assertion,
]
var postBody: [String: AnyHashable] = [
"authenticatorAssertionResponse": [
"credentialId": credentialID,
"authenticatorAssertionResponse": [
"clientDataJSON": clientDataJSON,
"authenticatorData": authenticatorData,
"signature": signature,
"userHandle": userId,
],
] as [String: AnyHashable],
"authenticatorAuthenticationResponse": authResponse,
]
if let tenantID = tenantID {
postBody["tenantId"] = tenantID
if let tenant = tenantID {
postBody["tenantId"] = tenant
}
return postBody
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import Foundation

@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
struct FinalizePasskeySignInResponse: AuthRPCResponse {
/// The user raw access token.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ struct GetAccountInfoResponse: AuthRPCResponse {
} else {
mfaEnrollments = nil
}
if let passkeyEnrollmentData = dictionary["passkeys"] as? [[String: AnyHashable]] {
if let passkeyEnrollmentData = dictionary["passkeyInfo"] as? [[String: AnyHashable]] {
enrolledPasskeys = passkeyEnrollmentData.map { PasskeyInfo(dictionary: $0) }
} else {
enrolledPasskeys = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

/// The GCIP endpoint for startPasskeySignIn rpc
private let startPasskeySignInEndpoint = "accounts/passkeySignIn:start"

Expand All @@ -29,7 +31,7 @@ class StartPasskeySignInRequest: IdentityToolkitRequest, AuthRPCRequest {

var unencodedHTTPRequestBody: [String: AnyHashable]? {
guard let tenantID = tenantID else {
return nil
return [:]
}
return ["tenantId": tenantID]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
struct StartPasskeySignInResponse: AuthRPCResponse {
/// The RP ID of the FIDO Relying Party
Expand Down
31 changes: 22 additions & 9 deletions FirebaseAuth/Sources/Swift/User/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1079,12 +1079,7 @@ extension User: NSSecureCoding {}
requestConfiguration: requestConfiguration
)
let response = try await backend.call(with: request)
guard let passkeyName = (name?.isEmpty ?? true) ? defaultPasskeyName : name
else { throw NSError(
domain: AuthErrorDomain,
code: AuthErrorCode.internalError.rawValue,
userInfo: [NSLocalizedDescriptionKey: "Failed to unwrap passkey name"]
) }
passkeyName = (name?.isEmpty ?? true) ? defaultPasskeyName : name
guard let challengeInData = Data(base64Encoded: response.challenge) else {
throw NSError(
domain: AuthErrorDomain,
Expand All @@ -1104,7 +1099,7 @@ extension User: NSSecureCoding {}
)
return provider.createCredentialRegistrationRequest(
challenge: challengeInData,
name: passkeyName,
name: passkeyName ?? defaultPasskeyName,
userID: userIdInData
)
}
Expand All @@ -1114,13 +1109,26 @@ extension User: NSSecureCoding {}
@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
public func finalizePasskeyEnrollment(withPlatformCredential platformCredential: ASAuthorizationPlatformPublicKeyCredentialRegistration) async throws
-> AuthDataResult {
guard
!platformCredential.credentialID.isEmpty,
!platformCredential.rawClientDataJSON.isEmpty,
let attestation = platformCredential.rawAttestationObject,
!attestation.isEmpty
else {
throw NSError(
domain: AuthErrorDomain,
code: AuthErrorCode.internalError.rawValue,
userInfo: [NSLocalizedDescriptionKey:
"Invalid platform credential: missing credentialID, clientDataJSON, or attestationObject."]
)
}
let credentialID = platformCredential.credentialID.base64EncodedString()
let clientDataJSON = platformCredential.rawClientDataJSON.base64EncodedString()
let attestationObject = platformCredential.rawAttestationObject!.base64EncodedString()

let request = FinalizePasskeyEnrollmentRequest(
idToken: rawAccessToken(),
name: passkeyName ?? "Unnamed account (Apple)",
name: passkeyName ?? defaultPasskeyName,
credentialID: credentialID,
clientDataJSON: clientDataJSON,
attestationObject: attestationObject,
Expand All @@ -1133,6 +1141,9 @@ extension User: NSSecureCoding {}
refreshToken: response.refreshToken,
anonymous: false
)
defer { self.passkeyName = nil }
try await user.reload()
try await auth!.updateCurrentUser(user)
return AuthDataResult(withUser: user, additionalUserInfo: nil)
}

Expand All @@ -1147,12 +1158,14 @@ extension User: NSSecureCoding {}
request.deletePasskeys = [credentialID]
request.accessToken = rawAccessToken()
let response = try await backend.call(with: request)
_ = try await auth!.completeSignIn(
let user = try await auth!.completeSignIn(
withAccessToken: response.idToken,
accessTokenExpirationDate: response.approximateExpirationDate,
refreshToken: response.refreshToken,
anonymous: false
)
try await user.reload()
try await auth!.updateCurrentUser(user)
}
#endif

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
Loading
Loading