Ver Fonte

passkey feature implementation

Srushti Vaidya há 11 meses atrás
pai
commit
c21d47f240
23 ficheiros alterados com 871 adições e 4 exclusões
  1. 38 0
      FirebaseAuth/Sources/Public/well-known/apple-app-site-association.json
  2. 73 0
      FirebaseAuth/Sources/Swift/Auth/Auth.swift
  3. 33 0
      FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
  4. 51 0
      FirebaseAuth/Sources/Swift/Backend/RPC/FinalizePasskeyEnrollmentRequest.swift
  5. 28 0
      FirebaseAuth/Sources/Swift/Backend/RPC/FinalizePasskeyEnrollmentResponse.swift
  6. 43 0
      FirebaseAuth/Sources/Swift/Backend/RPC/FinalizePasskeySignInRequest.swift
  7. 26 0
      FirebaseAuth/Sources/Swift/Backend/RPC/FinalizePasskeySignInResponse.swift
  8. 12 0
      FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoResponse.swift
  9. 48 0
      FirebaseAuth/Sources/Swift/Backend/RPC/Proto/AuthProtoPasskeyInfo.swift
  10. 7 0
      FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoRequest.swift
  11. 38 0
      FirebaseAuth/Sources/Swift/Backend/RPC/StartPasskeyEnrollmentRequest.swift
  12. 64 0
      FirebaseAuth/Sources/Swift/Backend/RPC/StartPasskeyEnrollmentResponse.swift
  13. 24 0
      FirebaseAuth/Sources/Swift/Backend/RPC/StartPasskeySignInRequest.swift
  14. 25 0
      FirebaseAuth/Sources/Swift/Backend/RPC/StartPasskeySignInResponse.swift
  15. 100 0
      FirebaseAuth/Sources/Swift/User/User.swift
  16. 2 0
      FirebaseAuth/Sources/Swift/User/UserInfo.swift
  17. 2 0
      FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift
  18. 33 1
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift
  19. 1 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/UserActions.swift
  20. 42 3
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift
  21. 102 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift
  22. 76 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/PasskeyViewController.swift
  23. 3 0
      FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/UserViewController.swift

+ 38 - 0
FirebaseAuth/Sources/Public/well-known/apple-app-site-association.json

@@ -0,0 +1,38 @@
+{
+  "applinks": {
+      "details": [
+           {
+             "appID": "EQHXZ8M8AV.com.google.firebaseAuthSDKSampleApp.dev",
+             "components": [
+               {
+                  "#": "no_universal_links",
+                  "exclude": true,
+                  "comment": "Matches any URL with a fragment that equals no_universal_links and instructs the system not to open it as a universal link."
+               },
+               {
+                  "/": "/buy/*",
+                  "comment": "Matches any URL with a path that starts with /buy/."
+               },
+               {
+                  "/": "/help/website/*",
+                  "exclude": true,
+                  "comment": "Matches any URL with a path that starts with /help/website/ and instructs the system not to open it as a universal link."
+               },
+               {
+                  "/": "/help/*",
+                  "?": { "articleNumber": "????" },
+                  "comment": "Matches any URL with a path that starts with /help/ and that has a query item with name 'articleNumber' and a value of exactly four characters."
+               }
+             ]
+           }
+       ]
+   },
+   "webcredentials": {
+      "apps": [ "EQHXZ8M8AV.com.google.firebaseAuthSDKSampleApp.dev" ]
+   },
+
+
+    "appclips": {
+        "apps": ["EQHXZ8M8AV.com.google.firebaseAuthSDKSampleApp.dev.Clip"]
+    }
+}

+ 73 - 0
FirebaseAuth/Sources/Swift/Auth/Auth.swift

@@ -18,6 +18,7 @@ import FirebaseAppCheckInterop
 import FirebaseAuthInterop
 import FirebaseCore
 import FirebaseCoreExtension
+
 #if COCOAPODS
   internal import GoogleUtilities
 #else
@@ -28,6 +29,10 @@ import FirebaseCoreExtension
 #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
   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
@@ -824,6 +829,62 @@ extension Auth: AuthInterop {
       }
     }
   }
