diff --git a/.gitignore b/.gitignore index a2b3093..7089732 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea/ .dist/ node_modules/ vendor/ diff --git a/plugins/mojang-verification/README.md b/plugins/mojang-verification/README.md new file mode 100644 index 0000000..cafdefd --- /dev/null +++ b/plugins/mojang-verification/README.md @@ -0,0 +1,19 @@ +# 正版验证 (微软) + +为拥有正版账号的用户提供验证、绑定。 + +## 使用方法 + +本插件部分配置通过修改 `.env` 来进行。 + +1. 在 `https://aka.ms/aad` 创建应用 +2. 增加三条配置项,`MICROSOFT_KEY`、 `MICROSOFT_SECRET`、 `MICROSOFT_REDIRECT_URI` +3. 将 `客户端 ID`、`客户端 Secret`、`回调 URL` 分别填入 `MICROSOFT_KEY`、 `MICROSOFT_SECRET`、 `MICROSOFT_REDIRECT_URI` + +## 示例 + +``` +MICROSOFT_KEY=9fce0559-44b4-4c95-a144-d3ccf50ea62b +MICROSOFT_SECRET=secret@123 +MICROSOFT_REDIRECT_URI=https://skin.bs-community.dev/mojang/callback +``` diff --git a/plugins/mojang-verification/bootstrap.php b/plugins/mojang-verification/bootstrap.php index 81f902d..7dcece6 100644 --- a/plugins/mojang-verification/bootstrap.php +++ b/plugins/mojang-verification/bootstrap.php @@ -7,11 +7,25 @@ use GPlane\Mojang\MojangVerification; use Illuminate\Contracts\Events\Dispatcher; return function (Dispatcher $events, Filter $filter) { + config(['logging.channels.mojang-verification' => [ + 'driver' => 'single', + 'path' => storage_path('logs/mojang-verification.log'), + ]]); + + config(['services.microsoft' => [ + 'client_id' => env('MICROSOFT_KEY'), + 'client_secret' => env('MICROSOFT_SECRET'), + 'redirect' => env('MICROSOFT_REDIRECT_URI'), + ]]); + View::composer('GPlane\Mojang::bind', function ($view) { $view->with('score', option('mojang_verification_score_award', 0)); }); - $events->listen('auth.login.attempt', Listeners\CreateNewUser::class); + $events->listen( + 'SocialiteProviders\Manager\SocialiteWasCalled', + 'GPlane\Mojang\Providers\MicrosoftExtendSocialite@handle' + ); $events->listen( Illuminate\Auth\Events\Authenticated::class, @@ -26,18 +40,13 @@ return function (Dispatcher $events, Filter $filter) { return $badges; }); - $filter->add('auth_page_rows:register', function ($rows) { - $rows[] = 'GPlane\Mojang::notice'; - - return $rows; - }); - Hook::addRoute(function () { Route::prefix('mojang') ->middleware(['web', 'auth']) ->namespace('GPlane\Mojang') ->group(function () { - Route::post('verify', 'AccountController@verify'); + Route::get('verify', 'AccountController@verify'); + Route::get('callback', 'AccountController@verifyCallback'); Route::post('update-uuid', 'AccountController@uuid'); }); }); diff --git a/plugins/mojang-verification/lang/en/bind.yml b/plugins/mojang-verification/lang/en/bind.yml index 88dc652..0f6b029 100644 --- a/plugins/mojang-verification/lang/en/bind.yml +++ b/plugins/mojang-verification/lang/en/bind.yml @@ -1,13 +1,12 @@ title: Bind Your Mojang Account description: | - If you have already paid for Minecraft, you could enter your Mojang account's password below to bind it. Please make sure your email address is the same as the email address of your Minecraft account before binding. - - Of course, we won't save your password. If you worry, you can use a temporary password for your Mojang account then change it back. + If you have already paid for Minecraft, you could click the button below to bind it. After passed the verification, you will get a player with the same name as your premium Minecraft player. Besides, you will become a "Pro" user and gain :score score. +verify: Verify failed: rate: Operations are too frequent. Please try again later. - password: Invalid password. + not-purchased: Unable to verify game ownership. other: Failed to verify. notification: diff --git a/plugins/mojang-verification/lang/en/general.yml b/plugins/mojang-verification/lang/en/general.yml index 7cdde52..9901e00 100644 --- a/plugins/mojang-verification/lang/en/general.yml +++ b/plugins/mojang-verification/lang/en/general.yml @@ -1,4 +1,3 @@ title: Mojang Verification description: Provides verification and binding support for paid Minecraft user. pro: Pro -notice: You could log in using your Mojang account if you have paid for Minecraft. diff --git a/plugins/mojang-verification/lang/zh_CN/bind.yml b/plugins/mojang-verification/lang/zh_CN/bind.yml index 9592243..4499222 100644 --- a/plugins/mojang-verification/lang/zh_CN/bind.yml +++ b/plugins/mojang-verification/lang/zh_CN/bind.yml @@ -1,13 +1,12 @@ title: 正版绑定 description: | - 如果您拥有正版 Minecraft 账号,可在下方输入密码进行验证并绑定。验证前请确保您在本站使用的邮箱与您的 Minecraft 账号的邮箱一致。 - - 请放心,我们不会保存您的密码。如果不放心,可以临时修改您的正版账号密码,并在验证后改回来。 + 如果您拥有正版 Minecraft 账号,可点击下面的按钮进行验证并绑定。 如果验证成功,您将获得正版账号对应的角色,并可获得 :score 积分。 +verify: 验证 failed: rate: 操作过于频繁,请稍后再试。 - password: 密码错误。 + not-purchased: 验证游戏所有权失败。 other: 验证失败,可能是无法连接 Mojang 服务器。 notification: diff --git a/plugins/mojang-verification/lang/zh_CN/general.yml b/plugins/mojang-verification/lang/zh_CN/general.yml index 59cba19..3b86205 100644 --- a/plugins/mojang-verification/lang/zh_CN/general.yml +++ b/plugins/mojang-verification/lang/zh_CN/general.yml @@ -1,4 +1,3 @@ title: 正版验证 description: 为拥有正版账号的用户提供验证、绑定。 pro: 正版 -notice: Mojang 正版用户可直接登录,无需注册。 diff --git a/plugins/mojang-verification/package.json b/plugins/mojang-verification/package.json index d86092f..6cab880 100644 --- a/plugins/mojang-verification/package.json +++ b/plugins/mojang-verification/package.json @@ -1,12 +1,13 @@ { "name": "mojang-verification", - "version": "1.13.2", + "version": "2.0.0", "title": "GPlane\\Mojang::general.title", "description": "GPlane\\Mojang::general.description", "author": "GPlane", "namespace": "GPlane\\Mojang", "require": { - "blessing-skin-server": "^5|^6" + "blessing-skin-server": "^5|^6", + "oauth": "^1.0.0" }, "enchants": { "config": "Configuration", diff --git a/plugins/mojang-verification/src/AccountController.php b/plugins/mojang-verification/src/AccountController.php index d11601c..2bc5ff8 100644 --- a/plugins/mojang-verification/src/AccountController.php +++ b/plugins/mojang-verification/src/AccountController.php @@ -7,25 +7,41 @@ use DB; use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Http; +use Laravel\Socialite\Facades\Socialite; +use Log; class AccountController extends Controller { - public function verify(Request $request, AccountService $accountService) + public function verify(Request $request) { $user = auth()->user(); if (MojangVerification::where('user_id', $user->uid)->count() === 1) { - return back(); + abort(403); } - $result = $accountService->validate($user->email, $request->input('password')); - if ($result['valid']) { - $accountService->bindAccount($user, $result['profiles'], $result['selected']); + Log::channel('mojang-verification')->info("User [$user->email] is try to start verification"); - return back(); - } else { - return back()->with('mojang-failed', $result['message']); + return Socialite::driver('microsoft')->redirect(); + } + + public function verifyCallback(Request $request, AccountService $accountService) + { + if (!$request->has('code')) { + abort(403); } + + $user = auth()->user(); + + if (MojangVerification::where('user_id', $user->uid)->count() === 1) { + abort(403); + } + + $userProfile = Socialite::driver('microsoft')->user(); + + $accountService->bindAccount($user, $userProfile); + + return redirect()->route('user.home'); } public function uuid() diff --git a/plugins/mojang-verification/src/AccountService.php b/plugins/mojang-verification/src/AccountService.php index 7adb48c..7ec3d5a 100644 --- a/plugins/mojang-verification/src/AccountService.php +++ b/plugins/mojang-verification/src/AccountService.php @@ -5,15 +5,11 @@ namespace GPlane\Mojang; use App\Models\Player; use App\Models\User; use App\Services\Hook; -use Composer\CaBundle\CaBundle; use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail as MailService; use Illuminate\Support\Facades\Schema; -use Illuminate\Support\Str; +use Log; class AccountService { @@ -25,106 +21,67 @@ class AccountService $this->events = $dispatcher; } - public function validate(string $username, string $password) + public function bindPlayers(User $user, $profile) { - try { - $response = Http::withOptions(['verify' => CaBundle::getSystemCaRootBundlePath()]) - ->post( - 'https://authserver.mojang.com/authenticate', - array_merge(compact('username', 'password'), [ - 'agent' => ['name' => 'Minecraft', 'version' => 1], - ]) - ); + $player = Player::where('name', $profile->name)->first(); - if ($response->ok()) { - $body = $response->json(); + if ($player) { + if ($player->uid != $user->uid) { + $owner = $player->user; - return [ - 'valid' => Arr::has($body, 'selectedProfile'), - 'profiles' => $body['availableProfiles'], - 'selected' => Arr::get($body, 'selectedProfile'), - ]; - } else { - Log::warning('Received unexpected HTTP status code from Mojang server: '.$response->status()); - - $error = $response->json()['errorMessage']; - if (Str::contains($error, 'Invalid username or password.')) { - $message = trans('GPlane\Mojang::bind.failed.password'); - } elseif ($error === 'Invalid credentials.') { - $message = trans('GPlane\Mojang::bind.failed.rate'); - } else { - $message = trans('GPlane\Mojang::bind.failed.other'); - } - - return ['valid' => false, 'message' => $message]; - } - } catch (\Exception $e) { - report($e); - - return ['valid' => false, 'message' => trans('GPlane\Mojang::bind.failed.other')]; - } - } - - public function bindPlayers(User $user, array $profiles) - { - array_walk($profiles, function ($profile) use ($user) { - $player = Player::where('name', $profile['name'])->first(); - if ($player) { - if ($player->uid != $user->uid) { - $owner = $player->user; - - $player->uid = $user->uid; - $player->tid_skin = 0; - $player->tid_cape = 0; - $player->save(); - - $owner->score += option('score_per_player'); - $owner->save(); - - if (config('mail.default') != '') { - @MailService::to($owner->email)->send(new Mail($owner, $profile['name'])); - $playerName = $player->name; - Hook::sendNotification( - [$owner], - trans('GPlane\Mojang::bind.notification.title', [], $owner->locale), - trans('GPlane\Mojang::bind.notification.content', [ - 'nickname' => $owner->nickname, - 'player' => $playerName, - 'score' => option('score_per_player'), - ], $owner->locale) - ); - } - } - } else { - $this->events->dispatch('player.adding', [$profile['name'], $user]); - - $player = new Player(); $player->uid = $user->uid; - $player->name = $profile['name']; $player->tid_skin = 0; $player->tid_cape = 0; $player->save(); - $this->events->dispatch('player.added', [$player, $user]); - } + $owner->score += option('score_per_player'); + $owner->save(); - // For "yggdrasil-api" plugin. - if (Schema::hasTable('uuid') && DB::table('uuid')->where('name', $profile['name'])->doesntExist()) { - DB::table('uuid')->insert(['name' => $profile['name'], 'uuid' => $profile['id']]); + if (config('mail.default') != '') { + @MailService::to($owner->email)->send(new Mail($owner, $profile->name)); + $playerName = $player->name; + Hook::sendNotification( + [$owner], + trans('GPlane\Mojang::bind.notification.title', [], $owner->locale), + trans('GPlane\Mojang::bind.notification.content', [ + 'nickname' => $owner->nickname, + 'player' => $playerName, + 'score' => option('score_per_player'), + ], $owner->locale) + ); + } } - }); + } else { + $this->events->dispatch('player.adding', [$profile->name, $user]); + + $player = new Player(); + $player->uid = $user->uid; + $player->name = $profile->name; + $player->tid_skin = 0; + $player->tid_cape = 0; + $player->save(); + + $this->events->dispatch('player.added', [$player, $user]); + } + + // For "yggdrasil-api" plugin. + if (Schema::hasTable('uuid') && DB::table('uuid')->where('name', $profile->name)->doesntExist()) { + DB::table('uuid')->insert(['name' => $profile->name, 'uuid' => $profile->id]); + } } - public function bindAccount(User $user, array $profiles, $selected) + public function bindAccount(User $user, $profile) { - $this->bindPlayers($user, $profiles); + $this->bindPlayers($user, $profile); MojangVerification::updateOrCreate( - ['uuid' => $selected['id']], + ['uuid' => $profile->id], ['user_id' => $user->uid, 'verified' => true] ); - $this->events->dispatch('user.mojang.verified', [$user, $selected, $profiles]); + Log::channel('mojang-verification')->info("User [$user->email] account binded successfully. [name=$profile->name,uuid=$profile->id]"); + + $this->events->dispatch('user.mojang-ms.verified', [$user, $profile]); $user->score += (int) option('mojang_verification_score_award', 0); $user->save(); diff --git a/plugins/mojang-verification/src/Listeners/CreateNewUser.php b/plugins/mojang-verification/src/Listeners/CreateNewUser.php deleted file mode 100644 index 0d5c0e1..0000000 --- a/plugins/mojang-verification/src/Listeners/CreateNewUser.php +++ /dev/null @@ -1,86 +0,0 @@ -filter = $filter; - $this->accountService = $accountService; - $this->events = $dispatcher; - } - - public function handle($email, $password, $authType) - { - if ($authType != 'email') { - return; - } - - $user = User::where('email', $email)->first(); - if ($user) { - return; - } - - $result = $this->accountService->validate($email, $password); - if (!$result['valid']) { - return; - } - - $uuid = Arr::get($result['selected'], 'id'); - $record = MojangVerification::where('uuid', $uuid)->first(); - if ($record) { - $user = User::find($record->user_id); - if ($user) { - $this->events->dispatch('user.profile.updating', [$user, 'email', ['email' => $email]]); - $user->update(['email' => $email]); - $this->events->dispatch('user.profile.updated', [$user, 'email', ['email' => $email]]); - - return; - } - } - - $whip = new Whip(); - $ip = $whip->getValidIpAddress(); - $ip = $this->filter->apply('client_ip', $ip); - - $user = new User(); - $user->email = $email; - $user->nickname = Arr::get($result['selected'], 'name', ''); - $user->score = option('user_initial_score'); - $user->avatar = 0; - $password = app('cipher')->hash(request('password'), config('secure.salt')); - $password = $this->filter->apply('user_password', $password); - $user->password = $password; - $user->ip = $ip; - $user->permission = User::NORMAL; - $user->register_at = Carbon::now(); - $user->last_sign_at = Carbon::now()->subDay(); - $user->save(); - - $this->events->dispatch('auth.registration.completed', [$user]); - - $this->accountService->bindAccount($user, $result['profiles'], $result['selected']); - } -} diff --git a/plugins/mojang-verification/src/Listeners/OnAuthenticated.php b/plugins/mojang-verification/src/Listeners/OnAuthenticated.php index a9d6c96..85a25ed 100644 --- a/plugins/mojang-verification/src/Listeners/OnAuthenticated.php +++ b/plugins/mojang-verification/src/Listeners/OnAuthenticated.php @@ -29,7 +29,7 @@ class OnAuthenticated return $grid; }); - Hook::addScriptFileToPage(plugin_assets('mojang-verification', 'update-uuid.js'), ['user/profile']); + Hook::addScriptFileToPage(plugin('mojang-verification')->assets('update-uuid.js'), ['user/profile']); } } else { $this->filter->add('grid:user.index', function ($grid) { diff --git a/plugins/mojang-verification/src/Providers/MicrosoftExtendSocialite.php b/plugins/mojang-verification/src/Providers/MicrosoftExtendSocialite.php new file mode 100644 index 0000000..f66c8a9 --- /dev/null +++ b/plugins/mojang-verification/src/Providers/MicrosoftExtendSocialite.php @@ -0,0 +1,13 @@ +extendSocialite('microsoft', __NAMESPACE__.'\MicrosoftProvider'); + } +} diff --git a/plugins/mojang-verification/src/Providers/MicrosoftProvider.php b/plugins/mojang-verification/src/Providers/MicrosoftProvider.php new file mode 100644 index 0000000..dd29554 --- /dev/null +++ b/plugins/mojang-verification/src/Providers/MicrosoftProvider.php @@ -0,0 +1,112 @@ +buildAuthUrlFromBase('https://login.live.com/oauth20_authorize.srf', $state); + } + + protected function getTokenUrl() + { + return 'https://login.live.com/oauth20_token.srf'; + } + + /** + * @see https://wiki.vg/Microsoft_Authentication_Scheme + */ + protected function getUserByToken($token) + { + $user = auth()->user(); + + // Authenticate with XBox Live + $response = Http::post('https://user.auth.xboxlive.com/user/authenticate', [ + 'Properties' => [ + 'AuthMethod' => 'RPS', + 'SiteName' => 'user.auth.xboxlive.com', + 'RpsTicket' => 'd='.$token, + ], + 'RelyingParty' => 'http://auth.xboxlive.com', + 'TokenType' => 'JWT', + ])->json(); + + $xbl_token = $response['Token']; + $user_hash = $response['DisplayClaims']['xui'][0]['uhs']; + + // Authenticate with XSTS (Xbox One Security Token Service) + $response = Http::post('https://xsts.auth.xboxlive.com/xsts/authorize', [ + 'Properties' => [ + 'SandboxId' => 'RETAIL', + 'UserTokens' => [$xbl_token], + ], + 'RelyingParty' => 'rp://api.minecraftservices.com/', + 'TokenType' => 'JWT', + ])->json(); + + if (Arr::exists($response, 'XErr')) { + // TODO show detail error to user + Log::channel('mojang-verification')->info("User [$user->email] authenticate with XSTS failed.", compact('response')); + abort(500, trans('GPlane\Mojang::bind.failed.other')); + } + + $xsts_token = $response['Token']; + + // Authenticate with Minecraft + $response = Http::post('https://api.minecraftservices.com/authentication/login_with_xbox', [ + 'identityToken' => 'XBL3.0 x='.$user_hash.';'.$xsts_token, + ])->json(); + + if (Arr::exists($response, 'error')) { + // UNAUTHORIZED + Log::channel('mojang-verification')->info("User [$user->email] authenticate with Minecraft failed.", compact('response')); + abort(500); + } + + $minecraft_access_token = $response['access_token']; + + // Get the profile + $response = Http::withToken($minecraft_access_token)->get('https://api.minecraftservices.com/minecraft/profile')->json(); + + if (Arr::exists($response, 'error')) { + // logger($response); + // NOT_FOUND + // CONSTRAINT_VIOLATION + Log::channel('mojang-verification')->info("User [$user->email] get the profile failed.", compact('response')); + abort(403, trans('GPlane\Mojang::bind.failed.not-purchased')); + } + + return $response; + } + + protected function mapUserToObject(array $user) + { + return (new User())->setRaw($user)->map([ + 'id' => $user['id'], + 'name' => $user['name'], + ]); + } + + protected function getTokenFields($code) + { + return array_merge(parent::getTokenFields($code), [ + 'grant_type' => 'authorization_code', + ]); + } +} diff --git a/plugins/mojang-verification/views/bind.twig b/plugins/mojang-verification/views/bind.twig index 124a3cf..047d688 100644 --- a/plugins/mojang-verification/views/bind.twig +++ b/plugins/mojang-verification/views/bind.twig @@ -1,26 +1,17 @@ -
- {{ csrf_field() }} -
-

- {{ trans('GPlane\\Mojang::bind.title') }} -

-
-
- {% if session_has('mojang-failed') %} -
- {{ session_pull('mojang-failed') }} -
- {% endif %} -
- {{ trans('GPlane\\Mojang::bind.description', {score: score})|nl2br }} -
- -
- + +
+

+ {{ trans('GPlane\\Mojang::bind.title') }} +

+
+
+
+ {{ trans('GPlane\\Mojang::bind.description', {score: score})|nl2br }} +
+
+
diff --git a/plugins/mojang-verification/views/notice.twig b/plugins/mojang-verification/views/notice.twig deleted file mode 100644 index 52a2c20..0000000 --- a/plugins/mojang-verification/views/notice.twig +++ /dev/null @@ -1,3 +0,0 @@ -
- {{ trans('GPlane\\Mojang::general.notice') }} -