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 #10297

Merged
merged 42 commits into from Mar 18, 2019
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b664211
create account_identity_proofs table
xgess Dec 27, 2018
73b9867
add endpoint for keybase to check local proofs
xgess Dec 27, 2018
be92df5
add async task to update validity and liveness of proofs from keybase
xgess Dec 28, 2018
391a714
first pass keybase proof CRUD
xgess Jan 8, 2019
5554b60
second pass keybase proof creation
xgess Jan 9, 2019
fd670b5
clean up proof list and add badges
xgess Jan 10, 2019
09886f5
add avatar url to keybase api
xgess Jan 14, 2019
90aa86b
Always highlight the “Identity Proofs” navigation item when interacti…
adamjspooner Jan 16, 2019
bed4005
Update translations.
adamjspooner Jan 16, 2019
b7af7d9
Add profile URL.
adamjspooner Jan 16, 2019
82e7a46
Reorder proofs.
adamjspooner Jan 16, 2019
cc09a00
Add proofs to bio.
adamjspooner Jan 16, 2019
3c42d2d
Update settings/identity_proofs front-end.
adamjspooner Jan 16, 2019
9451e8f
Use `link_to`.
adamjspooner Jan 16, 2019
3c7a42f
Only encode query params if they exist.
adamjspooner Jan 16, 2019
4b341b1
Only show live proofs.
adamjspooner Jan 16, 2019
5781466
change valid to active in proof list and update liveness before displ…
xgess Jan 16, 2019
c8dc078
minor fixes
xgess Jan 16, 2019
729f6ac
add keybase config at well-known path
xgess Jan 24, 2019
6ce8f39
fixes for rubocop
xgess Feb 11, 2019
29c390b
extremely naive feature flagging off the identity proof UI
xgess Jan 24, 2019
9476b28
make identity proofs page resilient to potential keybase issues
xgess Feb 11, 2019
0bf7c56
normalize i18n
xgess Feb 11, 2019
2a41def
tweaks for brakeman
xgess Feb 11, 2019
cda6394
remove two unused translations
xgess Feb 11, 2019
dc2ed71
cleanup and add more localizations
xgess Feb 13, 2019
cf214d3
make keybase_contacts an admin setting
xgess Feb 15, 2019
a93123f
fix ExternalProofService my_domain
xgess Feb 18, 2019
0b3a105
use Addressable::URI in identity proofs
xgess Feb 18, 2019
dcf94d6
use active model serializer for keybase proof config
xgess Feb 18, 2019
6f6e797
more cleanup of keybase proof config
xgess Feb 18, 2019
4c37390
rename proof is_valid and is_live to proof_valid and proof_live
xgess Feb 18, 2019
a93c2bd
cleanup
xgess Feb 18, 2019
1119574
assorted tweaks for more robust communication with keybase
xgess Feb 28, 2019
5aff20c
Merge branch 'xgess/keybase-proofs' of git://github.com/xgess/mastodo…
Gargron Mar 16, 2019
728e511
Clean up
Gargron Mar 16, 2019
de387b2
Merge branch 'master' into feature-keybase
Gargron Mar 17, 2019
b343e6a
Small fixes
Gargron Mar 17, 2019
2326745
Display verified identity identically to verified links
Gargron Mar 17, 2019
bbd36e0
Clean up unused CSS
Gargron Mar 17, 2019
baf695a
Add caching for Keybase avatar URLs
Gargron Mar 17, 2019
ec56376
Remove keybase_contacts setting
Gargron Mar 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions app/controllers/admin/settings_controller.rb
Expand Up @@ -27,6 +27,7 @@ class SettingsController < BaseController
preview_sensitive_media
custom_css
profile_directory
keybase_contacts
).freeze

BOOLEAN_SETTINGS = %w(
Expand Down
30 changes: 30 additions & 0 deletions app/controllers/api/proofs_controller.rb
@@ -0,0 +1,30 @@
# frozen_string_literal: true

class Api::ProofsController < Api::BaseController
before_action :set_account
before_action :set_provider
before_action :check_account_approval
before_action :check_account_suspension

def index
render json: @account, serializer: @provider.serializer_class
end

private

def set_provider
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
end

def set_account
@account = Account.find_local!(params[:username])
end

def check_account_approval
not_found if @account.user_pending?
end

def check_account_suspension
gone if @account.suspended?
end
end
45 changes: 45 additions & 0 deletions app/controllers/settings/identity_proofs_controller.rb
@@ -0,0 +1,45 @@
# frozen_string_literal: true

class Settings::IdentityProofsController < Settings::BaseController
layout 'admin'

before_action :authenticate_user!
before_action :check_required_params, only: :new

def index
@proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc)
@proofs.each(&:refresh!)
Copy link
Member Author