+  
+  let sessionId = "sessionId"
+  
+  // MARK: Passkeys
+  @available(iOS 15.0, *)
+  public func startPasskeySignIn() async throws -> ASAuthorizationPlatformPublicKeyCredentialAssertionRequest {
+    
+    let request = StartPasskeySignInRequest(
+      sessionId: sessionId,
+      requestConfiguration: requestConfiguration
+    )
+    
+    let response = try await self.backend.startPasskeySignIn(request: request)
+    
+    guard let challengeData = Data(base64Encoded: response.challenge ?? "nil") else {
+      throw NSError(domain: "com.firebase.auth", code: -3, userInfo: [NSLocalizedDescriptionKey: "Invalid challenge data"])
+    }
+    
+    let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
+      relyingPartyIdentifier: response.rpID ?? "fir-ios-auth-sample.web.app.com"
+    )
+    let assertionRequest = provider.createCredentialAssertionRequest(challenge: challengeData)
+    
+    return assertionRequest
+  }
+  
+  @available(iOS 15.0, *)
+  public func finalizePasskeySignIn(platformCredential: ASAuthorizationPlatformPublicKeyCredentialAssertion) async throws -> AuthDataResult {
+    guard 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()
+    else {
+      throw NSError(domain: "com.firebase.auth", code: -4, userInfo: [NSLocalizedDescriptionKey: "Invalid platform credential data"])
+    }
+    
+    let request = FinalizePasskeySignInRequest(credentialID: credentialID,
+                                               clientDataJson: clientDataJson,
+                                               authenticatorData: authenticatorData,
+                                               signature: signature,
+                                               userID: userID,
+                                               requestConfiguration: requestConfiguration)
+    
+    let response = try await backend.finalizePasskeySignIn(request: request)
+    
+    let user = try await completeSignIn(withAccessToken: response.idToken,
+                                        accessTokenExpirationDate: nil,
+                                        refreshToken: response.refreshToken,
+                                        anonymous: false)
+    let authDataResult = AuthDataResult(withUser: user, additionalUserInfo: nil)
+    
+    try updateCurrentUser(user, byForce: false, savingToDisk: false)
+    return authDataResult
+  }
+  
 
   /// Creates and, on success, signs in a user with the given email address and password.
   ///
@@ -2425,3 +2486,15 @@ extension Auth: AuthInterop {
   /// Mutations should occur within a @synchronized(self) context.
   private var listenerHandles: NSMutableArray = []
 }
+
+extension Data {
+    func base64EncodedString() -> String? {
+        return base64EncodedString()
+    }
+}
+
+extension String {
+    func base64EncodedString() -> String? {
+      return self.data(using: .utf8)?.base64EncodedString()
+    }
+}

+ 33 - 0
FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift

@@ -37,6 +37,39 @@ final class AuthBackend: AuthBackendProtocol {
   init(rpcIssuer: any AuthBackendRPCIssuerProtocol) {
     self.rpcIssuer = rpcIssuer
   }
+  
+  public func startPasskeyEnrollment(request: StartPasskeyEnrollmentRequest) async throws -> StartPasskeyEnrollmentResponse {
+    guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
+      throw AuthErrorUtils.error(code: AuthErrorCode.operationNotAllowed, message: "OS version is not supported for this action.")
+    }
+    let response = try await call(with: request) as StartPasskeyEnrollmentResponse
+    return response
+  }
+  
+  public func finalizePasskeyEnrollment(request: FinalizePasskeyEnrollmentRequest) async throws -> FinalizePasskeyEnrollmentResponse {
+    guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
+      throw AuthErrorUtils.error(code: AuthErrorCode.operationNotAllowed, message: "OS version is not supported for this action.")
+    }
+    let response = try await call(with: request) as FinalizePasskeyEnrollmentResponse
+    return response
+  }
+  
+  public func startPasskeySignIn(request: StartPasskeySignInRequest) async throws -> StartPasskeySignInResponse {
+    guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
+      throw AuthErrorUtils.error(code: AuthErrorCode.operationNotAllowed, message: "OS version is not supported for this action.")
+    }
+    let response = try await call(with: request) as StartPasskeySignInResponse
+    return response
+  }
+  
+  public func finalizePasskeySignIn(request: FinalizePasskeySignInRequest) async throws -> FinalizePasskeySignInResponse {
+    guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else{
+      throw AuthErrorUtils.error(code: AuthErrorCode.operationNotAllowed, message: "OS version is not supported for this action.")
+    }
+    let response = try await call(with: request) as FinalizePasskeySignInResponse
+    return response
+  }
+  
 
   /// Calls the RPC using HTTP request.
   /// Possible error responses:

+ 51 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/FinalizePasskeyEnrollmentRequest.swift

@@ -0,0 +1,51 @@
+import Foundation
+/// Represents the request for the `finalizePasskeyEnrollment` endpoint.
+@available(iOS 13, *)
+class FinalizePasskeyEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest {
+  typealias Response = FinalizePasskeyEnrollmentResponse
+  var unencodedHTTPRequestBody: [String : AnyHashable]?
+  
+  /// GCIP endpoint for finalizePasskeyEnrollment RPC.
+  let kFinalizePasskeyEnrollmentEndpoint = "accounts/passkeyEnrollment:finalize"
+  
+  
+  /// The raw user access token.
+  let idToken: String
+  //  name of user or passkey ?.?
+  let name: String
+  /// The credential ID.
+  var credentialID: String = "id"
+  /// The CollectedClientData object from the authenticator.
+  var clientDataJson: String = "clientDataJSON"
+  /// The attestation object from the authenticator.
+  var attestationObject: String = "response"
+  
+  /// The request configuration.
+  let requestConfiguration: AuthRequestConfiguration?
+  
+  
+  /// Initializes a new `FinalizePasskeyEnrollmentRequest`.
+  ///
+  /// - Parameters:
+  ///   - IDToken: The raw user access token.
+  ///   - name: The passkey name.
+  ///   - credentialID: The credential ID.
+  ///   - clientDataJson: The CollectedClientData object from the authenticator.
+  ///   - attestationObject: The attestation object from the authenticator.
+  ///   - requestConfiguration: The request configuration.
+  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
+    self.requestConfiguration = requestConfiguration
+    super.init(
+      endpoint: kFinalizePasskeyEnrollmentEndpoint,
+      requestConfiguration: requestConfiguration,
+      useIdentityPlatform: true
+    )
+  }
+}
+

