Skip to content

adding ui tests for passkey #15209

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
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 @@ -33,14 +33,6 @@ extension User: DataSourceProvidable {
return Section(headerDescription: "Info", items: items)
}

private var metaDataSection: Section {
let metadataRows = [
Item(title: metadata.lastSignInDate?.description, detailTitle: "Last Sign-in Date"),
Item(title: metadata.creationDate?.description, detailTitle: "Creation Date"),
]
return Section(headerDescription: "Firebase Metadata", items: metadataRows)
}

private var passkeysSection: Section {
let passkeys = enrolledPasskeys ?? []
guard !passkeys.isEmpty else {
Expand All @@ -55,6 +47,14 @@ extension User: DataSourceProvidable {
return Section(headerDescription: "Passkeys", items: items)
}

private var metaDataSection: Section {
let metadataRows = [
Item(title: metadata.lastSignInDate?.description, detailTitle: "Last Sign-in Date"),
Item(title: metadata.creationDate?.description, detailTitle: "Creation Date"),
]
return Section(headerDescription: "Firebase Metadata", items: metadataRows)
}

private var otherSection: Section {
let otherRows = [Item(title: isAnonymous ? "Yes" : "No", detailTitle: "Is User Anonymous?"),
Item(title: isEmailVerified ? "Yes" : "No", detailTitle: "Is Email Verified?")]
Expand All @@ -76,7 +76,7 @@ extension User: DataSourceProvidable {
}

var sections: [Section] {
[infoSection, metaDataSection, passkeysSection, otherSection, actionSection]
[infoSection, passkeysSection, metaDataSection, otherSection, actionSection]
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -962,10 +962,7 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
showAlert(for: "Please sign in first.")
return
}
guard let passkeyName = await showTextInputPrompt(with: "Passkey name") else {
print("Passkey enrollment cancelled: no name entered.")
return
}
let passkeyName = await showTextInputPrompt(with: "Passkey name")
guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
showAlert(for: "Not Supported", message: "This OS version does not support passkeys.")
return
Expand All @@ -989,16 +986,15 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate {
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)")
}
do {
let request = try await AppManager.shared.auth().startPasskeySignIn()
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
print("Started passkey sign in (challenge created).")
} catch {
print("Passkey sign-in failed with error: \(error)")
}
}

Expand Down Expand Up @@ -1144,7 +1140,23 @@ extension AuthViewController: ASAuthorizationControllerDelegate,
}
return
}

if let assertion = authorization
.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
Task { @MainActor [weak self] in
guard let self else { return }
do {
let _ = try await AppManager.shared.auth()
.finalizePasskeySignIn(withPlatformCredential: assertion)
self.showAlert(for: "Passkey Sign-In", message: "Succeeded")
print("Passkey sign-in succeeded.")
self.transitionToUserViewController()
} catch {
self.showAlert(for: "Passkey Sign-In failed", message: error.localizedDescription)
print("Finalize passkey sign-in failed: \(error.localizedDescription)")
}
}
return
}
guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
else {
print("Unable to retrieve AppleIDCredential")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,37 @@ class AuthenticationExampleUITests: XCTestCase {
)
}

func testPasskeyList() {
signOut()
let testEmail = "[email protected]"
let testPassword = "sample.ios.auth"
let testPasskeyName = "sampleiosauth"
app.staticTexts["Email & Password Login"].tap()
app.textFields["Email"].tap()
app.textFields["Email"].typeText(testEmail)
app.textFields["Password"].tap()
app.textFields["Password"].typeText(testPassword)
app.buttons["Login"].tap()
wait(forElement: app.navigationBars["User"], timeout: 5.0)
XCTAssertTrue(app.navigationBars["User"].exists)
XCTAssertTrue(
app.staticTexts[testEmail].exists,
"The user should be signed in and the email field should display their email."
)
let userTable = app.tables.firstMatch
XCTAssertTrue(userTable.waitForExistence(timeout: 5.0), "User detail list should exist")
let passkeyLabel = userTable.staticTexts[testPasskeyName]
if !passkeyLabel.exists {
for _ in 0 ..< 5 where !passkeyLabel.exists {
userTable.swipeUp()
}
}
XCTAssertTrue(
passkeyLabel.waitForExistence(timeout: 5.0),
"Passkey named '\(testPasskeyName)' should be visible in the Passkeys section."
)
}

// MARK: - Private Helpers

private func signOut() {
Expand Down
Loading
Loading