Skip to content

Commit 714df23

Browse files
committed
implement start passkey enrollment
1 parent 4e62da1 commit 714df23

File tree

3 files changed

+148
-0
lines changed

3 files changed

+148
-0
lines changed
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+
}

FirebaseAuth/Sources/Swift/User/User.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414

1515
import Foundation
1616

17+
#if os(iOS) || os(tvOS) || os(macOS) || targetEnvironment(macCatalyst)
18+
import AuthenticationServices
19+
#endif
20+
1721
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
1822
extension User: NSSecureCoding {}
1923

@@ -1047,6 +1051,59 @@ extension User: NSSecureCoding {}
10471051
}
10481052
}
10491053

1054+
// MARK: Passkey Implementation
1055+
1056+
#if os(iOS) || os(tvOS) || os(macOS) || targetEnvironment(macCatalyst)
1057+
1058+
/// A cached passkey name being passed from startPasskeyEnrollment(withName:) call and consumed
1059+
/// at finalizePasskeyEnrollment(withPlatformCredential:) call
1060+
private var passkeyName: String?
1061+
1062+
/// Start the passkey enrollment creating a plaform public key creation request with the
1063+
/// challenge from GCIP backend.
1064+
/// - Parameter name: The name for the passkey to be created.
1065+
@available(iOS 15.0, macOS 12.0, tvOS 16.0, *)
1066+
public func startPasskeyEnrollment(withName name: String?) async throws
1067+
-> ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest {
1068+
guard auth != nil else {
1069+
/// If auth is nil, this User object is in an invalid state for this operation.
1070+
fatalError(
1071+
"Firebase Auth Internal Error: Set user's auth property with non-nil instance. Cannot start passkey enrollment."
1072+
)
1073+
}
1074+
let enrollmentIdToken = rawAccessToken()
1075+
let request = StartPasskeyEnrollmentRequest(
1076+
idToken: enrollmentIdToken,
1077+
requestConfiguration: requestConfiguration
1078+
)
1079+
let response = try await backend.call(with: request)
1080+
passkeyName = (name?.isEmpty ?? true) ? "Unnamed account (Apple)" : name!
1081+
guard let challengeInData = Data(base64Encoded: response.challenge) else {
1082+
throw NSError(
1083+
domain: AuthErrorDomain,
1084+
code: AuthInternalErrorCode.RPCResponseDecodingError.rawValue,
1085+
userInfo: [NSLocalizedDescriptionKey: "Failed to decode base64 challenge from response."]
1086+
)
1087+
}
1088+
guard let userIdInData = Data(base64Encoded: response.userID) else {
1089+
throw NSError(
1090+
domain: AuthErrorDomain,
1091+
code: AuthInternalErrorCode.RPCResponseDecodingError.rawValue,
1092+
userInfo: [NSLocalizedDescriptionKey: "Failed to decode base64 userId from response."]
1093+
)
1094+
}
1095+
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
1096+
relyingPartyIdentifier: response.rpID
1097+
)
1098+
let registrationRequest = provider.createCredentialRegistrationRequest(
1099+
challenge: challengeInData,
1100+
name: passkeyName ?? "Unnamed account (Apple)",
1101+
userID: userIdInData
1102+
)
1103+
return registrationRequest
1104+
}
1105+
#endif
1106+
10501107
// MARK: Internal implementations below
10511108

10521109
func rawAccessToken() -> String {

0 commit comments

Comments
 (0)