1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6declare(strict_types=1);
7
8namespace App\Http\Controllers\Account;
9
10use App\Http\Controllers\Controller;
11use App\Models\GithubUser;
12use League\OAuth2\Client\Provider\Exception\GithubIdentityProviderException;
13use League\OAuth2\Client\Provider\Github as GithubProvider;
14
15class GithubUsersController extends Controller
16{
17 public function __construct()
18 {
19 $this->middleware('auth');
20 $this->middleware('verify-user');
21
22 parent::__construct();
23 }
24
25 public function callback()
26 {
27 $params = get_params(request()->all(), null, [
28 'code:string',
29 'error:string',
30 'state:string',
31 ], ['null_missing' => true]);
32
33 abort_if($params['state'] === null, 422, 'Missing state parameter.');
34 abort_unless(
35 hash_equals(session()->pull('github_auth_state', ''), $params['state']),
36 403,
37 'Invalid state.',
38 );
39
40 // If the user denied authorization on GitHub, redirect back to the GitHub account settings
41 // <https://docs.github.com/en/apps/oauth-apps/maintaining-oauth-apps/troubleshooting-authorization-request-errors#access-denied>
42 if ($params['error'] === 'access_denied') {
43 return redirect(route('account.edit').'#github');
44 }
45
46 abort_if($params['error'] !== null, 500, 'Error obtaining authorization from GitHub.');
47 abort_if($params['code'] === null, 422, 'Missing code parameter.');
48
49 try {
50 $token = $this
51 ->makeGithubOAuthProvider()
52 ->getAccessToken('authorization_code', ['code' => $params['code']]);
53 } catch (GithubIdentityProviderException $exception) {
54 switch ($exception->getMessage()) {
55 // <https://docs.github.com/en/apps/oauth-apps/maintaining-oauth-apps/troubleshooting-oauth-app-access-token-request-errors#bad-verification-code>
56 case 'bad_verification_code':
57 return abort(422, 'Invalid authorization code.');
58
59 // <https://docs.github.com/en/apps/oauth-apps/maintaining-oauth-apps/troubleshooting-oauth-app-access-token-request-errors#unverified-user-email>
60 case 'unverified_user_email':
61 return abort(422, osu_trans('accounts.github_user.error.unverified_email'));
62
63 default:
64 throw $exception;
65 }
66 }
67
68 $client = new \Github\Client();
69 $client->authenticate($token->getToken(), \Github\AuthMethod::ACCESS_TOKEN);
70 $apiUser = $client->currentUser()->show();
71
72 $githubUser = GithubUser::firstWhere('canonical_id', $apiUser['id']);
73
74 abort_if($githubUser === null, 422, osu_trans('accounts.github_user.error.no_contribution'));
75 abort_if($githubUser->user_id !== null, 422, osu_trans('accounts.github_user.error.already_linked'));
76
77 $githubUser->update([
78 'user_id' => auth()->id(),
79 'username' => $apiUser['login'],
80 ]);
81
82 return redirect(route('account.edit').'#github');
83 }
84
85 public function create()
86 {
87 abort_unless(GithubUser::canAuthenticate(), 404);
88
89 if (auth()->user()->githubUser()->exists()) {
90 return redirect(route('account.edit').'#github');
91 }
92
93 $provider = $this->makeGithubOAuthProvider();
94 $url = $provider->getAuthorizationUrl([
95 'allow_signup' => 'false',
96 'scope' => ' ', // Provider doesn't support empty scope
97 ]);
98
99 session()->put('github_auth_state', $provider->getState());
100
101 return redirect($url);
102 }
103
104 public function destroy()
105 {
106 auth()->user()->githubUser()->update(['user_id' => null]);
107
108 return response(null, 204);
109 }
110
111 private function makeGithubOAuthProvider(): GithubProvider
112 {
113 return new GithubProvider([
114 'clientId' => $GLOBALS['cfg']['osu']['github']['client_id'],
115 'clientSecret' => $GLOBALS['cfg']['osu']['github']['client_secret'],
116 ]);
117 }
118}