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

Add keybase integration #661

Merged
merged 15 commits into from
Jun 12, 2019
Merged

Add keybase integration #661

merged 15 commits into from
Jun 12, 2019

Conversation

devsnek
Copy link
Contributor

@devsnek devsnek commented Apr 15, 2019

This allows users to verify keybase with their account.

Refs: https://keybase.io/docs/proof_integration_guide

This is currently untested as I lack the infra needed to test lobster, and some SVG assets of the logo are needed for keybase.json. If anyone can help with these that would be nice. After this is deployed, an admin of the site will need to submit keybase.json to keybase.

@devsnek devsnek force-pushed the feature/keybase branch 2 times, most recently from 0fb743f to 7b706b5 Compare April 15, 2019 16:01
This allows users to verify keybase with their account.

Refs: https://keybase.io/docs/proof_integration_guide
@pushcx
Copy link
Member

pushcx commented Apr 17, 2019

I've posted a story to discuss this as I'm not sure what the value is here and, more generally, when an auth integration is worth it.

@malgorithms
Copy link

hi, @pushcx - someone asked me to comment here; I posted on the story instead of GitHub, but here's a link to it: https://lobste.rs/s/24k19n/should_we_support_authing_keybase#c_8s4fex

I don't know if it's better to discuss the PR here or there, but people from Keybase can be around for technical discussion if here. We don't have a particular attachment to this PR but would of course be quite happy to see lobste.rs as one of our first non-mastodon and non-commercial integrations.

@pushcx pushcx force-pushed the master branch 4 times, most recently from acf676b to 316d894 Compare April 24, 2019 03:37
@pushcx
Copy link
Member

pushcx commented Apr 24, 2019

That discussion has wound down and we got a bunch of folks who'd like to see it, so let's give a shot.(And, like the other integrations, someone besides me has done all the hard work.) Let me take a look at the spec and review the code here...

@@ -256,6 +256,25 @@ def twitter_disconnect
return redirect_to "/settings"
end

def keybase_auth
if !params[:kb_username].present? || !params[:kb_signature].present? || !params[:kb_ua].present? || (!params[:username].present? || !params[:username] == @user.username)
Copy link
Member

Choose a reason for hiding this comment

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

Can Rails's strong parameters clean this up? You may need to make like a non-db-backed model object named KeybaseAuth or something for the params to live on... and that would have the nice effect of pulling much of this complexity out of the controller.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll be honest I have no idea what any of this means, can you elaborate?

Copy link
Member

Choose a reason for hiding this comment

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

It's the Rails security feature for saying "I expect this form to give all these fields, and only these fields": https://guides.rubyonrails.org/action_controller_overview.html#strong-parameters

Copy link

Choose a reason for hiding this comment

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

for any users with multiple lobsters accounts, it might be nice to have the !params[:username] == @user.username error case handled a little differently. i.e. tell them that they're logged in as the wrong user. otherwise it could be pretty confusing what's actually going wrong.

Copy link
Member

Choose a reason for hiding this comment

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

Users are not permitted to have more than one account, it's considered sockpuppeting and leads to banning all but one account, at the least.

I don't try to actively seek it out because I'm ok with (hypothetical scenario here) an Apple employee who wants to gripe about how they don't like Swift without getting in trouble at work, but it's a regular problem with self-promoters who vote up their own links and comments.

@user.keybase_signatures = [{ kb_username: kb_username, sig_hash: kb_signature }]
return redirect_to Keybase.success_url(kb_username, kb_signature, kb_ua, @user.username)
else
flash[:error] = "Failed to connect your account to Keybase, invalid proof signature was provided."
Copy link
Member

Choose a reason for hiding this comment

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

What's next step can we give the user here? Should they be contacting Keybase about their bug, or mailing the site admin about our own bug?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's more of a 4xx than a 5xx, since invalid auth data was provided

Copy link
Member

Choose a reason for hiding this comment

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

Can the user fix it? If they can, let's tell them what to change. If it's something in our json config that's borked, tell them to email the admin.

Copy link

Choose a reason for hiding this comment

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