+ 28 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/FinalizePasskeyEnrollmentResponse.swift

@@ -0,0 +1,28 @@
+import Foundation
+
+/// Represents the response from the `finalizePasskeyEnrollment` endpoint.
+@available(iOS 13, *)
+struct FinalizePasskeyEnrollmentResponse: AuthRPCResponse {
+
+    /// The ID token for the authenticated user.
+    public let idToken: String
+
+    /// The refresh token for the authenticated user.
+    public let refreshToken: String
+
+    private static let kIdTokenKey = "idToken"
+    private static let kRefreshTokenKey = "refreshToken"
+
+    /// Initializes a new `FinalizePasskeyEnrollmentResponse` from a dictionary.
+    ///
+    /// - Parameter dictionary: The dictionary containing the response data.
+    /// - Throws: An error if parsing fails.
+    public init(dictionary: [String: AnyHashable]) throws {
+        guard let idToken = dictionary[Self.kIdTokenKey] as? String,
+              let refreshToken = dictionary[Self.kRefreshTokenKey] as? String else {
+            throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
+        }
+        self.idToken = idToken
+        self.refreshToken = refreshToken
+    }
+}

+ 43 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/FinalizePasskeySignInRequest.swift

@@ -0,0 +1,43 @@
+import Foundation
+import AuthenticationServices
+
+/// Represents the request for the `finalizePasskeySignIn` endpoint.
+@available(iOS 13, *)
+class FinalizePasskeySignInRequest: IdentityToolkitRequest, AuthRPCRequest {
+  typealias Response = FinalizePasskeySignInResponse
+  var unencodedHTTPRequestBody: [String : AnyHashable]?
+  
+  
+  /// GCIP endpoint for finalizePasskeySignIn RPC.
+  private let finalizePasskeySignInEndpoint = "accounts/passkeySignIn:finalize"
+  
+  /// The signature from the authenticator.
+  let signature: String
+  /// Identifier for the registered credential.
+  var credentialID: String = "id"
+  /// The CollectedClientData object from the authenticator.
+  var clientDataJSON: String = "clientDataJSON"
+  /// The AuthenticatorData from the authenticator.
+  var authenticatorData: String = "response"
+  /// The user ID.
+  let userID: String
+  
+  /// Initializes a new `FinalizePasskeySignInRequest` with platform credential and request configuration.
+  ///
+  /// - Parameters:
+  ///   - credentialID: The credential ID.
+  ///   - clientDataJson: The CollectedClientData object from the authenticator.
+  ///   - authenticatorData: The AuthenticatorData from the authenticator.
+  ///   - signature: The signature from the authenticator.
+  ///   - userID: The user ID.
+  ///   - requestConfiguration: An object containing configurations to be added to the request.
+  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)
+  }
+}

+ 26 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/FinalizePasskeySignInResponse.swift

@@ -0,0 +1,26 @@
+import Foundation
+
+/// Represents the response from the `finalizePasskeySignIn` endpoint.
+@available(iOS 13, *)
+struct FinalizePasskeySignInResponse: AuthRPCResponse {
+
+    /// The ID token for the authenticated user.
+    var idToken: String = "idToken"
+
+    /// The refresh token for the authenticated user.
+    var refreshToken: String = "refreshToken"
+
+
+    /// Initializes a new `FinalizePasskeySignInResponse` from a dictionary.
+    ///
+    /// - Parameter dictionary: The dictionary containing the response data.
+    /// - Throws: An error if parsing fails.
+    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
+    }
+}

+ 12 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoResponse.swift

@@ -91,6 +91,8 @@ struct GetAccountInfoResponse: AuthRPCResponse {
     let phoneNumber: String?
 
     let mfaEnrollments: [AuthProtoMFAEnrollment]?
+    
+    let enrolledPasskeys: [PasskeyInfo]?
 
     /// Designated initializer.
     /// - Parameter dictionary: The provider user info data from endpoint.
@@ -133,6 +135,16 @@ struct GetAccountInfoResponse: AuthRPCResponse {
       } else {
         mfaEnrollments = nil
       }
+      if let passkeyEnrollmentData = dictionary["passkeyInfo"] as? [[String: AnyHashable]] {
+        var enrolledPasskeys = [PasskeyInfo]()
+        for passkeyInfoDict in passkeyEnrollmentData {
+          let passkeyInfo = PasskeyInfo(dictionary: passkeyInfoDict)
+          enrolledPasskeys.append(passkeyInfo)
+        }
+        self.enrolledPasskeys = enrolledPasskeys
+      } else {
+        self.enrolledPasskeys = nil
+      }
     }
   }
 

