-
-
Notifications
You must be signed in to change notification settings - Fork 928
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
Use Privacy Access Tokens, CAPTCHA, and Rack to rate limit sign ins and sign ups #3519
base: master
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3519 +/- ##
==========================================
- Coverage 98.69% 98.65% -0.05%
==========================================
Files 198 204 +6
Lines 4902 5120 +218
==========================================
+ Hits 4838 5051 +213
- Misses 64 69 +5
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
@@ -209,7 +210,11 @@ | |||
end | |||
end | |||
|
|||
get '/sign_in' => 'clearance/sessions#new', as: 'sign_in' | |||
resource :user, only: [] do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is resource only: []
any different from using namespace
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question, I was following the pattern that currently exists in routes
for session and I think in this case it makes more sense. /user
isn't a namespace but a singular resource. Namespacing it would mean that by convention we would expect the controller to exist within a user module.
From the Rails docs
2.5 Singular Resources
Sometimes, you have a resource that clients always look up without referencing an ID. ..
2.6 Controller Namespaces and Routing
You may wish to organize groups of controllers under a namespace. Most commonly, you might group a number of administrative controllers under an Admin:: namespace, and place these controllers under the app/controllers/admin directory. You can route to such a group by using a namespace block:
# uint8_t token_key_id[32]; | ||
# uint8_t authenticator[Nk]; | ||
# } Token; | ||
TOKEN_TYPE_LENGTH = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be 2 since it's a uint16_t
as opposed to a uint8?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because we're checking the length of unpacked data and we're unpacking this via the uint16 directive (n
)
a41be42
to
9527095
Compare
eca0839
to
3f6cce8
Compare
Fastly says we are in their beta access group now, and they are targeting end of Q2 for the beta issuer. In the meantime, I would be okay to ship this with just captchas now, and add privacy pass support once there's a production issuer. (Fastly says the demo issuer that we built this to work with is not for production use, so we shouldn't ship anything that depends on that.) |
3f6cce8
to
4f44b20
Compare
Based on @indirect's most recent feedback, I went ahead and feature flagged the privacy pass implementation using LaunchDarkly (someone with access to that will need to configure the variation) |
|
||
def privacy_pass_enabled? | ||
ld_context = LaunchDarkly::LDContext.with_key(self.class.name) | ||
Rails.configuration.launch_darkly_client.variation("privacy_pass.enabled", ld_context, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's call the flag "gemcutter.privacy_pass.enabled"
. should we use the current controller & action as the key, so we can test by only turning it on for a single action, rather than every action?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the name.
I think using the current controller would be good. My only concern about using the action is that for every place we want it (sign up or sign in) we actually need it enabled in 2 actions. The new
action presents the challenge and the create
action handles redeeming the token.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fair enough! I just think we'll need a kind
on the LDContext of say controller
so it doesn't look like a user
95e9733
to
3293a22
Compare
app/views/sessions/captcha.html.erb
Outdated
<div class="mfa__container"> | ||
<div class="mfa__option"> | ||
<%= form_tag captcha_create_session_path, method: :post, class: "mfa-form" do %> | ||
<div class="h-captcha" data-sitekey="<%= ENV.fetch("HCAPTCHA_SITE_KEY", "10000000-ffff-ffff-ffff-000000000001") %>"></div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move HCAPTCHA_SITE_KEY
to be ingested in config/secrets.yml
from ENV (and specify this default for dev/testing environment if useful), introduce helper like hcaptcha_site_key
to read secrets and use it in templates?
lib/hcaptcha_verifier.rb
Outdated
sitekey: ENV.fetch("HCAPTCHA_SITE_KEY", "10000000-ffff-ffff-ffff-000000000001") | ||
} | ||
|
||
response = RestClient.post VERIFY_ENDPOINT, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RestClient was replaced by Faraday recently at #3602
@@ -172,6 +194,16 @@ def setup_mfa_authentication | |||
session[:mfa_user] = @user.id | |||
end | |||
|
|||
def setup_captcha_verification | |||
create_catpcha_user(id: @user.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per my local testing, captcha is also created when trying to login with unknown email, that means @user
is nil in here and it crashes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same seems applied when using wrong password for existing account. find_user
returns nil -> @user
is nil.
I did some testing and left some initial comments. Regarding Here's a screenshot from testing of captcha. The red text is just notice of using testing environment. It looks good to me. |
6b3c3b4
to
e7b8f9f
Compare
|
I got an email from GitGuardian about an exposed generic password. But I think that's ok b/c Hcaptcha publicly publishes this site key and secret for development testing |
lib/privacy_pass_tokenizer.rb
Outdated
# fastly demo fallback | ||
# https://www.fastly.com/blog/private-access-tokens-stepping-into-the-privacy-respecting-captcha-less#demo-configuration | ||
# rubocop:disable Layout/LineLength | ||
ENV.fetch("PRIVACY_PASS_ISSUER_PUBLIC_KEY", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to move this also to rubygems.yml/secrets.yml?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can. It's not really a secret but no harm in adding it there.
|
||
def valid_privacy_pass_redemption? | ||
return false unless privacy_pass_enabled? | ||
return session[:redeemed_privacy_pass] unless session[:redeemed_privacy_pass].nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found out this a little hard to read during testing. Would it be possible to keep the condition "positive"?
return session[:redeemed_privacy_pass] if session.key?(:redeemed_privacy_pass)
@mercedesb any more hints for local Privacy pass testing? I tried with Privacy pass extension, but I couldn't get it to work 🤔. |
Privacy Pass is currently supported natively only in iOS 16+, iPad 16+, macOS Ventura (13+). Through my testing, I had to use the Safari browser. But if you're running a supported OS with Safari, that should be all you need for testing. If things are working as expected, you won't see the 401 response with the token challenge in the browser's dev tools' Network tab. You'll see a 200 with an Authorization header from the client which means the challenge was handled successfully. But you can step through the code to see the entire flow. |
657c299
to
2619a61
Compare
That extension supports also hCaptcha and seems active on hCaptcha rubygems.org page, but I couldn't get it to work as well. I don't have any Apple product around to test this. |
hcaptcha is a different issuer and won't work with Fastly tokens. We are using the Fastly issuer.
That's a bummer. Given that Apple seems to be the only native support for PATs at the moment, I do think it important that it's tested there. |
elsif @user.save | ||
handle_user_after_save | ||
else | ||
render template: "users/new" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand it well, this is tricky to test. valid?
is called few lines before, but this save
can still fail. In theory codecov could be fixed by using save!
and rescue_from
, but I'm not sure if that's worth it. 🤔
We can ask someone for help with testing on staging. Other than that part I can't test, this looks good to me. 💪 |
should_verify(keys, SIGN_UP_CAPTCHA_THRESHOLD) | ||
end | ||
|
||
def call(client_response, remote_ip) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: I'm personally not big fan of call
methods if not used as a proc. Would it make sense to rename this to something like verified?
. Similar change could be done in PrivacyPassRedeemer.
f.request :url_encoded | ||
f.response :json | ||
f.response :logger, logger, headers: false, errors: true | ||
f.response :raise_error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this codecov complain related to stubs being used in tests instead of using test adapter? 🤔
Resolves #3518
What was the end-user or developer problem that led to this PR?
Please see #3518
Rubygems.org wants to reduce (and prevent if possible) bot activity around signup and login. Captchas are a common way to reduce activity, but can also be annoying and repetitive. We’d like to use Privacy Pass to get the same security with a smoother user experience then Captcha, although it doesn’t completely replace Captchas.
What is your fix for the problem, implemented in this PR?
Following the product design in the linked issue, I've implemented privacy pass token challenges and redemption, hcaptcha verification, and stricter rate limiting around sign in and sign up.
To test
Privacy pass is currently supported on iOS and MacOS Ventura. I tested this with Safari in MacOS Ventura 13.2.1 (it doesn't work in any non-Apple browser yet as far as I can tell).
Deployment next steps
Once we hear back from Fastly about getting added to their beta issuer we can take the following steps
PRIVACY_PASS_ISSUER_PUBLIC_KEY
andPRIVACY_PASS_ISSUER_NAME
vars fromconfig/deploy/unicorn.yaml.erb
config/deploy/staging/secrets.ejson
andconfig/deploy/production/secrets.ejson
Other helpful info
Apple has the best info currently about how to set up your web server to serve privacy pass challenges and redeem privacy pass tokens.
The various docs around the IETF RFC are also helpful
hcaptcha was straightforward to get set up and working. Their docs are pretty good.
Make sure the following tasks are checked