it's likely that the only situation where the user can do anything is if they're logged in as a different user than the one they're trying to prove. other than that, it's (1) a problem in the request being made by lobsters (maybe catch this with logging and general observability), (2) an error in keybase (not much we can do about it here i'm sorry to say), (3) the user is accidentally trying to prove something that will never work (maybe a notification that they should try again from keybase), (4) the user is purposefully trying to prove something that will never work (fuck 'em).

@@ -67,6 +67,7 @@ class User < ApplicationRecord
s.string :twitter_oauth_token
s.string :twitter_oauth_token_secret
s.string :twitter_username
s.array :keybase_signatures
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
s.array :keybase_signatures
s.string :keybase_signatures, array: true, default: [], null: false

I'm not super-familiar with typedstore so it needs a doublecheck, but I'd prefer to both prohibit nil and be explicit about the type of the elements in the array.

Copy link
Member

Choose a reason for hiding this comment

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

From closer reading above I see this isn't an array of strings, it's an array of hashes of symbols => strings... so, yeah, please fix. And please add a test (not roundtripping Keybase, just that we can store + retrieve credentials): some sister sites use postgresql (and I daydream about it...). There's a warning in the typedstore readme about its support for arrays, so at least we could give them a failing test to point directly to the problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've never used ruby before, can you elaborate on what I need to do here?

Copy link
Member

Choose a reason for hiding this comment

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

I don't know typedstore well enough; I'd just be googling for an example. It's the gem (library) we're using to serialize these attributes to a single database column. We want to configure it with the shape of the data.

kb_ua = params[:kb_ua]

if Keybase.validate_initial(kb_username, kb_signature, @user.username)
@user.keybase_signatures = [{ kb_username: kb_username, sig_hash: kb_signature }]
Copy link
Member

Choose a reason for hiding this comment

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

Please filter these response values down to alphanumeric if possible - we print them later and I'd like to make sure there's no XSS injection potential. I'd be less paranoid on the way in if Rails would let us tag attributes as user-supplied and really freak out if they ever get to output, but that's not a feature sooo....

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've never used ruby before, how would I go about doing this?

Copy link
Member

Choose a reason for hiding this comment

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

I hadn't read the Keybase class before I wrote this, and it would be a good place to enforce the validity of this data. kb_username.gsub!(/[^0-9A-Za-z_\-]/, '') removed anything that's not an acceptable character in the kb_username variable.

raw("<a href=\"https://keybase.io/\">Keybase</a>:"),
:class => "required" %>
<span>
<% if @edit_user.keybase_signatures.present? && Keybase.verify(@edit_user.keybase_signatures[0].kb_username, @edit_user.keybase_signatures[0].sig_hash, @edit_user.username) %>
Copy link
Member

Choose a reason for hiding this comment

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

Remove this. We don't want to hit an API from a view. If this is a check that has to happen before we're sure their Keybase identity is verified, put it before storing those credentials.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this check should be done every time we provide the signature externally, where should it go?

Copy link
Member

Choose a reason for hiding this comment

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

I don't see that in their doc and I don't want to block our page load on a call to their service. If they were down or degraded we'd break. User profiles are some of our highest-traffic pages.

I hope this isn't a dealbreaker requirement from Keybase.

Copy link

Choose a reason for hiding this comment

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

tricky thing here. we can't actually check that the proof is live in keybase before saving it in lobsters. keybase needs to see the proof being served from lobsters so it can report back to you that it's live.

Copy link
Member

Choose a reason for hiding this comment

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

Keybase can't see /settings without authenticating as that user. Sounds like the PR will need to add an endpoint to serve back the proof.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

keybase currently is configured to check /u/name.json

Copy link

Choose a reason for hiding this comment

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

confirmed. keybase is hitting, e.g. lobste.rs/u/pushcx.json, and looking at the "keybase_signatures" field in there. that should all be unauthed.

<span>
<% if @edit_user.keybase_signatures.present? && Keybase.verify(@edit_user.keybase_signatures[0].kb_username, @edit_user.keybase_signatures[0].sig_hash, @edit_user.username) %>
Linked to
<strong><a href="https://keybase.io/<%= h(@edit_user.keybase_signatures[0].keybase_username)
Copy link
Member

