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
Add Keybase integration #10297
Changes from 36 commits
b664211
73b9867
be92df5
391a714
5554b60
fd670b5
09886f5
90aa86b
bed4005
b7af7d9
82e7a46
cc09a00
3c42d2d
9451e8f
3c7a42f
4b341b1
5781466
c8dc078
729f6ac
6ce8f39
29c390b
9476b28
0bf7c56
2a41def
cda6394
dc2ed71
cf214d3
a93123f
0b3a105
dcf94d6
6f6e797
4c37390
a93c2bd
1119574
5aff20c
728e511
de387b2
b343e6a
2326745
bbd36e0
baf695a
ec56376
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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!) | ||
end | ||
|
||
def new | ||
@proof = current_account.identity_proofs.new( | ||
token: params[:token], | ||
provider: params[:provider], | ||
provider_username: params[:provider_username] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah no, the difference to resource_params is that these are like |
||
) | ||
|
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this could benefit from being an |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should ActiveRecord validation really have side-effects? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose you could argue that |
||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
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.
@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
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
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.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.
It does
worker_class.new.perform(@proof)
which is synchronousThere 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 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.