Choose a reason for hiding this comment

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

@xgess Is this necessary? n synchronous HTTP requests on page load is not great. I would think the worker queued after the proof is saved would take care of checking if the proof is live

Copy link
Contributor

Choose a reason for hiding this comment

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

The refresh! do not seem to be synchronous (it spawns a worker?), but I question the need to trigger a refresh each time that page is visited.

Copy link
Member Author

Choose a reason for hiding this comment

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

It does worker_class.new.perform(@proof) which is synchronous

Copy link
Contributor

Choose a reason for hiding this comment

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

the main reason i had to do this was that there's nothing currently built on the mastodon side to recognize remotely revoked proofs. i thought it might be weird if a user revokes a proof in keybase, then days later still sees it as live in mastodon until the first refresh. we had it as a note to talk about / build something that might let keybase inform mastodon a proof is revoked, or build a rake task for mastodon to check and invalidate revoked proofs.

i'm flexible on this though. if you want to change it to be async, i think that's reasonable.

end

def new
@proof = current_account.identity_proofs.new(
token: params[:token],
provider: params[:provider],
provider_username: params[:provider_username]
Copy link
Contributor

Choose a reason for hiding this comment

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

resource_params?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah no, the difference to resource_params is that these are like ?token=??? while resource_params after form submission are nested

)

render layout: 'auth'
end

def create
@proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params)
@proof.token = resource_params[:token]

if @proof.save
redirect_to @proof.on_success_path(params[:user_agent])
else
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
redirect_to settings_identity_proofs_path
end
end

private

def check_required_params
redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :token].all? { |k| params[k].present? }
end

def resource_params
params.require(:account_identity_proof).permit(:provider, :provider_username, :token)
end
end
9 changes: 9 additions & 0 deletions app/controllers/well_known/keybase_proof_config_controller.rb
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module WellKnown
class KeybaseProofConfigController < ActionController::Base
def show
render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer
krainboltgreene marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
1 change: 1 addition & 0 deletions app/javascript/images/logo_transparent_black.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/javascript/images/proof_providers/keybase.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions app/javascript/styles/mastodon/containers.scss
Expand Up @@ -723,6 +723,20 @@

a {
color: lighten($ui-highlight-color, 8%);

&.provider_username {
position: relative;
top: -7px;
}

&.proof {
color: $darker-text-color;
display: block;
font-size: 12px;
position: relative;
text-transform: lowercase;
top: -11px;
}
}

dl:first-child .verified {
Expand Down
55 changes: 55 additions & 0 deletions app/javascript/styles/mastodon/forms.scss
Expand Up @@ -801,3 +801,58 @@ code {
}
}
}

.connection-prompt {
margin-bottom: 25px;

.fa-link {
background-color: darken($ui-base-color, 4%);
border-radius: 100%;
font-size: 24px;
padding: 10px;
}

&__column {
align-items: center;
display: flex;
flex: 1;
flex-direction: column;
flex-shrink: 1;

&-sep {
flex-grow: 0;
overflow: visible;
position: relative;
z-index: 1;
}
}

.account__avatar {
margin-bottom: 20px;
}

&__connection {
background-color: lighten($ui-base-color, 8%);
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
border-radius: 4px;
padding: 25px 10px;
position: relative;
text-align: center;

&::after {
background-color: darken($ui-base-color, 4%);
content: '';
display: block;
height: 100%;
left: 50%;
position: absolute;
width: 1px;
}
}

&__row {
align-items: center;
display: flex;
flex-direction: row;
}
}
12 changes: 12 additions & 0 deletions app/lib/proof_provider.rb
@@ -0,0 +1,12 @@
# frozen_string_literal: true
krainboltgreene marked this conversation as resolved.
Show resolved Hide resolved

module ProofProvider
SUPPORTED_PROVIDERS = %w(keybase).freeze

def self.find(identifier, proof = nil)
case identifier
when 'keybase'
ProofProvider::Keybase.new(proof)
end
end
end
59 changes: 59 additions & 0 deletions app/lib/proof_provider/keybase.rb
@@ -0,0 +1,59 @@
# frozen_string_literal: true

class ProofProvider::Keybase
BASE_URL = 'https://keybase.io'

class Error < StandardError; end

class ExpectedProofLiveError < Error; end

class UnexpectedResponseError < Error; end
krainboltgreene marked this conversation as resolved.
Show resolved Hide resolved

def initialize(proof = nil)
@proof = proof
end
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this could benefit from being an ActiveModel::Model?


def serializer_class
ProofProvider::Keybase::Serializer
end