Choose a reason for hiding this comment

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

Please show all, not one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how to iterate with this template language, can you provide some more information

Copy link
Member

Choose a reason for hiding this comment

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

<% @edit_user.keybase_signatures.each do |kb_signature| %>
  <strong><a href="https://keybase.io/<%= h(kb_signature.keybase_username)</a></strong>
<% end %>

config/routes.rb Outdated
@@ -146,6 +146,7 @@
get "/settings/twitter_auth" => "settings#twitter_auth"
get "/settings/twitter_callback" => "settings#twitter_callback"
post "/settings/twitter_disconnect" => "settings#twitter_disconnect"
get "/settings/keybase_auth" => "settings#keybase_auth"
Copy link
Member

Choose a reason for hiding this comment

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

if Keybase.enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

where do I put that?

Copy link
Member

Choose a reason for hiding this comment

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

Ruby allows trailining conditionals, you can put it at the end of the line. (If there's some syntax error hassle, just put it above the get, indent the get, and add the end.)

@@ -256,6 +256,25 @@ def twitter_disconnect
return redirect_to "/settings"
end

def keybase_auth
Copy link
Member

Choose a reason for hiding this comment

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

raise SomethingAppropriate unless Keybase.enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what is SomethingAppropriate here? I'm not familiar with errors in ruby

Copy link
Member

Choose a reason for hiding this comment

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

I totally guessed wrong about your familiarity, sorry about that. :) I was writing quickly and didn't want to stop to look it up. ActionController::RoutingError.new("Keybase not enabled")

(This is a belt-and-suspenders check that's redundant with disabling the routing, but I'm a little paranoid around security features like authorization.)

keybase.json Outdated
"brand_color": "#AC130D",

"logo": {
"svg_black": "https://lobste.rs/small-black-logo.svg",
Copy link
Member

Choose a reason for hiding this comment

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

logo

This is all we have. Font is Baskerville Semi-Bold if you want to create an svg.

keybase.json Outdated
},

"username": {
"re": "^[A-Za-z0-9][A-Za-z0-9_-]{0,24}$",
Copy link
Member

Choose a reason for hiding this comment

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

Please add a comment that this needs to match the User model's validation, and a comment there pointing back here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

json doesn't have comments, but i will add some comments to the other locations

@pushcx
Copy link
Member

pushcx commented Apr 24, 2019

Also needs to display on app/views/user/show.html.erb.

Thanks for diving in to a new environment to contribute! I'll respond as I can (coming up on the end of my pre-work coding time) so maybe @malgorithms or someone else who knows Ruby can also help.

@xgess
Copy link

xgess commented Apr 24, 2019

the way this is implemented, lobsters is immediately validating and saving whatever keybase sends it in the initial GET request to settings/keybase_auth. per the keybase docs, the user should have a chance to review that the two accounts are both theirs before submitting (e.g. a second POST route on the same controller). it could be a little jarring as a user to push a button in keybase, wait a couple seconds, and then see another keybase thing without ever actually seeing that they've gone to lobste.rs and verified something.

@xgess
Copy link

xgess commented Apr 24, 2019

i made a pr into your pr to show a few places where things weren't quite working. devsnek#1
if you hit me up on keybase @xgess i can set you up with a sockpuppet account so you can test this against a live keybase environment a little more easily.

and thanks for building this! we're excited about it :)

patch keybase integration for functionality
raw("<a href=\"https://keybase.io/\">Keybase</a>:"),
:class => "required" %>
<span>
<% if @edit_user.keybase_signatures.present? && Keybase.validate(@edit_user.keybase_signatures[0][:kb_username], @edit_user.keybase_signatures[0][:sig_hash], @edit_user.username) %>
Copy link

Choose a reason for hiding this comment

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

the comment is gone, but i agree that these requests are better done in the controller than in the view. i'd suggest setting up an instance variable in the controller (like @edit_user) for @keybase_signatures that's got everything on it. that way the view can just iterate through and show whatever it wants from it.

@xgess
Copy link

xgess commented May 3, 2019

just a little update. @devsnek and i are working on this, and we'll push a bigger update probably in the next several days.

@xgess
Copy link

xgess commented May 15, 2019

@pushcx i haven't heard from @devsnek in a bit, so i kinda just did the last couple bits. devsnek#2 is a PR into this branch that i think makes it feature complete, but i need him to merge it. if we don't hear from him, i can just open my branch as a new PR into lobsters. that way you can see it all at once.

some keybase integration updates
@devsnek
Copy link
Contributor Author

devsnek commented May 15, 2019

yeah sorry this fell off my radar a bit 😄

@xgess
Copy link

xgess commented May 15, 2019

sweet. thanks @devsnek. @pushcx i think this is ready for a re-review.

@devsnek devsnek marked this pull request as ready for review May 18, 2019 15:18
private

def check_user_matches
unless @user.username.casecmp(params[:username]).zero?
Copy link
Member

Choose a reason for hiding this comment

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

Would casecmp? match Keybase's desired behavior here? I like that it's a little clearer to use a boolean predicate than check for zero on a sorting operator, and it's nice that it supports Unicode for the non-english sister sites that have broadened the definition of User::VALID_USERNAME.

Copy link

Choose a reason for hiding this comment

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

casecmp? isn't until ruby 2.4.6 and we're on 2.3.3 :/
but i can extract this into a predicate so at least it's easier to read.

@@ -0,0 +1,45 @@
class KeybaseProofsController < ApplicationController
Copy link
Member

@pushcx pushcx May 29, 2019

Choose a reason for hiding this comment

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

I really like that you've split this out into its own controller rather than fit it into Users or Settings, this is great.

Could you bring the /.well-known endpoint into this controller as well? If we support more of those endpoints in the future it makes a lot more sense to group code by functionality than by URL. And if (knock on wood) anyone has to debug Keybase integration they'll be happy to have it all together.

Copy link

Choose a reason for hiding this comment

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

sure :)