+ 48 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/Proto/AuthProtoPasskeyInfo.swift

@@ -0,0 +1,48 @@
+// Copyright 2023 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
+
+#if os(watchOS)
+#else
+/// Represents information about a Passkey.
+public struct PasskeyInfo: Codable, AuthProto {
+    /// The name of the Passkey.
+    public let name: String?
+    
+    /// The credential ID of the Passkey.
+    public let credentialID: String?
+
+    /// Creates a `PasskeyInfo` instance from a dictionary.
+    ///
+    /// - Parameter dictionary: A dictionary containing the Passkey info.
+    public init(dictionary: [String: AnyHashable]) {
+        self.name = dictionary["name"] as? String
+        self.credentialID = dictionary["credentialId"] as? String
+    }
+    
+    // MARK: - AuthProto conformance
+    
+    public func toDictionary() -> [String: AnyHashable] {
+        var dictionary: [String: AnyHashable] = [:]
+        if let name = name {
+          dictionary["name"] = name
+        }
+        if let credentialID = credentialID {
+          dictionary["credentialId"] = credentialID
+        }
+        return dictionary
+    }
+}
+#endif

+ 7 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoRequest.swift

@@ -70,6 +70,8 @@ private let kDeleteAttributesKey = "deleteAttribute"
 /// The key for the "deleteProvider" value in the request.
 private let kDeleteProvidersKey = "deleteProvider"
 
+private let kDeletePasskeysKey = "deletePasskeys"
+
 /// The key for the "returnSecureToken" value in the request.
 private let kReturnSecureTokenKey = "returnSecureToken"
 