def worker_class
ProofProvider::Keybase::Worker
end
krainboltgreene marked this conversation as resolved.
Show resolved Hide resolved

def validate!
unless @proof.token&.size == 66
@proof.errors.add(:base, I18n.t('identity_proofs.errors.keybase.invalid_token'))
krainboltgreene marked this conversation as resolved.
Show resolved Hide resolved
return
end

return if @proof.provider_username.blank?
krainboltgreene marked this conversation as resolved.
Show resolved Hide resolved

if verifier.valid?
@proof.verified = true
@proof.live = false
Copy link
Contributor

Choose a reason for hiding this comment

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

Should ActiveRecord validation really have side-effects?

Copy link
Member Author

Choose a reason for hiding this comment

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

I suppose you could argue that verified could be set before_save which would not happen if validation doesn't pass, but I am not too unhappy with how it is

else
@proof.errors.add(:base, I18n.t('identity_proofs.errors.keybase.verification_failed', kb_username: @proof.provider_username))
end
end

def refresh!
worker_class.new.perform(@proof)
rescue ProofProvider::Keybase::Error
nil
end

def on_success_path(user_agent = nil)
verifier.on_success_path(user_agent)
end

def badge
@badge ||= ProofProvider::Keybase::Badge.new(@proof.account.username, @proof.provider_username, @proof.token)
end

private

def verifier
@verifier ||= ProofProvider::Keybase::Verifier.new(@proof.account.username, @proof.provider_username, @proof.token)
end
end
46 changes: 46 additions & 0 deletions app/lib/proof_provider/keybase/badge.rb
@@ -0,0 +1,46 @@
# frozen_string_literal: true

class ProofProvider::Keybase::Badge
include RoutingHelper

def initialize(local_username, provider_username, token)
@local_username = local_username
@provider_username = provider_username
@token = token
end

def proof_url
"#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}/sigchain\##{@token}"
end

def profile_url
"#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}"
end

def icon_url
"#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}/proof_badge/#{@token}?username=#{@local_username}&domain=#{domain}"
end

def avatar_url
request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/user/pic_url.json", params: { username: @provider_username })

url = request.perform do |res|
json = Oj.load(res.body_with_limit, mode: :strict)
json['pic_url'] if json.is_a?(Hash)
end

url || default_avatar_url
rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError
default_avatar_url
end

private

def default_avatar_url
asset_pack_path('media/images/proof_providers/keybase.png')
end

def domain
Rails.configuration.x.local_domain
end
end
69 changes: 69 additions & 0 deletions app/lib/proof_provider/keybase/config_serializer.rb
@@ -0,0 +1,69 @@
# frozen_string_literal: true

class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer
include RoutingHelper

attributes :version, :domain, :display_name, :username,
:brand_color, :logo, :description, :prefill_url,
:profile_url, :check_url, :check_path, :avatar_path,
:contact

def version
1
end

def domain
Rails.configuration.x.local_domain
end

def display_name
Setting.site_title
end

def logo
{ svg_black: full_asset_url(asset_pack_path('media/images/logo_transparent_black.svg')), svg_full: full_asset_url(asset_pack_path('media/images/logo.svg')) }
end

def brand_color
'#282c37'
end

def description
Setting.site_short_description.presence || Setting.site_description.presence || I18n.t('about.about_mastodon_html')
end

def username
{ min: 1, max: 30, re: Account::USERNAME_RE.inspect }
end

def prefill_url
params = {
provider: 'keybase',
token: '%{sig_hash}',
provider_username: '%{kb_username}',
user_agent: '%{kb_ua}',
}
Gargron marked this conversation as resolved.
Show resolved Hide resolved

CGI.unescape(new_settings_identity_proof_url(params))
end

def profile_url
CGI.unescape(short_account_url('%{username}')) # rubocop:disable Style/FormatStringToken
end

def check_url
CGI.unescape(api_proofs_url(username: '%{username}', provider: 'keybase'))
end

def check_path
['signatures']
end

def avatar_path
['avatar']
end

def contact
Setting.keybase_contacts.split(',').map(&:strip)
Gargron marked this conversation as resolved.
Show resolved Hide resolved
end
end
25 changes: 25 additions & 0 deletions app/lib/proof_provider/keybase/serializer.rb
@@ -0,0 +1,25 @@
# frozen_string_literal: true

class ProofProvider::Keybase::Serializer < ActiveModel::Serializer
include RoutingHelper

attribute :avatar

has_many :identity_proofs, key: :signatures

def avatar
full_asset_url(object.avatar_original_url)
end

class AccountIdentityProofSerializer < ActiveModel::Serializer
attributes :sig_hash, :kb_username

def sig_hash
object.token
end

def kb_username
object.provider_username
end
end
end