end

def check_new_params
redirect_to settings_path unless [:kb_username, :kb_signature, :kb_ua, :username].all? do |k|
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
redirect_to settings_path unless [:kb_username, :kb_signature, :kb_ua, :username].all? do |k|
redirect_to settings_path unless [:kb_username, :kb_signature, :kb_ua, :username].all?(&:present?)

Could you explain this logic? Why do we push someone back to /settings without an error message if all the params are present? Should this be sending them back if any are missing instead of if all of them are present?

Copy link

Choose a reason for hiding this comment

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

this redirects to the main settings unless they're all there. also, this should never happen in the normal user flow, since these parameters will all be populated by keybase (there aren't links to the /new path anywhere because you need to start the proof flow from keybase). so you'd only hit this redirect if you were manually messing with the url trying to do something weird.

@brand_color = "#AC130D"
@description = "Computing-focused community centered around link aggregation and discussion"
@contacts = ["admin@#{Keybase.DOMAIN}"]
# rubocop:disable Style/FormatStringToken
Copy link
Member

Choose a reason for hiding this comment

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

Huh, why did this cop fire? I've read its docs and I don't even get why it's firing in this case. I see you do %{} instead of #{} for some values, but I don't see a call to format or % in this code or view, so I'm puzzled why you do that?

Copy link

Choose a reason for hiding this comment

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

sure. %{} is what keybase is expecting, so we need to do that. and here's the error rubocop gives...

C: Style/FormatStringToken: Prefer annotated tokens (like %<foo>s) over template tokens (like %{foo}).

@@ -433,6 +438,14 @@ def is_new?
Time.current - self.created_at <= NEW_USER_DAYS.days
end

def add_or_update_keybase_proof(kb_username, kb_signature)
self.keybase_signatures ||= []
self.keybase_signatures = self.keybase_signatures.select do |kbsig|
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
self.keybase_signatures = self.keybase_signatures.select do |kbsig|
self.keybase_signatures.reject! {|kbsig| kbsig['kb_username'] == kb_username }

One-liner, also makes it explicit that we're throwing away any existing sig with this username.

Copy link

Choose a reason for hiding this comment

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

agreed. this is strictly better.

</div>
<% end %>

