From 45b9404ec107d805c77508334100b20193a0c78f Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 16 Jul 2023 07:09:15 -0600 Subject: [PATCH] Add Sign-in with Mastodon --- app/Models/RemoteAuth.php | 19 ++ app/Models/RemoteAuthInstance.php | 13 + app/Services/Account/RemoteAuthService.php | 183 ++++++++++++ app/User.php | 8 +- config/remote-auth.php | 56 ++++ ...07_07_025757_create_remote_auths_table.php | 38 +++ ...427_create_remote_auth_instances_table.php | 37 +++ .../remote-auth/GettingStartedComponent.vue | 262 ++++++++++++++++++ .../components/remote-auth/StartComponent.vue | 113 ++++++++ .../views/auth/remote/onboarding.blade.php | 10 + resources/views/auth/remote/start.blade.php | 10 + 11 files changed, 742 insertions(+), 7 deletions(-) create mode 100644 app/Models/RemoteAuth.php create mode 100644 app/Models/RemoteAuthInstance.php create mode 100644 app/Services/Account/RemoteAuthService.php create mode 100644 config/remote-auth.php create mode 100644 database/migrations/2023_07_07_025757_create_remote_auths_table.php create mode 100644 database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php create mode 100644 resources/assets/components/remote-auth/GettingStartedComponent.vue create mode 100644 resources/assets/components/remote-auth/StartComponent.vue create mode 100644 resources/views/auth/remote/onboarding.blade.php create mode 100644 resources/views/auth/remote/start.blade.php diff --git a/app/Models/RemoteAuth.php b/app/Models/RemoteAuth.php new file mode 100644 index 000000000..98909f09b --- /dev/null +++ b/app/Models/RemoteAuth.php @@ -0,0 +1,19 @@ + 'array', + 'last_successful_login_at' => 'datetime', + 'last_verify_credentials_at' => 'datetime' + ]; +} diff --git a/app/Models/RemoteAuthInstance.php b/app/Models/RemoteAuthInstance.php new file mode 100644 index 000000000..bdc03fcb2 --- /dev/null +++ b/app/Models/RemoteAuthInstance.php @@ -0,0 +1,13 @@ +exists()) { + return RemoteAuthInstance::whereDomain($domain)->first(); + } + + try { + $url = 'https://' . $domain . '/api/v1/apps'; + $res = Http::asForm()->throw()->timeout(10)->post($url, [ + 'client_name' => config('pixelfed.domain.app', 'pixelfed'), + 'redirect_uris' => url('/auth/mastodon/callback'), + 'scopes' => 'read', + 'website' => 'https://pixelfed.org' + ]); + + if(!$res->ok()) { + return false; + } + } catch (RequestException $e) { + return false; + } catch (ConnectionException $e) { + return false; + } catch (Exception $e) { + return false; + } + + $body = $res->json(); + + if(!$body || !isset($body['client_id'])) { + return false; + } + + $raw = RemoteAuthInstance::updateOrCreate([ + 'domain' => $domain + ], [ + 'client_id' => $body['client_id'], + 'client_secret' => $body['client_secret'], + 'redirect_uri' => $body['redirect_uri'], + ]); + + return $raw; + } + + public static function getToken($domain, $code) + { + $raw = RemoteAuthInstance::whereDomain($domain)->first(); + if(!$raw || !$raw->active || $raw->banned) { + return false; + } + + $url = 'https://' . $domain . '/oauth/token'; + $res = Http::asForm()->post($url, [ + 'code' => $code, + 'grant_type' => 'authorization_code', + 'client_id' => $raw->client_id, + 'client_secret' => $raw->client_secret, + 'redirect_uri' => $raw->redirect_uri, + 'scope' => 'read' + ]); + + return $res; + } + + public static function getVerifyCredentials($domain, $code) + { + $raw = RemoteAuthInstance::whereDomain($domain)->first(); + if(!$raw || !$raw->active || $raw->banned) { + return false; + } + + $url = 'https://' . $domain . '/api/v1/accounts/verify_credentials'; + + $res = Http::withToken($code)->get($url); + + return $res->json(); + } + + public static function getFollowing($domain, $code, $id) + { + $raw = RemoteAuthInstance::whereDomain($domain)->first(); + if(!$raw || !$raw->active || $raw->banned) { + return false; + } + + $url = 'https://' . $domain . '/api/v1/accounts/' . $id . '/following?limit=80'; + $key = self::CACHE_KEY . 'get-following:code:' . substr($code, 0, 16) . substr($code, -5) . ':domain:' . $domain. ':id:' .$id; + + return Cache::remember($key, 3600, function() use($url, $code) { + $res = Http::withToken($code)->get($url); + return $res->json(); + }); + } + + public static function isDomainCompatible($domain = false) + { + if(!$domain) { + return false; + } + + return Cache::remember(self::CACHE_KEY . 'domain-compatible:' . $domain, 14400, function() use($domain) { + try { + $res = Http::timeout(20)->retry(3, 750)->get('https://beagle.pixelfed.net/api/v1/raa/domain?domain=' . $domain); + if(!$res->ok()) { + return false; + } + } catch (RequestException $e) { + return false; + } catch (ConnectionException $e) { + return false; + } catch (Exception $e) { + return false; + } + $json = $res->json(); + + if(!in_array('compatible', $json)) { + return false; + } + + return $res['compatible']; + }); + } + + public static function lookupWebfingerUses($wf) + { + try { + $res = Http::timeout(20)->retry(3, 750)->get('https://beagle.pixelfed.net/api/v1/raa/lookup?webfinger=' . $wf); + if(!$res->ok()) { + return false; + } + } catch (RequestException $e) { + return false; + } catch (ConnectionException $e) { + return false; + } catch (Exception $e) { + return false; + } + $json = $res->json(); + if(!$json || !isset($json['count'])) { + return false; + } + + return $json['count']; + } + + public static function submitToBeagle($ow, $ou, $dw, $du) + { + try { + $url = 'https://beagle.pixelfed.net/api/v1/raa/submit'; + $res = Http::throw()->timeout(10)->get($url, [ + 'ow' => $ow, + 'ou' => $ou, + 'dw' => $dw, + 'du' => $du, + ]); + + if(!$res->ok()) { + return; + } + } catch (RequestException $e) { + return; + } catch (ConnectionException $e) { + return; + } catch (Exception $e) { + return; + } + + return; + } +} diff --git a/app/User.php b/app/User.php index 23faf63f4..3e2097811 100644 --- a/app/User.php +++ b/app/User.php @@ -31,13 +31,7 @@ class User extends Authenticatable * @var array */ protected $fillable = [ - 'name', - 'username', - 'email', - 'password', - 'app_register_ip', - 'email_verified_at', - 'last_active_at' + 'name', 'username', 'email', 'password', 'app_register_ip', 'email_verified_at', 'register_source' ]; /** diff --git a/config/remote-auth.php b/config/remote-auth.php new file mode 100644 index 000000000..6d1ac7948 --- /dev/null +++ b/config/remote-auth.php @@ -0,0 +1,56 @@ + [ + 'enabled' => env('PF_LOGIN_WITH_MASTODON_ENABLED', false), + + 'contraints' => [ + /* + * Skip email verification + * + * To improve the onboarding experience, you can opt to skip the email + * verification process and automatically verify their email + */ + 'skip_email_verification' => env('PF_LOGIN_WITH_MASTODON_SKIP_EMAIL', true), + ], + + 'domains' => [ + 'default' => 'mastodon.social,mastodon.online,mstdn.social,mas.to', + + /* + * Custom mastodon domains + * + * Define a comma separated list of custom domains to allow + */ + 'custom' => env('PF_LOGIN_WITH_MASTODON_DOMAINS'), + + /* + * Use only default domains + * + * Allow Sign-in with Mastodon using only the default domains + */ + 'only_default' => env('PF_LOGIN_WITH_MASTODON_ONLY_DEFAULT', true), + + /* + * Use only custom domains + * + * Allow Sign-in with Mastodon using only the custom domains + * you define, in comma separated format + */ + 'only_custom' => env('PF_LOGIN_WITH_MASTODON_ONLY_CUSTOM', false), + ], + + 'max_uses' => [ + /* + * Max Uses + * + * Using a centralized service operated by pixelfed.org that tracks mastodon imports, + * you can set a limit of how many times a mastodon account can be imported across + * all known and reporting Pixelfed instances to prevent the same masto account from + * abusing this + */ + 'enabled' => env('PF_LOGIN_WITH_MASTODON_ENFORCE_MAX_USES', true), + 'limit' => env('PF_LOGIN_WITH_MASTODON_MAX_USES_LIMIT', 3) + ] + ], +]; diff --git a/database/migrations/2023_07_07_025757_create_remote_auths_table.php b/database/migrations/2023_07_07_025757_create_remote_auths_table.php new file mode 100644 index 000000000..774965aa2 --- /dev/null +++ b/database/migrations/2023_07_07_025757_create_remote_auths_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('software')->nullable(); + $table->string('domain')->nullable()->index(); + $table->string('webfinger')->nullable()->unique()->index(); + $table->unsignedInteger('instance_id')->nullable()->index(); + $table->unsignedInteger('user_id')->nullable()->unique()->index(); + $table->unsignedInteger('client_id')->nullable()->index(); + $table->string('ip_address')->nullable(); + $table->text('bearer_token')->nullable(); + $table->json('verify_credentials')->nullable(); + $table->timestamp('last_successful_login_at')->nullable(); + $table->timestamp('last_verify_credentials_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('remote_auths'); + } +}; diff --git a/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php b/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php new file mode 100644 index 000000000..690197b9b --- /dev/null +++ b/database/migrations/2023_07_07_030427_create_remote_auth_instances_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('domain')->nullable()->unique()->index(); + $table->unsignedInteger('instance_id')->nullable()->index(); + $table->string('client_id')->nullable(); + $table->string('client_secret')->nullable(); + $table->string('redirect_uri')->nullable(); + $table->string('root_domain')->nullable()->index(); + $table->boolean('allowed')->nullable()->index(); + $table->boolean('banned')->default(false)->index(); + $table->boolean('active')->default(true)->index(); + $table->timestamp('last_refreshed_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('remote_auth_instances'); + } +}; diff --git a/resources/assets/components/remote-auth/GettingStartedComponent.vue b/resources/assets/components/remote-auth/GettingStartedComponent.vue new file mode 100644 index 000000000..241730fe8 --- /dev/null +++ b/resources/assets/components/remote-auth/GettingStartedComponent.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/resources/assets/components/remote-auth/StartComponent.vue b/resources/assets/components/remote-auth/StartComponent.vue new file mode 100644 index 000000000..558fa13f3 --- /dev/null +++ b/resources/assets/components/remote-auth/StartComponent.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/resources/views/auth/remote/onboarding.blade.php b/resources/views/auth/remote/onboarding.blade.php new file mode 100644 index 000000000..82212a75d --- /dev/null +++ b/resources/views/auth/remote/onboarding.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/auth/remote/start.blade.php b/resources/views/auth/remote/start.blade.php new file mode 100644 index 000000000..9369d7f73 --- /dev/null +++ b/resources/views/auth/remote/start.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.app') + +@section('content') + +@endsection + +@push('scripts') + + +@endpush