Authorize HelpdeskMac
+ ++ HelpdeskMac is requesting access to your Helpdesk account. +
+ ++ Signed in as {{ auth()->user()->email }} +
+ + + ++ Only approve this request if you initiated login from HelpdeskMac. +
+diff --git a/Modules/Helpdesk/Http/Controllers/NativeAuthorizationController.php b/Modules/Helpdesk/Http/Controllers/NativeAuthorizationController.php index 759fa2d..ef29d30 100644 --- a/Modules/Helpdesk/Http/Controllers/NativeAuthorizationController.php +++ b/Modules/Helpdesk/Http/Controllers/NativeAuthorizationController.php @@ -4,30 +4,81 @@ declare(strict_types=1); namespace Modules\Helpdesk\Http\Controllers; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; use Illuminate\View\View; final class NativeAuthorizationController extends Controller { public function authorize(Request $request): View { - $request->validate([ - 'client_id' => ['required', 'string'], - 'redirect_uri' => ['required', 'string'], + $data = $this->validateAuthorizationRequest($request); + + return view('helpdesk::native.authorize', [ + 'clientId' => $data['client_id'], + 'parameters' => $data, + ]); + } + + public function approve(Request $request): RedirectResponse + { + $data = $this->validateAuthorizationRequest($request); + + $code = Str::random(64); + + DB::table('helpdesk_native_authorization_codes')->insert([ + 'user_id' => $request->user()->getKey(), + 'client_id' => $data['client_id'], + 'code_hash' => hash('sha256', $code), + 'redirect_uri' => $data['redirect_uri'], + 'code_challenge' => $data['code_challenge'], + 'code_challenge_method' => 'S256', + 'expires_at' => now()->addSeconds( + (int) config('helpdesk-native.authorization_code_lifetime', 60) + ), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $separator = str_contains($data['redirect_uri'], '?') ? '&' : '?'; + + return redirect()->away( + $data['redirect_uri'].$separator.http_build_query([ + 'code' => $code, + 'state' => $data['state'], + ]) + ); + } + + private function validateAuthorizationRequest(Request $request): array + { + $data = $request->validate([ + 'client_id' => ['required', 'string', 'max:100'], + 'redirect_uri' => ['required', 'string', 'max:2048'], 'response_type' => ['required', 'in:code'], - 'state' => ['required', 'string', 'min:32'], - 'code_challenge' => ['required', 'string', 'size:43'], + 'state' => ['required', 'string', 'min:32', 'max:512'], + 'code_challenge' => [ + 'required', + 'string', + 'regex:/^[A-Za-z0-9_-]{43}$/', + ], 'code_challenge_method' => ['required', 'in:S256'], ]); - // Authorization-code issuance is not implemented yet. - // Never redirect a code or token to an unregistered URI. - abort(501, 'Native authorization is not yet implemented.'); - } + $registeredUris = config( + 'helpdesk-native.clients.'.$data['client_id'].'.redirect_uris', + [] + ); - public function approve(Request $request): void - { - abort(501, 'Native authorization approval is not yet implemented.'); + abort_unless( + in_array($data['redirect_uri'], $registeredUris, true), + 400, + 'Unregistered native application callback.' + ); + + return $data; } } diff --git a/Modules/Helpdesk/Http/Controllers/NativeTokenController.php b/Modules/Helpdesk/Http/Controllers/NativeTokenController.php new file mode 100644 index 0000000..58edfcd --- /dev/null +++ b/Modules/Helpdesk/Http/Controllers/NativeTokenController.php @@ -0,0 +1,103 @@ +validate([ + 'grant_type' => ['required', 'in:authorization_code'], + 'client_id' => ['required', 'string'], + 'code' => ['required', 'string'], + 'redirect_uri' => ['required', 'string'], + 'code_verifier' => [ + 'required', + 'string', + 'regex:/^[A-Za-z0-9._~-]{43,128}$/', + ], + ]); + + $challenge = rtrim( + strtr( + base64_encode(hash('sha256', $data['code_verifier'], true)), + '+/', + '-_' + ), + '=' + ); + + $result = DB::transaction(function () use ($data, $challenge): ?array { + $authorization = DB::table('helpdesk_native_authorization_codes') + ->where('code_hash', hash('sha256', $data['code'])) + ->lockForUpdate() + ->first(); + + if (! $authorization) { + return null; + } + + if ( + $authorization->consumed_at !== null + || now()->greaterThanOrEqualTo($authorization->expires_at) + || ! hash_equals($authorization->client_id, $data['client_id']) + || ! hash_equals($authorization->redirect_uri, $data['redirect_uri']) + || $authorization->code_challenge_method !== 'S256' + || ! hash_equals($authorization->code_challenge, $challenge) + ) { + return null; + } + + $user = User::query()->find($authorization->user_id); + + if (! $user) { + return null; + } + + DB::table('helpdesk_native_authorization_codes') + ->where('id', $authorization->id) + ->update([ + 'consumed_at' => now(), + 'updated_at' => now(), + ]); + + $token = $user->createToken( + 'HelpdeskMac', + ['helpdesk:native'], + now()->addDays( + (int) config('helpdesk-native.access_token_lifetime_days', 30) + ) + ); + + return [ + 'access_token' => $token->plainTextToken, + 'token_type' => 'Bearer', + 'expires_at' => $token->accessToken->expires_at?->toIso8601String(), + 'user' => [ + 'id' => $user->getKey(), + 'name' => $user->name, + 'email' => $user->email, + ], + ]; + }); + + if ($result === null) { + throw ValidationException::withMessages([ + 'code' => ['Invalid or expired authorization code.'], + ]); + } + + return response()->json($result) + ->header('Cache-Control', 'no-store') + ->header('Pragma', 'no-cache'); + } +} diff --git a/Modules/Helpdesk/Resources/views/native/authorize.blade.php b/Modules/Helpdesk/Resources/views/native/authorize.blade.php new file mode 100644 index 0000000..dde3f50 --- /dev/null +++ b/Modules/Helpdesk/Resources/views/native/authorize.blade.php @@ -0,0 +1,41 @@ + + +
+ + ++ HelpdeskMac is requesting access to your Helpdesk account. +
+ ++ Signed in as {{ auth()->user()->email }} +
+ + + ++ Only approve this request if you initiated login from HelpdeskMac. +
+