<% if Keybase.enabled? %>
<% for_self = (@showing_user == @user) %>
Copy link
Member

Choose a reason for hiding this comment

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

...oh, of course. :) Nice bit of abstraction to be explicit about what repeated checks are doing.

Copy link

Choose a reason for hiding this comment

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

thanks :)

extras/sponge.rb Outdated
@@ -141,7 +141,7 @@ def fetch(url, method = :get, fields = nil, raw_post_data = nil, headers = {}, l
end

if BAD_NETS.select {|n| IPAddr.new(n).include?(ip) }.any?
raise BadIPsError.new("refusing to talk to IP #{ip}")
raise BadIPsError.new("refusing to talk to IP #{ip}") if Rails.env.production?
Copy link
Member

Choose a reason for hiding this comment

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

What triggered this in dev/testing? I'm leery of relaxing this protection in such a general way.

Copy link

Choose a reason for hiding this comment

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

this blocks requests to localhost, so i couldn't test an end-to-end proof locally (with a locally running keybase stack) without suppressing the check in dev. i understand your concern. i'll remove it and replace with a little comment somewhere else.

@pushcx
Copy link
Member

pushcx commented May 29, 2019

This is looking great! I have a couple small tweaks and then a few more questions to make sure I understand what's at work. It's all small enough that I'll be hope to squeeze in a merge some lunch/evening rather than wait until my next Wednesday morning to re-review.

xgess added 2 commits May 31, 2019 12:02
* casecmp.zero? => case_insensitive_match?

* move keybase config to the keybase proofs controller

* keybase_proofs select => reject

* remove previous restriction of BadIPsError and replace with descriptive comment

* clean up keybase link creation with a helper
* remove a test i just added that wasn't actually worth it

* rubocop
@xgess
Copy link

xgess commented Jun 5, 2019

hey @pushcx, i just wanted to ping you here and maybe keep the momentum going. thanks!

@@ -141,7 +141,9 @@ def fetch(url, method = :get, fields = nil, raw_post_data = nil, headers = {}, l
end

if BAD_NETS.select {|n| IPAddr.new(n).include?(ip) }.any?
raise BadIPsError.new("refusing to talk to IP #{ip}") if Rails.env.production?
# This blocks all requests to localhost, so you might need to comment
Copy link
Member

Choose a reason for hiding this comment

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

Great comment, makes it instantly apparent what's going on.

@pushcx
Copy link
Member

pushcx commented Jun 12, 2019

Hey, I was out of town last week for work and didn't have much outside dev time. I'm merging and deploying this now.

Thanks for all this work implementing and revising this PR, and also to @xgess for joining in.

@pushcx pushcx merged commit 4fc391e into lobsters:master Jun 12, 2019
@devsnek devsnek deleted the feature/keybase branch June 12, 2019 13:56
@pushcx
Copy link
Member

pushcx commented Jun 12, 2019

OK. Their validator reports our config is good, so this is waiting on Keybase listing the integration. I've emailed them according to the install guide.

@arp242
Copy link
Contributor

arp242 commented Jun 13, 2019

Seems active and working 👍 I linked my profile: https://lobste.rs/u/arp242

@pushcx
Copy link
Member

pushcx commented Nov 2, 2021

Leaving a comment here because people are presumably still cc'd - is there value to keeping this integration? We have a couple open bugs (#807, #796) but I'm not sure they're worth dev time given how few users have mentioned them.

Keybase's value was supposed to be integrating with many sites to collect identity info, but since Zoom acquired them their public repos have had very little activity. There's not much benefit to Keybase if they don't add new sites and maintain integrations. We can wait until Zoom shuts it down entirely, but if that happens informally when something breaks in a way that exceeds their maintenance budget, we're suddenly given a high-priority bug report as our integration starts throwing 500s or timing out. Maybe it's better to bite the bullet and remove it now.

Looking at the database, 505 of our users have authenticated with Keybase, which is significantly higher than I expected. I'll send a note to the couple dozen that have been active on Lobsters recently to ask what value they get out of our integration and post back here, probably later this month.

pushcx pushed a commit that referenced this pull request Jan 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants