pixelfed/pixelfed

View on GitHub
app/Http/Controllers/RemoteAuthController.php

Summary

Maintainability
F
5 days
Test Coverage
<?php

namespace App\Http\Controllers;

use App\Models\RemoteAuth;
use App\Services\Account\RemoteAuthService;
use App\Services\EmailService;
use App\Services\MediaStorageService;
use App\User;
use App\Util\ActivityPub\Helpers;
use App\Util\Lexer\RestrictedNames;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Purify;

class RemoteAuthController extends Controller
{
    public function start(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        if ($request->user()) {
            return redirect('/');
        }

        return view('auth.remote.start');
    }

    public function startRedirect(Request $request)
    {
        return redirect('/login');
    }

    public function getAuthDomains(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);

        if (config('remote-auth.mastodon.domains.only_custom')) {
            $res = config('remote-auth.mastodon.domains.custom');
            if (! $res || ! strlen($res)) {
                return [];
            }
            $res = explode(',', $res);

            return response()->json($res);
        }

        if (config('remote-auth.mastodon.domains.custom') &&
            ! config('remote-auth.mastodon.domains.only_default') &&
            strlen(config('remote-auth.mastodon.domains.custom')) > 3 &&
            strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1
        ) {
            $res = config('remote-auth.mastodon.domains.custom');
            if (! $res || ! strlen($res)) {
                return [];
            }
            $res = explode(',', $res);

            return response()->json($res);
        }

        $res = config('remote-auth.mastodon.domains.default');
        $res = explode(',', $res);