@@ -126,6 +128,8 @@ class SetAccountInfoRequest: IdentityToolkitRequest, AuthRPCRequest {
 
   /// The list of identity providers to delete.
   var deleteProviders: [String]?
+  
+  var deletePasskeys: [String]?
 
   /// Whether the response should return access token and refresh token directly.
   /// The default value is `true` .
@@ -180,6 +184,9 @@ class SetAccountInfoRequest: IdentityToolkitRequest, AuthRPCRequest {
     if let deleteProviders {
       postBody[kDeleteProvidersKey] = deleteProviders
     }
+    if let deletePasskeys {
+      postBody[kDeletePasskeysKey] = deletePasskeys
+    }
     if returnSecureToken {
       postBody[kReturnSecureTokenKey] = true
     }

+ 38 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/StartPasskeyEnrollmentRequest.swift

@@ -0,0 +1,38 @@
+import Foundation
+
+/// Represents the parameters for the `startPasskeyEnrollment` endpoint.
+@available(iOS 13, *)
+class StartPasskeyEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest {
+  typealias Response = StartPasskeyEnrollmentResponse
+  var unencodedHTTPRequestBody: [String : AnyHashable]?
+  
+  /// The raw user access token.
+  let idToken: String
+  
+  /// The tenant ID for the request.
+  let tenantId: String?
+  
+  /// The endpoint for the request.
+  private let kStartPasskeyEnrollmentEndpoint = "accounts/passkeyEnrollment:start"
+  
+  
+  /// The request configuration.
+  let requestConfiguration: AuthRequestConfiguration?
+  
+  /// Initializes a new `StartPasskeyEnrollmentRequest`.
+  ///
+  /// - Parameters:
+  ///   - idToken: The raw user access token.
+  ///   - requestConfiguration: The request configuration.
+  ///   - tenantId: The tenant ID for the request.
+  init(idToken: String, requestConfiguration: AuthRequestConfiguration?, tenantId: String? = nil) {
+    self.idToken = idToken
+    self.requestConfiguration = requestConfiguration
+    self.tenantId = tenantId
+    super.init(
+      endpoint: kStartPasskeyEnrollmentEndpoint,
+      requestConfiguration: requestConfiguration!,
+      useIdentityPlatform: true
+    )
+  }
+}

+ 64 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/StartPasskeyEnrollmentResponse.swift

@@ -0,0 +1,64 @@
+import Foundation
+
+/// Represents the response from the `startPasskeyEnrollment` endpoint.
+@available(iOS 13, *)
+struct StartPasskeyEnrollmentResponse: AuthRPCResponse {
+  
+  /// The RP ID of the FIDO Relying Party.
+  private(set) var rpID: String = "fir-ios-auth-sample.web.app.com"
+  
+  /// The user ID.
+  private(set) var userID: Data
+  
+  /// The FIDO challenge.
+  private(set) var challenge: Data
+  
+  ///  The name of the field in the response JSON for CredentialCreationOptions.
+  private let kOptionsKey = "credentialCreationOptions"
+  
+  /// The name of the field in the response JSON for Relying Party.
+  private let kRpKey = "rp"
+  
+  /// The name of the field in the response JSON for User.
+  private let kUserKey = "user"
+  
+  /// The name of the field in the response JSON for ids.
+  private let kIDKey = "id"
+  
+  /// The name of the field in the response JSON for challenge.
+  private let kChallengeKey = "challenge"
+  
+  
+  /// Initializes a new `StartPasskeyEnrollmentResponse` from a dictionary.
+  ///
+  /// - Parameters:
+  ///   - dictionary: The dictionary containing the response data.
+  /// - Throws: An error if parsing fails.
+  init(dictionary: [String: AnyHashable]) throws {
+    guard let options = dictionary[kOptionsKey] as? [String: AnyHashable] else {
+      throw NSError(domain: "com.firebase.auth", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing credentialCreationOptions"])
+    }
+    
+    guard let rp = options[kRpKey] as? [String: AnyHashable],
+          let rpID = rp[kIDKey] as? String, !rpID.isEmpty else {
+      throw NSError(domain: "com.firebase.auth", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing or invalid rpID"])
+    }
+    
+    guard let user = options[kUserKey] as? [String: AnyHashable],
+          let userID = user[kIDKey] as? String, !userID.isEmpty, let userIDData = userID.data(using: .utf8) else {
+      throw NSError(domain: "com.firebase.auth", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing or invalid userID"])
+    }
+    
+    guard let challenge = options[kChallengeKey] as? String, !challenge.isEmpty, let challengeData = challenge.data(using: .utf8) else {
+      throw NSError(domain: "com.firebase.auth", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing or invalid challenge"])
+    }
+    self.rpID = rpID
+    self.userID = userIDData
+    self.challenge = challengeData
+  }
+  
+  // MARK: - AuthRPCResponse default implementation
+  func clientError(shortErrorMessage: String, detailedErrorMessage: String? = nil) -> Error? {
+    return nil
+  }
+}

+ 24 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/StartPasskeySignInRequest.swift

@@ -0,0 +1,24 @@
+import Foundation
+import FirebaseAuthInterop
+
+/// The request to start passkey sign in.
+@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
+class StartPasskeySignInRequest: IdentityToolkitRequest, AuthRPCRequest {
+  typealias Response = StartPasskeySignInResponse
+  var unencodedHTTPRequestBody: [String : AnyHashable]?
+
+  private let kStartPasskeySignInEndpoint = "accounts/passkeySignIn:start"
+
+    /// The sessionID
+    var sessionId: String
+    /// Designated initializer.
+    /// - Parameter sessionId: The sessionId for the request.
+    init(sessionId: String, requestConfiguration: AuthRequestConfiguration) {
+        self.sessionId = sessionId
+        super.init(
+          endpoint: kStartPasskeySignInEndpoint,
+          requestConfiguration: requestConfiguration
+        )
+    }
+}
+

+ 25 - 0
FirebaseAuth/Sources/Swift/Backend/RPC/StartPasskeySignInResponse.swift

@@ -0,0 +1,25 @@
+import Foundation
+
+@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
+struct StartPasskeySignInResponse: AuthRPCResponse {
+  /// The relying party ID.
+  private(set) var  rpID: String? = "fir-ios-auth-sample.web.app.com"
+  /// The FIDO challenge.
+  private(set) var challenge: String? = "challenge"
+  
+  private let options = "options"
+
+    
+    enum CodingKeys: String, CodingKey {
+        case credentialRequestOptions = "credentialRequestOptions"
+        case rpID = "rpId"
+        case challenge
+    }
+   init(dictionary: [String : AnyHashable]) throws {
+    let options = dictionary["options"] as? [String: AnyHashable]
+    let rpID = options?["rpId"] as? String
+    let challenge = options?["challenge"] as? String
+    self.rpID = rpID
+    self.challenge = challenge
+  }
+}

+ 100 - 0
FirebaseAuth/Sources/Swift/User/User.swift

@@ -13,6 +13,8 @@
 // limitations under the License.
 
 import Foundation
+import AuthenticationServices
+
 
 @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
 extension User: NSSecureCoding {}
@@ -57,6 +59,9 @@ extension User: NSSecureCoding {}
 
   /// The tenant ID of the current user. `nil` if none is available.
   @objc public private(set) var tenantID: String?
+  
+  /// The list of enrolled passkeys for the user.
+      public private(set) var enrolledPasskeys: [PasskeyInfo]?
 
   #if os(iOS)
     /// Multi factor object associated with the user.
@@ -1046,6 +1051,98 @@ extension User: NSSecureCoding {}
       }
     }
   }
+  
+  /// Current user object.
+  var currentUser: User?
+  
+  /// Starts the passkey enrollment flow, creating a platform public key registration request.
+  ///
+  /// - Parameter name: The desired name for the passkey.
+  /// - Returns: The ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest.
+  @available(iOS 15.0, *)
+  public func startPasskeyEnrollmentWithName(withName name: String?) async throws -> ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest {
+    let idToken = rawAccessToken()
+    let request = StartPasskeyEnrollmentRequest(
+      idToken: idToken,
+      requestConfiguration: requestConfiguration,
+      tenantId: auth?.tenantID
+    )
+    let response = try await backend.startPasskeyEnrollment(request: request)
+    // Cache the passkey name
+    passkeyName = name
+    let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: response.rpID)
+    let registrationRequest = provider.createCredentialRegistrationRequest(
+      challenge: response.challenge,
+      name: self.passkeyName ?? "Unnamed account (Apple)",
+      userID: response.userID
+    )
+    return registrationRequest
+  }
+  
+  @available(iOS 15.0, *)
+  public func finalizePasskeyEnrollmentWithPlatformCredentials(platformCredential: ASAuthorizationPlatformPublicKeyCredentialRegistration) async throws -> AuthDataResult {
+    
+    let credentialID = platformCredential.credentialID.base64EncodedString() ?? "nil"
+    let clientDataJson = platformCredential.rawClientDataJSON.base64EncodedString() ?? "nil"
+    let attestationObject = platformCredential.rawAttestationObject!.base64EncodedString()
+    
+    let rawAccessToken = self.rawAccessToken
+    let request = FinalizePasskeyEnrollmentRequest(
+      idToken: rawAccessToken(),
+      name: passkeyName!,
+      credentialID: credentialID,
+      clientDataJson: clientDataJson,
+      attestationObject: attestationObject!,
+      requestConfiguration: self.auth!.requestConfiguration
+    )
+    
+    let response = try await backend.finalizePasskeyEnrollment(request: request)
+    
+    let user = try await self.auth!.completeSignIn(
+      withAccessToken: response.idToken,
+      accessTokenExpirationDate: nil,
+      refreshToken: response.refreshToken,
+      anonymous: false
+    )
+    return AuthDataResult(withUser: user, additionalUserInfo: nil)
+  }
+  
+  
+  /// To unenroll a passkey with platform credential.
+  /// - Parameter credentialID: The passkey credential ID to unenroll.
+  @objc open func unenrollPasskey(with credentialID: String, completion: ((Error?) -> Void)? = nil) {
+    kAuthGlobalWorkQueue.async {
+      self.internalGetToken(backend: self.backend) { accessToken, error in
+        if let error {
+          User.callInMainThreadWithError(callback: completion, error: error)
+          return
+        }
+        guard let accessToken = accessToken else {
+          fatalError("Auth Internal Error: Both error and accessToken are nil")
+        }
+        guard let requestConfiguration = self.auth?.requestConfiguration else {
+          fatalError("Auth Internal Error: Missing request configuration.")
+        }
+        
+        self.executeUserUpdateWithChanges(changeBlock: { user, request in
+          request.deletePasskeys = [credentialID]
+        }) { error in
+          if let error {
+            User.callInMainThreadWithError(callback: completion, error: error)
+            return
+          }
+          
+          // Remove passkey from local cache
+          if let enrolledPasskeys = self.enrolledPasskeys, let index = enrolledPasskeys.firstIndex(where: { $0.credentialID == credentialID }) {
+            self.enrolledPasskeys?.remove(at: index)
+          }
+          
+          User.callInMainThreadWithError(callback: completion, error: nil)
+        }
+      }
+    }
+  }
+  
 
   // MARK: Internal implementations below
 
@@ -1111,6 +1208,8 @@ extension User: NSSecureCoding {}
 
   /// The name of the user.
   @objc open var displayName: String?
+  
+  open var passkeyName: String?
 
   /// The URL of the user's profile photo.
   @objc open var photoURL: URL?
@@ -1302,6 +1401,7 @@ extension User: NSSecureCoding {}
       }
       multiFactor.user = self
     #endif
+    enrolledPasskeys = user.enrolledPasskeys
   }
 
   #if os(iOS)

