RemoteAuthController.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Support\Str;
  4. use Illuminate\Http\Request;
  5. use App\Services\Account\RemoteAuthService;
  6. use App\Models\RemoteAuth;
  7. use App\Profile;
  8. use App\Instance;
  9. use App\User;
  10. use Purify;
  11. use Illuminate\Support\Facades\Auth;
  12. use Illuminate\Support\Facades\Hash;
  13. use Illuminate\Auth\Events\Registered;
  14. use App\Util\Lexer\RestrictedNames;
  15. use App\Services\EmailService;
  16. use App\Services\MediaStorageService;
  17. use App\Util\ActivityPub\Helpers;
  18. use InvalidArgumentException;
  19. class RemoteAuthController extends Controller
  20. {
  21. public function start(Request $request)
  22. {
  23. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  24. if($request->user()) {
  25. return redirect('/');
  26. }
  27. return view('auth.remote.start');
  28. }
  29. public function startRedirect(Request $request)
  30. {
  31. return redirect('/login');
  32. }
  33. public function getAuthDomains(Request $request)
  34. {
  35. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  36. if(config('remote-auth.mastodon.domains.only_custom')) {
  37. $res = config('remote-auth.mastodon.domains.custom');
  38. if(!$res || !strlen($res)) {
  39. return [];
  40. }
  41. $res = explode(',', $res);
  42. return response()->json($res);
  43. }
  44. if( config('remote-auth.mastodon.domains.custom') &&
  45. !config('remote-auth.mastodon.domains.only_default') &&
  46. strlen(config('remote-auth.mastodon.domains.custom')) > 3 &&
  47. strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1
  48. ) {
  49. $res = config('remote-auth.mastodon.domains.custom');
  50. if(!$res || !strlen($res)) {
  51. return [];
  52. }
  53. $res = explode(',', $res);
  54. return response()->json($res);
  55. }
  56. $res = config('remote-auth.mastodon.domains.default');
  57. $res = explode(',', $res);
  58. return response()->json($res);
  59. }
  60. public function redirect(Request $request)
  61. {
  62. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  63. $this->validate($request, ['domain' => 'required']);
  64. $domain = $request->input('domain');
  65. if(str_starts_with(strtolower($domain), 'http')) {
  66. $res = [
  67. 'domain' => $domain,
  68. 'ready' => false,
  69. 'action' => 'incompatible_domain'
  70. ];
  71. return response()->json($res);
  72. }
  73. $validateInstance = Helpers::validateUrl('https://' . $domain . '/?block-check=' . time());
  74. if(!$validateInstance) {
  75. $res = [
  76. 'domain' => $domain,
  77. 'ready' => false,
  78. 'action' => 'blocked_domain'
  79. ];
  80. return response()->json($res);
  81. }
  82. $compatible = RemoteAuthService::isDomainCompatible($domain);
  83. if(!$compatible) {
  84. $res = [
  85. 'domain' => $domain,
  86. 'ready' => false,
  87. 'action' => 'incompatible_domain'
  88. ];
  89. return response()->json($res);
  90. }
  91. if(config('remote-auth.mastodon.domains.only_default')) {
  92. $defaultDomains = explode(',', config('remote-auth.mastodon.domains.default'));
  93. if(!in_array($domain, $defaultDomains)) {
  94. $res = [
  95. 'domain' => $domain,
  96. 'ready' => false,
  97. 'action' => 'incompatible_domain'
  98. ];
  99. return response()->json($res);
  100. }
  101. }
  102. if(config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) {
  103. $customDomains = explode(',', config('remote-auth.mastodon.domains.custom'));
  104. if(!in_array($domain, $customDomains)) {
  105. $res = [
  106. 'domain' => $domain,
  107. 'ready' => false,
  108. 'action' => 'incompatible_domain'
  109. ];
  110. return response()->json($res);
  111. }
  112. }
  113. $client = RemoteAuthService::getMastodonClient($domain);
  114. abort_unless($client, 422, 'Invalid mastodon client');
  115. $request->session()->put('state', $state = Str::random(40));
  116. $request->session()->put('oauth_domain', $domain);
  117. $query = http_build_query([
  118. 'client_id' => $client->client_id,
  119. 'redirect_uri' => $client->redirect_uri,
  120. 'response_type' => 'code',
  121. 'scope' => 'read',
  122. 'state' => $state,
  123. ]);
  124. $request->session()->put('oauth_redirect_to', 'https://' . $domain . '/oauth/authorize?' . $query);
  125. $dsh = Str::random(17);
  126. $res = [
  127. 'domain' => $domain,
  128. 'ready' => true,
  129. 'dsh' => $dsh
  130. ];
  131. return response()->json($res);
  132. }
  133. public function preflight(Request $request)
  134. {
  135. if(!$request->filled('d') || !$request->filled('dsh') || !$request->session()->exists('oauth_redirect_to')) {
  136. return redirect('/login');
  137. }
  138. return redirect()->away($request->session()->pull('oauth_redirect_to'));
  139. }
  140. public function handleCallback(Request $request)
  141. {
  142. $domain = $request->session()->get('oauth_domain');
  143. if($request->filled('code')) {
  144. $code = $request->input('code');
  145. $state = $request->session()->pull('state');
  146. throw_unless(
  147. strlen($state) > 0 && $state === $request->state,
  148. InvalidArgumentException::class,
  149. 'Invalid state value.'
  150. );
  151. $res = RemoteAuthService::getToken($domain, $code);
  152. if(!$res || !isset($res['access_token'])) {
  153. $request->session()->regenerate();
  154. return redirect('/login');
  155. }
  156. $request->session()->put('oauth_remote_session_token', $res['access_token']);
  157. return redirect('/auth/mastodon/getting-started');
  158. }
  159. return redirect('/login');
  160. }
  161. public function onboarding(Request $request)
  162. {
  163. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  164. if($request->user()) {
  165. return redirect('/');
  166. }
  167. return view('auth.remote.onboarding');
  168. }
  169. public function sessionCheck(Request $request)
  170. {
  171. abort_if($request->user(), 403);
  172. abort_unless($request->session()->exists('oauth_domain'), 403);
  173. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  174. $domain = $request->session()->get('oauth_domain');
  175. $token = $request->session()->get('oauth_remote_session_token');
  176. $res = RemoteAuthService::getVerifyCredentials($domain, $token);
  177. abort_if(!$res || !isset($res['acct']), 403, 'Invalid credentials');
  178. $webfinger = strtolower('@' . $res['acct'] . '@' . $domain);
  179. $request->session()->put('oauth_masto_webfinger', $webfinger);
  180. if(config('remote-auth.mastodon.max_uses.enabled')) {
  181. $limit = config('remote-auth.mastodon.max_uses.limit');
  182. $uses = RemoteAuthService::lookupWebfingerUses($webfinger);
  183. if($uses >= $limit) {
  184. return response()->json([
  185. 'code' => 200,
  186. 'msg' => 'Success!',
  187. 'action' => 'max_uses_reached'
  188. ]);
  189. }
  190. }
  191. $exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first();
  192. if($exists && $exists->user_id) {
  193. return response()->json([
  194. 'code' => 200,
  195. 'msg' => 'Success!',
  196. 'action' => 'redirect_existing_user'
  197. ]);
  198. }
  199. return response()->json([
  200. 'code' => 200,
  201. 'msg' => 'Success!',
  202. 'action' => 'onboard'
  203. ]);
  204. }
  205. public function sessionGetMastodonData(Request $request)
  206. {
  207. abort_if($request->user(), 403);
  208. abort_unless($request->session()->exists('oauth_domain'), 403);
  209. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  210. $domain = $request->session()->get('oauth_domain');
  211. $token = $request->session()->get('oauth_remote_session_token');
  212. $res = RemoteAuthService::getVerifyCredentials($domain, $token);
  213. $res['_webfinger'] = strtolower('@' . $res['acct'] . '@' . $domain);
  214. $res['_domain'] = strtolower($domain);
  215. $request->session()->put('oauth_remasto_id', $res['id']);
  216. $ra = RemoteAuth::updateOrCreate([
  217. 'domain' => $domain,
  218. 'webfinger' => $res['_webfinger'],
  219. ], [
  220. 'software' => 'mastodon',
  221. 'ip_address' => $request->ip(),
  222. 'bearer_token' => $token,
  223. 'verify_credentials' => $res,
  224. 'last_verify_credentials_at' => now(),
  225. 'last_successful_login_at' => now()
  226. ]);
  227. $request->session()->put('oauth_masto_raid', $ra->id);
  228. return response()->json($res);
  229. }
  230. public function sessionValidateUsername(Request $request)
  231. {
  232. abort_if($request->user(), 403);
  233. abort_unless($request->session()->exists('oauth_domain'), 403);
  234. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  235. $this->validate($request, [
  236. 'username' => [
  237. 'required',
  238. 'min:2',
  239. 'max:15',
  240. function ($attribute, $value, $fail) {
  241. $dash = substr_count($value, '-');
  242. $underscore = substr_count($value, '_');
  243. $period = substr_count($value, '.');
  244. if(ends_with($value, ['.php', '.js', '.css'])) {
  245. return $fail('Username is invalid.');
  246. }
  247. if(($dash + $underscore + $period) > 1) {
  248. return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
  249. }
  250. if (!ctype_alnum($value[0])) {
  251. return $fail('Username is invalid. Must start with a letter or number.');
  252. }
  253. if (!ctype_alnum($value[strlen($value) - 1])) {
  254. return $fail('Username is invalid. Must end with a letter or number.');
  255. }
  256. $val = str_replace(['_', '.', '-'], '', $value);
  257. if(!ctype_alnum($val)) {
  258. return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
  259. }
  260. $restricted = RestrictedNames::get();
  261. if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
  262. return $fail('Username cannot be used.');
  263. }
  264. }
  265. ]
  266. ]);
  267. $username = strtolower($request->input('username'));
  268. $exists = User::where('username', $username)->exists();
  269. return response()->json([
  270. 'code' => 200,
  271. 'username' => $username,
  272. 'exists' => $exists
  273. ]);
  274. }
  275. public function sessionValidateEmail(Request $request)
  276. {
  277. abort_if($request->user(), 403);
  278. abort_unless($request->session()->exists('oauth_domain'), 403);
  279. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  280. $this->validate($request, [
  281. 'email' => [
  282. 'required',
  283. 'email:strict,filter_unicode,dns,spoof',
  284. ]
  285. ]);
  286. $email = $request->input('email');
  287. $banned = EmailService::isBanned($email);
  288. $exists = User::where('email', $email)->exists();
  289. return response()->json([
  290. 'code' => 200,
  291. 'email' => $email,
  292. 'exists' => $exists,
  293. 'banned' => $banned
  294. ]);
  295. }
  296. public function sessionGetMastodonFollowers(Request $request)
  297. {
  298. abort_unless($request->session()->exists('oauth_domain'), 403);
  299. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  300. abort_unless($request->session()->exists('oauth_remasto_id'), 403);
  301. $domain = $request->session()->get('oauth_domain');
  302. $token = $request->session()->get('oauth_remote_session_token');
  303. $id = $request->session()->get('oauth_remasto_id');
  304. $res = RemoteAuthService::getFollowing($domain, $token, $id);
  305. if(!$res) {
  306. return response()->json([
  307. 'code' => 200,
  308. 'following' => []
  309. ]);
  310. }
  311. $res = collect($res)->filter(fn($acct) => Helpers::validateUrl($acct['url']))->values()->toArray();
  312. return response()->json([
  313. 'code' => 200,
  314. 'following' => $res
  315. ]);
  316. }
  317. public function handleSubmit(Request $request)
  318. {
  319. abort_unless($request->session()->exists('oauth_domain'), 403);
  320. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  321. abort_unless($request->session()->exists('oauth_remasto_id'), 403);
  322. abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
  323. abort_unless($request->session()->exists('oauth_masto_raid'), 403);
  324. $this->validate($request, [
  325. 'email' => 'required|email:strict,filter_unicode,dns,spoof',
  326. 'username' => [
  327. 'required',
  328. 'min:2',
  329. 'max:15',
  330. 'unique:users,username',
  331. function ($attribute, $value, $fail) {
  332. $dash = substr_count($value, '-');
  333. $underscore = substr_count($value, '_');
  334. $period = substr_count($value, '.');
  335. if(ends_with($value, ['.php', '.js', '.css'])) {
  336. return $fail('Username is invalid.');
  337. }
  338. if(($dash + $underscore + $period) > 1) {
  339. return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
  340. }
  341. if (!ctype_alnum($value[0])) {
  342. return $fail('Username is invalid. Must start with a letter or number.');
  343. }
  344. if (!ctype_alnum($value[strlen($value) - 1])) {
  345. return $fail('Username is invalid. Must end with a letter or number.');
  346. }
  347. $val = str_replace(['_', '.', '-'], '', $value);
  348. if(!ctype_alnum($val)) {
  349. return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
  350. }
  351. $restricted = RestrictedNames::get();
  352. if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
  353. return $fail('Username cannot be used.');
  354. }
  355. }
  356. ],
  357. 'password' => 'required|string|min:8|confirmed',
  358. 'name' => 'nullable|max:30'
  359. ]);
  360. $email = $request->input('email');
  361. $username = $request->input('username');
  362. $password = $request->input('password');
  363. $name = $request->input('name');
  364. $user = $this->createUser([
  365. 'name' => $name,
  366. 'username' => $username,
  367. 'password' => $password,
  368. 'email' => $email
  369. ]);
  370. $raid = $request->session()->pull('oauth_masto_raid');
  371. $webfinger = $request->session()->pull('oauth_masto_webfinger');
  372. $token = $user->createToken('Onboarding')->accessToken;
  373. $ra = RemoteAuth::where('id', $raid)->where('webfinger', $webfinger)->firstOrFail();
  374. $ra->user_id = $user->id;
  375. $ra->save();
  376. return [
  377. 'code' => 200,
  378. 'msg' => 'Success',
  379. 'token' => $token
  380. ];
  381. }
  382. public function storeBio(Request $request)
  383. {
  384. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  385. abort_unless($request->user(), 404);
  386. abort_unless($request->session()->exists('oauth_domain'), 403);
  387. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  388. abort_unless($request->session()->exists('oauth_remasto_id'), 403);
  389. $this->validate($request, [
  390. 'bio' => 'required|nullable|max:500',
  391. ]);
  392. $profile = $request->user()->profile;
  393. $profile->bio = Purify::clean($request->input('bio'));
  394. $profile->save();
  395. return [200];
  396. }
  397. public function accountToId(Request $request)
  398. {
  399. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  400. abort_if($request->user(), 404);
  401. abort_unless($request->session()->exists('oauth_domain'), 403);
  402. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  403. abort_unless($request->session()->exists('oauth_remasto_id'), 403);
  404. $this->validate($request, [
  405. 'account' => 'required|url'
  406. ]);
  407. $account = $request->input('account');
  408. abort_unless(substr(strtolower($account), 0, 8) === 'https://', 404);
  409. $host = strtolower(config('pixelfed.domain.app'));
  410. $domain = strtolower(parse_url($account, PHP_URL_HOST));
  411. if($domain == $host) {
  412. $username = Str::of($account)->explode('/')->last();
  413. $user = User::where('username', $username)->first();
  414. if($user) {
  415. return ['id' => (string) $user->profile_id];
  416. } else {
  417. return [];
  418. }
  419. } else {
  420. try {
  421. $profile = Helpers::profileFetch($account);
  422. if($profile) {
  423. return ['id' => (string) $profile->id];
  424. } else {
  425. return [];
  426. }
  427. } catch (\GuzzleHttp\Exception\RequestException $e) {
  428. return;
  429. } catch (Exception $e) {
  430. return [];
  431. }
  432. }
  433. }
  434. public function storeAvatar(Request $request)
  435. {
  436. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  437. abort_unless($request->user(), 404);
  438. $this->validate($request, [
  439. 'avatar_url' => 'required|active_url',
  440. ]);
  441. $user = $request->user();
  442. $profile = $user->profile;
  443. abort_if(!$profile->avatar, 404, 'Missing avatar');
  444. $avatar = $profile->avatar;
  445. $avatar->remote_url = $request->input('avatar_url');
  446. $avatar->save();
  447. MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false);
  448. return [200];
  449. }
  450. public function finishUp(Request $request)
  451. {
  452. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  453. abort_unless($request->user(), 404);
  454. $currentWebfinger = '@' . $request->user()->username . '@' . config('pixelfed.domain.app');
  455. $ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail();
  456. RemoteAuthService::submitToBeagle(
  457. $ra->webfinger,
  458. $ra->verify_credentials['url'],
  459. $currentWebfinger,
  460. $request->user()->url()
  461. );
  462. return [200];
  463. }
  464. public function handleLogin(Request $request)
  465. {
  466. abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
  467. abort_if($request->user(), 404);
  468. abort_unless($request->session()->exists('oauth_domain'), 403);
  469. abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
  470. abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
  471. $domain = $request->session()->get('oauth_domain');
  472. $wf = $request->session()->get('oauth_masto_webfinger');
  473. $ra = RemoteAuth::where('webfinger', $wf)->where('domain', $domain)->whereNotNull('user_id')->firstOrFail();
  474. $user = User::findOrFail($ra->user_id);
  475. abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action');
  476. Auth::loginUsingId($ra->user_id);
  477. return [200];
  478. }
  479. protected function createUser($data)
  480. {
  481. event(new Registered($user = User::create([
  482. 'name' => Purify::clean($data['name']),
  483. 'username' => $data['username'],
  484. 'email' => $data['email'],
  485. 'password' => Hash::make($data['password']),
  486. 'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null,
  487. 'app_register_ip' => request()->ip(),
  488. 'register_source' => 'mastodon'
  489. ])));
  490. $this->guarder()->login($user);
  491. return $user;
  492. }
  493. protected function guarder()
  494. {
  495. return Auth::guard();
  496. }
  497. }