        return response()->json($res);
    }

    public function redirect(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);

        $this->validate($request, ['domain' => 'required']);

        $domain = $request->input('domain');

        if (str_starts_with(strtolower($domain), 'http')) {
            $res = [
                'domain' => $domain,
                'ready' => false,
                'action' => 'incompatible_domain',
            ];

            return response()->json($res);
        }

        $validateInstance = Helpers::validateUrl('https://'.$domain.'/?block-check='.time());

        if (! $validateInstance) {
            $res = [
                'domain' => $domain,
                'ready' => false,
                'action' => 'blocked_domain',
            ];

            return response()->json($res);
        }

        $compatible = RemoteAuthService::isDomainCompatible($domain);

        if (! $compatible) {
            $res = [
                'domain' => $domain,
                'ready' => false,
                'action' => 'incompatible_domain',
            ];

            return response()->json($res);
        }

        if (config('remote-auth.mastodon.domains.only_default')) {
            $defaultDomains = explode(',', config('remote-auth.mastodon.domains.default'));
            if (! in_array($domain, $defaultDomains)) {
                $res = [
                    'domain' => $domain,
                    'ready' => false,
                    'action' => 'incompatible_domain',
                ];

                return response()->json($res);
            }
        }

        if (config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) {
            $customDomains = explode(',', config('remote-auth.mastodon.domains.custom'));
            if (! in_array($domain, $customDomains)) {
                $res = [
                    'domain' => $domain,
                    'ready' => false,
                    'action' => 'incompatible_domain',
                ];

                return response()->json($res);
            }
        }

        $client = RemoteAuthService::getMastodonClient($domain);

        abort_unless($client, 422, 'Invalid mastodon client');

        $request->session()->put('state', $state = Str::random(40));
        $request->session()->put('oauth_domain', $domain);

        $query = http_build_query([
            'client_id' => $client->client_id,
            'redirect_uri' => $client->redirect_uri,
            'response_type' => 'code',
            'scope' => 'read',
            'state' => $state,
        ]);

        $request->session()->put('oauth_redirect_to', 'https://'.$domain.'/oauth/authorize?'.$query);

        $dsh = Str::random(17);
        $res = [
            'domain' => $domain,
            'ready' => true,
            'dsh' => $dsh,
        ];

        return response()->json($res);
    }

    public function preflight(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);

        if (! $request->filled('d') || ! $request->filled('dsh') || ! $request->session()->exists('oauth_redirect_to')) {
            return redirect('/login');
        }

        return redirect()->away($request->session()->pull('oauth_redirect_to'));
    }

    public function handleCallback(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);

        $domain = $request->session()->get('oauth_domain');

        if ($request->filled('code')) {
            $code = $request->input('code');
            $state = $request->session()->pull('state');

            throw_unless(
                strlen($state) > 0 && $state === $request->state,
                InvalidArgumentException::class,
                'Invalid state value.'
            );

            $res = RemoteAuthService::getToken($domain, $code);

            if (! $res || ! isset($res['access_token'])) {
                $request->session()->regenerate();

                return redirect('/login');
            }

            $request->session()->put('oauth_remote_session_token', $res['access_token']);

            return redirect('/auth/mastodon/getting-started');
        }

        return redirect('/login');
    }

    public function onboarding(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        if ($request->user()) {
            return redirect('/');
        }

        return view('auth.remote.onboarding');
    }

    public function sessionCheck(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_if($request->user(), 403);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);

        $domain = $request->session()->get('oauth_domain');
        $token = $request->session()->get('oauth_remote_session_token');

        $res = RemoteAuthService::getVerifyCredentials($domain, $token);

        abort_if(! $res || ! isset($res['acct']), 403, 'Invalid credentials');

        $webfinger = strtolower('@'.$res['acct'].'@'.$domain);
        $request->session()->put('oauth_masto_webfinger', $webfinger);

        if (config('remote-auth.mastodon.max_uses.enabled')) {
            $limit = config('remote-auth.mastodon.max_uses.limit');
            $uses = RemoteAuthService::lookupWebfingerUses($webfinger);
            if ($uses >= $limit) {
                return response()->json([
                    'code' => 200,
                    'msg' => 'Success!',
                    'action' => 'max_uses_reached',
                ]);
            }
        }

        $exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first();
        if ($exists && $exists->user_id) {
            return response()->json([
                'code' => 200,
                'msg' => 'Success!',
                'action' => 'redirect_existing_user',
            ]);
        }

        return response()->json([
            'code' => 200,
            'msg' => 'Success!',
            'action' => 'onboard',
        ]);
    }

    public function sessionGetMastodonData(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_if($request->user(), 403);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);

        $domain = $request->session()->get('oauth_domain');
        $token = $request->session()->get('oauth_remote_session_token');

        $res = RemoteAuthService::getVerifyCredentials($domain, $token);
        $res['_webfinger'] = strtolower('@'.$res['acct'].'@'.$domain);
        $res['_domain'] = strtolower($domain);
        $request->session()->put('oauth_remasto_id', $res['id']);

        $ra = RemoteAuth::updateOrCreate([
            'domain' => $domain,
            'webfinger' => $res['_webfinger'],
        ], [
            'software' => 'mastodon',
            'ip_address' => $request->ip(),
            'bearer_token' => $token,
            'verify_credentials' => $res,
            'last_verify_credentials_at' => now(),
            'last_successful_login_at' => now(),
        ]);

        $request->session()->put('oauth_masto_raid', $ra->id);

        return response()->json($res);
    }

    public function sessionValidateUsername(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_if($request->user(), 403);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);

        $this->validate($request, [
            'username' => [
                'required',
                'min:2',
                'max:15',
                function ($attribute, $value, $fail) {
                    $dash = substr_count($value, '-');
                    $underscore = substr_count($value, '_');
                    $period = substr_count($value, '.');

                    if (ends_with($value, ['.php', '.js', '.css'])) {
                        return $fail('Username is invalid.');
                    }

                    if (($dash + $underscore + $period) > 1) {
                        return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
                    }

                    if (! ctype_alnum($value[0])) {
                        return $fail('Username is invalid. Must start with a letter or number.');
                    }

                    if (! ctype_alnum($value[strlen($value) - 1])) {
                        return $fail('Username is invalid. Must end with a letter or number.');
                    }

                    $val = str_replace(['_', '.', '-'], '', $value);
                    if (! ctype_alnum($val)) {
                        return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
                    }

                    $restricted = RestrictedNames::get();
                    if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
                        return $fail('Username cannot be used.');
                    }
                },
            ],
        ]);
        $username = strtolower($request->input('username'));

        $exists = User::where('username', $username)->exists();

        return response()->json([
            'code' => 200,
            'username' => $username,
            'exists' => $exists,
        ]);
    }

    public function sessionValidateEmail(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_if($request->user(), 403);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);

        $this->validate($request, [
            'email' => [
                'required',
                'email:strict,filter_unicode,dns,spoof',
            ],
        ]);

        $email = $request->input('email');
        $banned = EmailService::isBanned($email);
        $exists = User::where('email', $email)->exists();

        return response()->json([
            'code' => 200,
            'email' => $email,
            'exists' => $exists,
            'banned' => $banned,
        ]);
    }

    public function sessionGetMastodonFollowers(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
        abort_unless($request->session()->exists('oauth_remasto_id'), 403);

        $domain = $request->session()->get('oauth_domain');
        $token = $request->session()->get('oauth_remote_session_token');
        $id = $request->session()->get('oauth_remasto_id');

        $res = RemoteAuthService::getFollowing($domain, $token, $id);

        if (! $res) {
            return response()->json([
                'code' => 200,
                'following' => [],
            ]);
        }

        $res = collect($res)->filter(fn ($acct) => Helpers::validateUrl($acct['url']))->values()->toArray();

        return response()->json([
            'code' => 200,
            'following' => $res,
        ]);
    }

    public function handleSubmit(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
        abort_unless($request->session()->exists('oauth_remasto_id'), 403);
        abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
        abort_unless($request->session()->exists('oauth_masto_raid'), 403);

        $this->validate($request, [
            'email' => 'required|email:strict,filter_unicode,dns,spoof',
            'username' => [
                'required',
                'min:2',
                'max:15',
                'unique:users,username',
                function ($attribute, $value, $fail) {
                    $dash = substr_count($value, '-');
                    $underscore = substr_count($value, '_');
                    $period = substr_count($value, '.');

                    if (ends_with($value, ['.php', '.js', '.css'])) {
                        return $fail('Username is invalid.');
                    }

                    if (($dash + $underscore + $period) > 1) {
                        return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
                    }

                    if (! ctype_alnum($value[0])) {
                        return $fail('Username is invalid. Must start with a letter or number.');
                    }

                    if (! ctype_alnum($value[strlen($value) - 1])) {
                        return $fail('Username is invalid. Must end with a letter or number.');
                    }

                    $val = str_replace(['_', '.', '-'], '', $value);
                    if (! ctype_alnum($val)) {
                        return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
                    }

                    $restricted = RestrictedNames::get();
                    if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
                        return $fail('Username cannot be used.');
                    }
                },
            ],
            'password' => 'required|string|min:8|confirmed',
            'name' => 'nullable|max:30',
        ]);

        $email = $request->input('email');
        $username = $request->input('username');
        $password = $request->input('password');
        $name = $request->input('name');

        $user = $this->createUser([
            'name' => $name,
            'username' => $username,
            'password' => $password,
            'email' => $email,
        ]);

        $raid = $request->session()->pull('oauth_masto_raid');
        $webfinger = $request->session()->pull('oauth_masto_webfinger');
        $token = $user->createToken('Onboarding')->accessToken;

        $ra = RemoteAuth::where('id', $raid)->where('webfinger', $webfinger)->firstOrFail();
        $ra->user_id = $user->id;
        $ra->save();

        return [
            'code' => 200,
            'msg' => 'Success',
            'token' => $token,
        ];
    }

    public function storeBio(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_unless($request->user(), 404);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
        abort_unless($request->session()->exists('oauth_remasto_id'), 403);

        $this->validate($request, [
            'bio' => 'required|nullable|max:500',
        ]);

        $profile = $request->user()->profile;
        $profile->bio = Purify::clean($request->input('bio'));
        $profile->save();

        return [200];
    }

    public function accountToId(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_if($request->user(), 404);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
        abort_unless($request->session()->exists('oauth_remasto_id'), 403);

        $this->validate($request, [
            'account' => 'required|url',
        ]);

        $account = $request->input('account');
        abort_unless(substr(strtolower($account), 0, 8) === 'https://', 404);

        $host = strtolower(config('pixelfed.domain.app'));
        $domain = strtolower(parse_url($account, PHP_URL_HOST));

        if ($domain == $host) {
            $username = Str::of($account)->explode('/')->last();
            $user = User::where('username', $username)->first();
            if ($user) {
                return ['id' => (string) $user->profile_id];
            } else {
                return [];
            }
        } else {
            try {
                $profile = Helpers::profileFetch($account);
                if ($profile) {
                    return ['id' => (string) $profile->id];
                } else {
                    return [];
                }
            } catch (\GuzzleHttp\Exception\RequestException $e) {
                return;
            } catch (Exception $e) {
                return [];
            }
        }
    }

    public function storeAvatar(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_unless($request->user(), 404);
        $this->validate($request, [
            'avatar_url' => 'required|active_url',
        ]);

        $user = $request->user();
        $profile = $user->profile;

        abort_if(! $profile->avatar, 404, 'Missing avatar');

        $avatar = $profile->avatar;
        $avatar->remote_url = $request->input('avatar_url');
        $avatar->save();

        MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false);

        return [200];
    }

    public function finishUp(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_unless($request->user(), 404);

        $currentWebfinger = '@'.$request->user()->username.'@'.config('pixelfed.domain.app');
        $ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail();
        RemoteAuthService::submitToBeagle(
            $ra->webfinger,
            $ra->verify_credentials['url'],
            $currentWebfinger,
            $request->user()->url()
        );

        return [200];
    }

    public function handleLogin(Request $request)
    {
        abort_unless((
            config_cache('pixelfed.open_registration') &&
            config('remote-auth.mastodon.enabled')
        ) || (
            config('remote-auth.mastodon.ignore_closed_state') &&
            config('remote-auth.mastodon.enabled')
        ), 404);
        abort_if($request->user(), 404);
        abort_unless($request->session()->exists('oauth_domain'), 403);
        abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
        abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);

        $domain = $request->session()->get('oauth_domain');
        $wf = $request->session()->get('oauth_masto_webfinger');

        $ra = RemoteAuth::where('webfinger', $wf)->where('domain', $domain)->whereNotNull('user_id')->firstOrFail();

        $user = User::findOrFail($ra->user_id);
        abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action');
        Auth::loginUsingId($ra->user_id);

        return [200];
    }

    protected function createUser($data)
    {
        event(new Registered($user = User::create([
            'name' => Purify::clean($data['name']),
            'username' => $data['username'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
            'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null,
            'app_register_ip' => request()->ip(),
            'register_source' => 'mastodon',
        ])));

        $this->guarder()->login($user);

        return $user;
    }

    protected function guarder()
    {
        return Auth::guard();
    }
}