+ 2 - 0
FirebaseAuth/Sources/Swift/User/UserInfo.swift

@@ -35,4 +35,6 @@ import Foundation
   ///
   /// This property is only available for users authenticated via phone number auth.
   var phoneNumber: String? { get }
+  
+  var passkeyName: String? { get }
 }

+ 2 - 0
FirebaseAuth/Sources/Swift/User/UserInfoImpl.swift

@@ -20,6 +20,8 @@ extension UserInfoImpl: NSSecureCoding {}
 @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
 @objc(FIRUserInfoImpl) // objc Needed for decoding old versions
 class UserInfoImpl: NSObject, UserInfo {
+  var passkeyName: String?
+
   /// A convenience factory method for constructing a `UserInfo` instance from data
   /// returned by the getAccountInfo endpoint.
   /// - Parameter providerUserInfo: Data returned by the getAccountInfo endpoint.

+ 33 - 1
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift

@@ -53,6 +53,10 @@ enum AuthMenu: String {
   case phoneEnroll
   case totpEnroll
   case multifactorUnenroll
+  case passkeySignUp
+  case passkeySignIn
+  case passkeyEnroll
+  case passkeyUnenroll
 
   // More intuitively named getter for `rawValue`.
   var id: String { rawValue }
@@ -139,6 +143,15 @@ enum AuthMenu: String {
       return "TOTP Enroll"
     case .multifactorUnenroll:
       return "Multifactor unenroll"
+    // Passkey
+    case .passkeySignUp:
+      return "Sign Up with Passkey"
+    case .passkeySignIn:
+      return "Sign In with Passkey"
+    case .passkeyEnroll:
+      return "Enroll with Passkey"
+    case .passkeyUnenroll:
+      return "Unenroll Passkey"
     }
   }
 
@@ -220,6 +233,14 @@ enum AuthMenu: String {
       self = .totpEnroll
     case "Multifactor unenroll":
       self = .multifactorUnenroll
+    case "Sign Up with Passkey":
+      self = .passkeySignUp
+    case "Sign In with Passkey":
+      self = .passkeySignIn
+    case "Enroll with Passkey":
+      self = .passkeyEnroll
+    case "Unenroll Passkey":
+      self = .passkeyUnenroll
     default:
       return nil
     }
@@ -353,10 +374,21 @@ 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.passkeySignIn.name),
+      Item(title: AuthMenu.passkeyEnroll.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 }

