rubygems / rubygems.org

The Ruby community's gem hosting service.
https://rubygems.org
MIT License
2.31k stars 916 forks source link

Add WebAuthn CLI fallback for Safari browsers #3826

Closed jenshenny closed 1 year ago

jenshenny commented 1 year ago

Is your feature request related to a problem?

The current WebAuthn implementation does not support Safari browsers. This is because Safari blocks any content from localhost servers.

Safari issue trackers: https://bugs.webkit.org/show_bug.cgi?id=186039 https://bugs.webkit.org/show_bug.cgi?id=171934

Other tools

Other CLI tools support interactions with Safari (npm, gh, heroku). Upon a deeper look, they have a similar polling flow.

  1. The send a POST request to a login endpoint to receive an unique link and optionally a unique identifier to avoid phishing (user code)

  2. They poll a certain endpoint with the unique identifier and retrieve the api key/token when the user finishes authenticating on the browser.

    github Screenshot 2023-05-26 at 9 57 17 AM

npm

Screenshot 2023-05-26 at 9 56 57 AM

Potential Solutions

1. Modified Sockets

Manually redirect to localhost after webauthn verification has been completed.

Prototype 1: Via clicking the localhost link. * simplest solution

https://github.com/rubygems/rubygems.org/assets/42748004/4e2c6af3-22da-4b62-b01a-9b2cad827789

Prototype 2: Browser sending a GET request.

https://github.com/rubygems/rubygems.org/assets/42748004/0656c902-d58c-4a06-a03e-5a2faf76cf95

Branch: https://github.com/Shopify/rubygems.org/pull/263

2. Polling

As with other CLI tools, when requesting a unique url, return a code to use to poll for the otp code upon completion. This would require adding an endpoint to poll and modifying the current API endpoint to return an unique identifier for polling.

If this solution is chosen, should we only poll for Safari? Should we switch to this implementation in the long term?

3. Manual pasting

After browser verification, instead of sending the OTP via socket, display the code for the user to use.

How does the user input the manual code? Have a code prompt in the CLI if safari is their default browser, via the –otp option?

jenshenny commented 1 year ago

Currently my opinion is to see if https://github.com/Shopify/rubygems.org/pull/263 works, and if it does, proceed with that. I have an inkling that it'll hit the same localhost issue though.

Then, I would want to fallback to polling for Safari without entering a device code. It's more susceptible to phishing but it should be fine it we're using it for Safari browsers.

indirect commented 1 year ago

Thanks for investigating! That sounds good to me.

jenshenny commented 1 year ago

Currently my opinion is to see if Shopify#263 works, and if it does, proceed with that. I have an inkling that it'll hit the same localhost issue though.

Indeed, the modified sockets approach didn’t work on staging.

I would want to fallback to polling for Safari without entering a device code.

We’re intending to move forward with this approach.

We explored the option of serving a fully qualified localhost domain (FQDN) and redirecting from there to bypass Safari's protection of serving mixed content (content from non-https) similar to the strategy mentioned here. However, we also discovered that Spotify had moved away from this approach, although it’s unclear what factors lead to them changing their API. If this is your preferred option, we’d be happy to collaborate but the execution will need to be largely owned by other maintainers since it requires configuring domains.

With the polling solution, we can be self-sufficient and deliver a fix to Safari users more quickly.

indirect commented 1 year ago

I’m happy with fallback to polling 👍🏻 A fallback that depends on distributing a constantly expiring SSL cert strikes me as likely failure-prone over time.

jenshenny commented 1 year ago

Here are the PRs that adds this in https://github.com/rubygems/rubygems.org/pull/3873 https://github.com/rubygems/rubygems/pull/6774

I'll also update the guides when they are ready to go https://guides.rubygems.org/using-webauthn-mfa-in-command-line/