Skip to content

Testing gcip passkey swift #15218

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

Draft
wants to merge 13 commits into
base: passkey-swift-migration
Choose a base branch
from
64 changes: 64 additions & 0 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import FirebaseCoreExtension
import UIKit
#endif

#if os(iOS) || os(tvOS) || os(macOS) || targetEnvironment(macCatalyst)
import AuthenticationServices
#endif

// Export the deprecated Objective-C defined globals and typedefs.
#if SWIFT_PACKAGE
@_exported import FirebaseAuthInternal
Expand Down Expand Up @@ -1641,6 +1645,66 @@ extension Auth: AuthInterop {
public static let authStateDidChangeNotification =
NSNotification.Name(rawValue: "FIRAuthStateDidChangeNotification")

// MARK: Passkey Implementation

#if os(iOS) || os(tvOS) || os(macOS) || targetEnvironment(macCatalyst)

/// starts sign in with passkey retrieving challenge from GCIP and create an assertion request.
@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
public func startPasskeySignIn() async throws ->
ASAuthorizationPlatformPublicKeyCredentialAssertionRequest {
let request = StartPasskeySignInRequest(requestConfiguration: requestConfiguration)
print("invoking StartPasskeySignIn\n")
let response = try await backend.call(with: request)
guard let challengeInData = Data(base64Encoded: response.challenge) else {
throw NSError(
domain: AuthErrorDomain,
code: AuthInternalErrorCode.RPCResponseDecodingError.rawValue,
userInfo: [NSLocalizedDescriptionKey: "Failed to decode base64 challenge from response."]
)
}
print("StartPasskeySignIn Succeed!\n")
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: response.rpID
)
return provider.createCredentialAssertionRequest(
challenge: challengeInData
)
}

/// finalize sign in with passkey with existing credential assertion.
/// - Parameter platformCredential The existing credential assertion created by device.
@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 authenticatorData = platformCredential.rawAuthenticatorData.base64EncodedString()
let signature = platformCredential.signature.base64EncodedString()
let userId = platformCredential.userID.base64EncodedString()
let request = FinalizePasskeySignInRequest(
credentialID: credentialId,
clientDataJSON: clientDataJson,
authenticatorData: authenticatorData,
signature: signature,
userId: userId,
requestConfiguration: requestConfiguration
)
print("invoking finalizePasskeySignIn for Passkey credentialId: \(credentialId)\n\n")
let response = try await backend.call(with: request)
let user = try await Auth.auth().completeSignIn(
withAccessToken: response.idToken,
accessTokenExpirationDate: nil,
refreshToken: response.refreshToken,
anonymous: false
)
try await user.reload()
try await updateCurrentUser(user)
print("FinalizePasskeySignIn Succeed!\nidToken: \(response.idToken)\n")
return AuthDataResult(withUser: user, additionalUserInfo: nil)
}
#endif

// MARK: Internal methods