+ 1 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/UserActions.swift

@@ -26,4 +26,5 @@ enum UserAction: String {
   case updatePhoneNumber = "Phone Number"
   case refreshUserInfo = "Refresh User Info"
   case updatePassword = "Update Password"
+  case passkey = "Passkeys"
 }

+ 42 - 3
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AccountLinkingViewController.swift

@@ -523,6 +523,37 @@ extension AccountLinkingViewController: DataSourceProvidable {
   }
 }
 
+// MARK: - Passkey Enrollment
+@available(iOS 16.0, *)
+private func handlePasskeyEnrollment(platformCredential: ASAuthorizationPlatformPublicKeyCredentialRegistration){
+    let user = Auth.auth().currentUser
+    Task {
+        do {
+          let authResult = try await user?.finalizePasskeyEnrollmentWithPlatformCredentials(
+            platformCredential: platformCredential
+          )
+          print("Passkey Enrollment succeeded with uid: \(authResult?.user.uid ?? "empty with uid")")
+        } catch {
+            print("Passkey Enrollment failed with error: \(error)")
+        }
+    }
+}
+
+// MARK: - Passkey Sign-in
+@available(iOS 16.0, *)
+private func handlePasskeySignIn(platformCredential: ASAuthorizationPlatformPublicKeyCredentialAssertion){
+    Task {
+        do {
+            let authResult = try await AppManager.shared.auth().finalizePasskeySignIn(platformCredential: platformCredential)
+          print("Passkey sign-in succeeded with uid: \(authResult.user.uid)")
+
+        } catch {
+            print("Passkey sign-in failed with error: \(error)")
+        }
+    }
+}
+
+
 // MARK: - Implementing Sign in with Apple with Firebase
 
 extension AccountLinkingViewController: ASAuthorizationControllerDelegate,
@@ -541,10 +572,18 @@ extension AccountLinkingViewController: ASAuthorizationControllerDelegate,
       authorizationController.performRequests()
     }
   }
+  
+  // MARK: - ASAuthorizationControllerDelegate
 
-  func authorizationController(controller: ASAuthorizationController,
-                               didCompleteWithAuthorization authorization: ASAuthorization) {
-    if case let appleIDCredential as ASAuthorizationAppleIDCredential = authorization.credential {
+  @available(iOS 13.0, *)
+  func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
+    if #available(iOS 16.0, *) {
+      if let platformCredential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
+        handlePasskeyEnrollment(platformCredential: platformCredential)
+      } else if let platformAssertionCredential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
+        handlePasskeySignIn(platformCredential: platformAssertionCredential)
+      }
+    } else if case let appleIDCredential as ASAuthorizationAppleIDCredential = authorization.credential {
       continuation?.resume(returning: appleIDCredential)
     } else {
       fatalError("Unexpected authorization credential type.")

+ 102 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift

@@ -191,6 +191,18 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
 
     case .multifactorUnenroll:
       mfaUnenroll()
+      
+    case .passkeySignUp:
+      passkeySignUp()
+      
+    case .passkeySignIn:
+      passkeySignIn()
+      
+    case .passkeyEnroll:
+      passkeyEnroll()
+      
+    case .passkeyUnenroll:
+      passkeyUnenroll()
     }
   }
 
@@ -921,6 +933,96 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
       }
     }
   }
