Skip to content
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

Passkeys: Errors, Alerts, UI Feedback #1610

Merged
merged 27 commits into from
Jul 12, 2024

Conversation

charliescheer
Copy link
Contributor

Fix

The first implementation of passkeys works to create passkeys and allow for users to authenticate with them, but it lacks some UI refinement. There is no indication of progress happening, the UI remains unlocked, and there is no indication if the process is succeeding or failing.... Not a great user experience.

So to improve that I have added spinners, ui locking, errors, and alerts so that users have a better idea of what is going on when they use passkeys.

Test

Setup:

  1. Go to the passkey DB and delete any existing passkeys you have (ping me if you need a link to the db)
  2. On a Mac linked to the same iCloud as your testing device, go to Apple Settings > Passwords and search for any existing passkeys for simplenote at the testing url and delete them

Registration:

  1. Log into simplenote using email/password and go to the settings view
  2. Put your device into airplane mode and then try to register a passkey by tapping on Add Passkey Authentication
  3. Confirm that you see a spinner for a moment and then an alert appears that you failed to register
  4. Go out of airplane mode, Tap on Add Passkey Authentication again. Enter your email and confirm the spinner appears
  5. When the FaceID modal pops up, dismiss it without confirming. Confirm that an alert pops up saying you failed to register. Also confirm that the UI is unlocked
  6. Tap on Add Passkey Authentication again and this time let it scan your face. You should succeed this time. Confirm that you get a Success alert

Login:

  1. Logout of the account you just registered a passkey for
  2. On the login screen go to Login > Login with Passkeys and enter your email address
  3. Put the device in airplane mode and then tap on the login with passkey button. Confirm you see an error alert
  4. Take the device out of airplane mode, tap on the login button again. When the faceID ui appears dismiss it. Confirm you see an alert
  5. Tap on the login button again and this time go through the whole process. Confirm you see a login spinner on the button, the ui is locked, and then when it succeeds confirm you are logged into your account

Review

(Required) Add instructions for reviewers. For example:

Only one developer and one designer are required to review these changes, but anyone can perform the review.

Release

(Required) Add a concise statement to RELEASE-NOTES.txt if the changes should be included in release notes. Include details about updating the notes in this section. For example:

RELEASE-NOTES.txt was updated in d3adb3ef with:

Added markdown support

If the changes should not be included in release notes, add a statement to this section. For example:

These changes do not require release notes.

@charliescheer charliescheer added the [feature] login Anything relating to login. label Jul 3, 2024
@charliescheer charliescheer added this to the Future milestone Jul 3, 2024
@charliescheer charliescheer self-assigned this Jul 3, 2024
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Jul 3, 2024

You can test the changes in simplenote-ios from this Pull Request by:

  • Clicking here or scanning the QR code below to access App Center
  • Then installing the build number pr1610-be6eb44-0190a8da-b916-4d45-aaa6-d1789448360d on your iPhone

If you need access to App Center, please ask a maintainer to add you.

Copy link
Contributor

@jleandroperez jleandroperez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@charliescheer sending you a few Threading-Y notes!!

Also: Registration fails 10 out of 10 for me. I've nuked the Weauthn rows I had, am I missing something?

Thank youuuuu

Copy link
Contributor

@jleandroperez jleandroperez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test scenarios are green
✅ Code looks good!

Added a few notes on an approach that would allow us encapsulate all of the complexity (so that it doesn't leak into the VC).

Other than that, :shipit:

authController.presentationContextProvider = presentationContext
authController.performRequests()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I had in mind when we were discussing API approach!

Rough edges, untested, not finished., and has room for probably splitting a bit.

The idea is that all of the business logic doesn't leak into the ViewController, from the outside, you'd expect a simple and straightforward API, with clearly defined spots where it begins and ends.

class PasskeyAuthControllerDelegate: NSObject, ASAuthorizationControllerDelegate {

    var onCompletion: ((Result<ASAuthorization, Error>) -> Void)?
    
    public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) {
        onCompletion?(.failure(error))
    }

    public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        onCompletion?(.success(authorization))
    }
}


class PasskeyAuthenticator: NSObject {
    let passkeyRemote: PasskeyRemote
    let internalAuthControllerDelegate: PasskeyAuthControllerDelegate

    init(passkeyRemote: PasskeyRemote = PasskeyRemote(), authControllerDelegate: PasskeyAuthControllerDelegate = .init()) {
        self.passkeyRemote = passkeyRemote
        self.internalAuthControllerDelegate = authControllerDelegate
    }

    func attemptPasskeyAuth(challenge: PasskeyAuthChallenge?, in presentationContext: PresentationContext, delegate: ASAuthorizationControllerDelegate) async throws {
        guard let challenge else {
            throw PasskeyError.couldNotFetchAuthChallenge
        }

        let challengeData = try Data.decodeUrlSafeBase64(challenge.challenge)
        let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: challenge.relayingParty)
        let request = provider.createCredentialAssertionRequest(challenge: challengeData)

        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = internalAuthControllerDelegate
        controller.presentationContextProvider = presentationContext
        
        let authorization = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<ASAuthorization, any Error>) in
            internalAuthControllerDelegate.onCompletion = { result in
                continuation.resume(with: result)
            }
            
            controller.performRequests()
        }

/// Potentially split
/// Pending actual Simperium Auth
    }

}

controller.simperiumService.authenticate(withUsername: verifyResponse.username, token: verifyResponse.accessToken)
unlockInterface()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally the ViewController doesn't need to deal with json encoding / decoding. More on that below!!

Copy link
Contributor

@jleandroperez jleandroperez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending you few comments Charlie!!

There's a bit of responsibility, yet, that can be transferred from the VC into the Passkey* classes. Plus there's a bit of an overlap in between PasskeyAuthControllerDelegate and PasskeyAuthenticator, both of them own a PasskeyRemote instance.

LMK if anything!!

Simplenote/Classes/SPAuthViewController.swift Show resolved Hide resolved
Simplenote/PasskeyRegistrator.swift Outdated Show resolved Hide resolved
Simplenote/PasskeyAuthenticator.swift Outdated Show resolved Hide resolved
Simplenote/PasskeyAuthenticator.swift Outdated Show resolved Hide resolved
}
}

controller.performRequests()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this API okay with BG invocation? (we're not necessarily in the main Task)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be I believe. The finishing and ui changes happen on the main thread, the auth can happen in the background I think

Simplenote/Classes/SPAuthViewController.swift Show resolved Hide resolved
Copy link
Contributor

@jleandroperez jleandroperez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work Charlie!! Found a retain cycle, other than that, :shipit: when ready!!

Thank youuu!!

Simplenote/PasskeyAuthenticator.swift Outdated Show resolved Hide resolved
Simplenote/PasskeyRegistrator.swift Show resolved Hide resolved
Simplenote/PasskeyAuthenticator.swift Show resolved Hide resolved
@charliescheer charliescheer merged commit efc1af3 into feature/passkeys Jul 12, 2024
8 of 10 checks passed
@charliescheer charliescheer deleted the charlie/passkey-auth-errors-ui-mk2 branch July 12, 2024 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[feature] login Anything relating to login.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants