RemoteAuthController.php 20 KB

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