init(app: FirebaseApp,
Expand Down
2 changes: 2 additions & 0 deletions FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ final class AuthBackend: AuthBackendProtocol {
.missingIosBundleIDError(message: serverDetailErrorMessage)
case "MISSING_ANDROID_PACKAGE_NAME": return AuthErrorUtils
.missingAndroidPackageNameError(message: serverDetailErrorMessage)
case "PASSKEY_ENROLLMENT_NOT_FOUND": return AuthErrorUtils.missingPasskeyEnrollment()
case "UNAUTHORIZED_DOMAIN": return AuthErrorUtils
.unauthorizedDomainError(message: serverDetailErrorMessage)
case "INVALID_CONTINUE_URI": return AuthErrorUtils
Expand Down Expand Up @@ -440,6 +441,7 @@ final class AuthBackend: AuthBackendProtocol {
return AuthErrorUtils.credentialAlreadyInUseError(
message: serverDetailErrorMessage, credential: credential, email: email
)
case "INVALID_AUTHENTICATOR_RESPONSE": return AuthErrorUtils.invalidAuthenticatorResponse()
default:
if let underlyingErrors = errorDictionary["errors"] as? [[String: String]] {
for underlyingError in underlyingErrors {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

/// The GCIP endpoint for finalizePasskeyEnrollment rpc
private let finalizePasskeyEnrollmentEndPoint = "accounts/passkeyEnrollment:finalize"

@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
class FinalizePasskeyEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest {
typealias Response = FinalizePasskeyEnrollmentResponse

/// The raw user access token.
let idToken: String
/// The passkey name.
let name: String
/// The credential ID.
let credentialID: String
/// The CollectedClientData object from the authenticator.
let clientDataJSON: String
/// The attestation object from the authenticator.
let attestationObject: String

init(idToken: String,
name: String,
credentialID: String,
clientDataJSON: String,
attestationObject: String,
requestConfiguration: AuthRequestConfiguration) {
self.idToken = idToken
self.name = name
self.credentialID = credentialID
self.clientDataJSON = clientDataJSON
self.attestationObject = attestationObject
super.init(
endpoint: finalizePasskeyEnrollmentEndPoint,
requestConfiguration: requestConfiguration,
useIdentityPlatform: true
)
}

var unencodedHTTPRequestBody: [String: AnyHashable]? {
var postBody: [String: AnyHashable] = [
"idToken": idToken,
"name": name,
]
let authAttestationResponse: [String: AnyHashable] = [
"clientDataJSON": clientDataJSON,
"attestationObject": attestationObject,
]
let authRegistrationResponse: [String: AnyHashable] = [
"id": credentialID,
"response": authAttestationResponse,
]
postBody["authenticatorRegistrationResponse"] = authRegistrationResponse
if let tenantId = tenantID {
postBody["tenantId"] = tenantId
}
return postBody
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// 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 FinalizePasskeyEnrollmentResponse: AuthRPCResponse {
/// The user raw access token.
let idToken: String
/// Refresh token for the authenticated user.
let refreshToken: String

init(dictionary: [String: AnyHashable]) throws {
guard
let idToken = dictionary["idToken"] as? String,
let refreshToken = dictionary["refreshToken"] as? String
else {
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
}
self.idToken = idToken
self.refreshToken = refreshToken
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* 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"

@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
class FinalizePasskeySignInRequest: IdentityToolkitRequest, AuthRPCRequest {
typealias Response = FinalizePasskeySignInResponse
/// The credential ID
let credentialID: String
/// The CollectedClientData object from the authenticator.
let clientDataJSON: String
/// The AuthenticatorData from the authenticator.
let authenticatorData: String
/// The signature from the authenticator.
let signature: String
/// The user handle
let userId: String

init(credentialID: String,
clientDataJSON: String,
authenticatorData: String,
signature: String,
userId: String,
requestConfiguration: AuthRequestConfiguration) {
self.credentialID = credentialID
self.clientDataJSON = clientDataJSON
self.authenticatorData = authenticatorData
self.signature = signature
self.userId = userId
super.init(
endpoint: finalizePasskeySignInEndPoint,
requestConfiguration: requestConfiguration,
useIdentityPlatform: true
)
}

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] = [
"authenticatorAuthenticationResponse": authResponse,
]
if let tenant = tenantID {
postBody["tenantId"] = tenant
}
return postBody
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* 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 FinalizePasskeySignInResponse: AuthRPCResponse {
/// The user raw access token.
let idToken: String
/// Refresh token for the authenticated user.
let refreshToken: String

init(dictionary: [String: AnyHashable]) throws {
guard
let idToken = dictionary["idToken"] as? String,
let refreshToken = dictionary["refreshToken"] as? String
else {
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
}
self.idToken = idToken
self.refreshToken = refreshToken
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ struct GetAccountInfoResponse: AuthRPCResponse {

let mfaEnrollments: [AuthProtoMFAEnrollment]?

/// A list of the user’s enrolled passkeys.
let enrolledPasskeys: [PasskeyInfo]?

/// Designated initializer.
/// - Parameter dictionary: The provider user info data from endpoint.
init(dictionary: [String: Any]) {
Expand Down Expand Up @@ -133,6 +136,11 @@ struct GetAccountInfoResponse: AuthRPCResponse {
} else {
mfaEnrollments = nil
}
if let passkeyEnrollmentData = dictionary["passkeyInfo"] as? [[String: AnyHashable]] {
enrolledPasskeys = passkeyEnrollmentData.map { PasskeyInfo(dictionary: $0) }
} else {
enrolledPasskeys = nil
}
}
}

Expand Down
40 changes: 40 additions & 0 deletions FirebaseAuth/Sources/Swift/Backend/RPC/Proto/PasskeyInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

public final class PasskeyInfo: NSObject, AuthProto, NSSecureCoding, Sendable {
/// The display name for this passkey.
public let name: String?
/// The credential ID used by the server.
public let credentialID: String?
required init(dictionary: [String: AnyHashable]) {
name = dictionary["name"] as? String
credentialID = dictionary["credentialId"] as? String
}

// NSSecureCoding
public static var supportsSecureCoding: Bool { true }

public func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(credentialID, forKey: "credentialId")
}

public required init?(coder: NSCoder) {
name = coder.decodeObject(of: NSString.self, forKey: "name") as String?
credentialID = coder.decodeObject(of: NSString.self, forKey: "credentialId") as String?
super.init()
}
}
Loading
Loading