Skip to content

Commit 006848e

Browse files
committed
implementing start passkey enrollment (#15162)
1 parent 74761d1 commit 006848e

18 files changed

+1106
-1
lines changed

FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ final class AuthBackend: AuthBackendProtocol {
355355
.missingIosBundleIDError(message: serverDetailErrorMessage)
356356
case "MISSING_ANDROID_PACKAGE_NAME": return AuthErrorUtils
357357
.missingAndroidPackageNameError(message: serverDetailErrorMessage)
358+
case "PASSKEY_ENROLLMENT_NOT_FOUND": return AuthErrorUtils.missingPasskeyEnrollment()
358359
case "UNAUTHORIZED_DOMAIN": return AuthErrorUtils
359360
.unauthorizedDomainError(message: serverDetailErrorMessage)
360361
case "INVALID_CONTINUE_URI": return AuthErrorUtils
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
/// The GCIP endpoint for finalizePasskeyEnrollment rpc
18+
private let finalizePasskeyEnrollmentEndPoint = "accounts/passkeyEnrollment:finalize"
19+
20+
@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
21+
class FinalizePasskeyEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest {
22+
typealias Response = FinalizePasskeyEnrollmentResponse
23+
24+
/// The raw user access token.
25+
let idToken: String
26+
/// The passkey name.
27+
let name: String
28+
/// The credential ID.
29+
let credentialID: String
30+
/// The CollectedClientData object from the authenticator.
31+
let clientDataJSON: String
32+
/// The attestation object from the authenticator.
33+
let attestationObject: String
34+
35+
init(idToken: String,
36+
name: String,
37+
credentialID: String,
38+
clientDataJSON: String,
39+
attestationObject: String,
40+
requestConfiguration: AuthRequestConfiguration) {
41+
self.idToken = idToken
42+
self.name = name
43+
self.credentialID = credentialID
44+
self.clientDataJSON = clientDataJSON
45+
self.attestationObject = attestationObject
46+
super.init(
47+
endpoint: finalizePasskeyEnrollmentEndPoint,
48+
requestConfiguration: requestConfiguration,
49+
useIdentityPlatform: true
50+
)
51+
}
52+
53+
var unencodedHTTPRequestBody: [String: AnyHashable]? {
54+
var postBody: [String: AnyHashable] = [
55+
"idToken": idToken,
56+
"name": name,
57+
]
58+
let authAttestationResponse: [String: AnyHashable] = [
59+
"clientDataJSON": clientDataJSON,
60+
"attestationObject": attestationObject,
61+
]
62+
let authRegistrationResponse: [String: AnyHashable] = [
63+
"id": credentialID,
64+
"response": authAttestationResponse,
65+
]
66+
postBody["authenticatorRegistrationResponse"] = authRegistrationResponse
67+
if let tenantId = tenantID {
68+
postBody["tenantId"] = tenantId
69+
}
70+
return postBody
71+
}
72+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
18+
struct FinalizePasskeyEnrollmentResponse: AuthRPCResponse {
19+
/// The user raw access token.
20+
let idToken: String
21+
/// Refresh token for the authenticated user.
22+
let refreshToken: String
23+
24+
init(dictionary: [String: AnyHashable]) throws {
25+
guard
26+
let idToken = dictionary["idToken"] as? String,
27+
let refreshToken = dictionary["refreshToken"] as? String
28+
else {
29+
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
30+
}
31+
self.idToken = idToken
32+
self.refreshToken = refreshToken
33+
}
34+
}

FirebaseAuth/Sources/Swift/Backend/RPC/GetAccountInfoResponse.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ struct GetAccountInfoResponse: AuthRPCResponse {
9292

9393
let mfaEnrollments: [AuthProtoMFAEnrollment]?
9494

95+
/// A list of the user’s enrolled passkeys.
96+
let enrolledPasskeys: [PasskeyInfo]?
97+
9598
/// Designated initializer.
9699
/// - Parameter dictionary: The provider user info data from endpoint.
97100
init(dictionary: [String: Any]) {
@@ -133,6 +136,11 @@ struct GetAccountInfoResponse: AuthRPCResponse {
133136
} else {
134137
mfaEnrollments = nil
135138
}
139+
if let passkeyEnrollmentData = dictionary["passkeys"] as? [[String: AnyHashable]] {
140+
enrolledPasskeys = passkeyEnrollmentData.map { PasskeyInfo(dictionary: $0) }
141+
} else {
142+
enrolledPasskeys = nil
143+
}
136144
}
137145
}
138146

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
public final class PasskeyInfo: NSObject, AuthProto, NSSecureCoding, Sendable {
18+
/// The display name for this passkey.
19+
public let name: String?
20+
/// The credential ID used by the server.
21+
public let credentialID: String?
22+
required init(dictionary: [String: AnyHashable]) {
23+
name = dictionary["name"] as? String
24+
credentialID = dictionary["credentialId"] as? String
25+
}
26+
27+
// NSSecureCoding
28+
public static var supportsSecureCoding: Bool { true }
29+
30+
public func encode(with coder: NSCoder) {
31+
coder.encode(name, forKey: "name")
32+
coder.encode(credentialID, forKey: "credentialId")
33+
}
34+
35+
public required init?(coder: NSCoder) {
36+
name = coder.decodeObject(of: NSString.self, forKey: "name") as String?
37+
credentialID = coder.decodeObject(of: NSString.self, forKey: "credentialId") as String?
38+
super.init()
39+
}
40+
}

FirebaseAuth/Sources/Swift/Backend/RPC/SetAccountInfoRequest.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ private let kDeleteProvidersKey = "deleteProvider"
7373
/// The key for the "returnSecureToken" value in the request.
7474
private let kReturnSecureTokenKey = "returnSecureToken"
7575

76+
private let kDeletePasskeysKey = "deletePasskey"
77+
7678
/// The key for the tenant id value in the request.
7779
private let kTenantIDKey = "tenantId"
7880

@@ -131,6 +133,9 @@ class SetAccountInfoRequest: IdentityToolkitRequest, AuthRPCRequest {
131133
/// The default value is `true` .
132134
var returnSecureToken: Bool = true
133135

136+
/// The list of credential IDs of the passkeys to be deleted.
137+
var deletePasskeys: [String]? = nil
138+
134139
init(accessToken: String? = nil, requestConfiguration: AuthRequestConfiguration) {
135140
self.accessToken = accessToken
136141
super.init(endpoint: kSetAccountInfoEndpoint, requestConfiguration: requestConfiguration)
@@ -183,6 +188,9 @@ class SetAccountInfoRequest: IdentityToolkitRequest, AuthRPCRequest {
183188
if returnSecureToken {
184189
postBody[kReturnSecureTokenKey] = true
185190
}
191+
if let deletePasskeys {
192+
postBody[kDeletePasskeysKey] = deletePasskeys
193+
}
186194
if let tenantID {
187195
postBody[kTenantIDKey] = tenantID
188196
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
/// The GCIP endpoint for startPasskeyEnrollment rpc
18+
private let startPasskeyEnrollmentEndPoint = "accounts/passkeyEnrollment:start"
19+
20+
@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
21+
class StartPasskeyEnrollmentRequest: IdentityToolkitRequest, AuthRPCRequest {
22+
typealias Response = StartPasskeyEnrollmentResponse
23+
24+
/// The raw user access token
25+
let idToken: String
26+
27+
init(idToken: String,
28+
requestConfiguration: AuthRequestConfiguration) {
29+
self.idToken = idToken
30+
super.init(
31+
endpoint: startPasskeyEnrollmentEndPoint,
32+
requestConfiguration: requestConfiguration,
33+
useIdentityPlatform: true
34+
)
35+
}
36+
37+
var unencodedHTTPRequestBody: [String: AnyHashable]? {
38+
var body: [String: AnyHashable] = [
39+
"idToken": idToken,
40+
]
41+
if let tenantID = tenantID {
42+
body["tenantId"] = tenantID
43+
}
44+
return body
45+
}
46+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
18+
struct StartPasskeyEnrollmentResponse: AuthRPCResponse {
19+
/// The RP ID of the FIDO Relying Party.
20+
let rpID: String
21+
/// The user id
22+
let userID: String
23+
/// The FIDO challenge.
24+
let challenge: String
25+
26+
init(dictionary: [String: AnyHashable]) throws {
27+
guard let options = dictionary["credentialCreationOptions"] as? [String: Any] else {
28+
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
29+
}
30+
guard let rp = options["rp"] as? [String: Any],
31+
let rpID = rp["id"] as? String else {
32+
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
33+
}
34+
guard let user = options["user"] as? [String: Any],
35+
let userID = user["id"] as? String else {
36+
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
37+
}
38+
guard let challenge = options["challenge"] as? String else {
39+
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
40+
}
41+
self.rpID = rpID
42+
self.userID = userID
43+
self.challenge = challenge
44+
}
45+
}

0 commit comments

Comments
 (0)