+  
+  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
+    }
+    // Sign in anonymously
+    AppManager.shared.auth().signInAnonymously { [weak self] result, error in
+      guard let self = self else { return }
+      if let error = error {
+        self.showAlert(for:"sign-in anonymously failed")
+        print("Sign in anonymously first")
+      } else {
+        print("sign-in anonymously succeeded.")
+        if let user = result?.user {
+          print("User ID : \(user.uid)")
+          self.passkeyEnroll()
+        }else{
+          self.showAlert(for: "sign-in anonymously failed: User is nil")
+          print("sign-in anonymously failed: User is nil")
+        }
+      }
+    }
+  }
+  
+  func passkeySignIn() {
+    guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
+      print("OS version is not supported for this action.")
+      return
+    }
+    Task {
+      do {
+        let request = try await AppManager.shared.auth().startPasskeySignIn()
+        let controller = ASAuthorizationController(authorizationRequests: [request])
+        controller.delegate = self
+        controller.presentationContextProvider = self
+        controller.performRequests(options: .preferImmediatelyAvailableCredentials)
+      } catch {
+        print("Passkey sign-in failed with error: \(error)")
+      }
+    }
+  }
+  
+  func passkeyEnroll() {
+    guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
+      print("OS version is not supported for this action.")
+      return
+    }
+    guard let user = Auth.auth().currentUser else {
+      print("Please sign in first.")
+      return
+    }
+    showTextInputPrompt(with: "Passkey name") { [weak self] passkeyName in
+      let passkeyName: String = passkeyName
+      print ("Passkey Name is \(passkeyName)")
+      Task {
+        do {
+          let regRequest = try await user.startPasskeyEnrollmentWithName(withName: passkeyName)
+          print("request done \(regRequest)")
+          let controller = ASAuthorizationController(authorizationRequests: [regRequest])
+          controller.delegate = self
+          controller.presentationContextProvider = self
+          controller.performRequests()
+        } catch {
+          print("Error during passkey enrollment: \(error)")
+        }
+      }
+    }
+  }
+  
+  func passkeyUnenroll() {
+    guard let user = Auth.auth().currentUser else {
+      print("Please sign in first.")
+      return
+    }
+    Task {
+      guard let credentialID = await showTextInputPrompt(with: "passkey credential ID") else {
+        print("User cancelled or didn't provide credential ID.")
+        return
+      }
+      user.unenrollPasskey(with: credentialID) { [weak self] error in
+        guard let self = self else { return }
+        if let error = error {
+          print("Withdraw passkey with credential ID: \(credentialID) failed with error: \(error)")
+        } else {
+          print("Withdraw passkey with credential ID: \(credentialID) succeeded")
+        }
+      }
+    }
+  }
 
   // MARK: - Private Helpers
 

+ 76 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/PasskeyViewController.swift

@@ -0,0 +1,76 @@
+// 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 FirebaseAuth
+import FirebaseCore
+import AuthenticationServices
+
+class PasskeyViewController: UIViewController{
+  
+}
+
+//func passkeySignUp(appManager: AppManager, logFailure: @escaping (String, Error?) -> Void, logSuccess: @escaping (String) -> Void, log: @escaping (String) -> Void, passkeyEnroll: @escaping () -> Void) {
+//    // Sign in anonymously
+//  appManager.auth().signInAnonymously { (result: AuthDataResult?, error: Error?) in
+//        if let error = error {
+//          logFailure("sign-in anonymously failed", error)
+//        } else if let user = result?.user {
+//            logSuccess("sign-in anonymously succeeded.")
+//            log("User ID : \(user.uid)")
+//            passkeyEnroll() // Call passkeyEnroll after successful anonymous sign-in
+//        } else {
+//          logFailure("sign-in anonymously failed", nil)
+//        }
+//    }
+//}
+//
+//private func passkeySignIn(){
+//  user?.startPasskeyEnrollmentWithName(withName: <#T##String?#>)
+//}
+//
+//func passkeyEnroll(
+//    appManager: AppManager,
+//    logFailure: @escaping (String, Error?) -> Void,
+//    log: @escaping (String) -> Void,
+//    showTextInputPrompt: @escaping (String, UIKeyboardType, @escaping (Bool, String?) -> Void) -> Void,
+//    presentationContextProvider: ASAuthorizationControllerPresentationContextProviding,
+//    authorizationControllerDelegate: ASAuthorizationControllerDelegate
+//) async {
+//    guard let user = appManager.auth().currentUser else {
+//        logFailure("Please sign in first.", nil)
+//        return
+//    }
+//
+//  guard let passkeyName = await showTextInputPrompt("passkey name", keyboardType: UIKeyboardType = .default) else {
+//        return
+//    }
+//
+//    if #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) {
+//        do {
+//            let request = try await user.startPasskeyEnrollmentWithName(withName: passkeyName)
+//            let controller = ASAuthorizationController(authorizationRequests: [request])
+//            controller.delegate = authorizationControllerDelegate
+//            controller.presentationContextProvider = presentationContextProvider
+//            controller.performRequests()
+//        } catch {
+//            logFailure("Passkey enrollment failed", error)
+//        }
+//    } else {
+//        log("OS version is not supported for this action.")
+//    }
+//}
+//
+//func passkeyUnenroll(){
+//  
+//}

+ 3 - 0
FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/UserViewController.swift

@@ -112,6 +112,9 @@ class UserViewController: UIViewController, DataSourceProviderDelegate {
 
     case .refreshUserInfo:
       refreshUserInfo()
+    
+    case .passkey:
+      user?.passkeyName
     }
   }