query(), [ 'response_type' => ['required', 'in:code'], 'client_id' => ['required', 'string', 'max:80'], 'redirect_uri' => ['required', 'url:http,https'], 'state' => ['nullable', 'string', 'max:1024'], 'nonce' => ['nullable', 'string', 'max:255'], 'prompt' => ['nullable', 'string', 'max:100'], ]); if ($validator->fails()) { return $this->authorizationError( (string) $request->query('redirect_uri', ''), (string) $request->query('state', ''), 'invalid_request', (string) $validator->errors()->first() ); } $client = $this->clientAuthService->findActiveClientById((string) $request->query('client_id')); if (! $client) { return $this->authorizationError( (string) $request->query('redirect_uri', ''), (string) $request->query('state', ''), 'invalid_client', 'Client not found or disabled.' ); } $redirectUri = (string) $request->query('redirect_uri'); if (! $this->clientAuthService->isRedirectUriAllowed($client, $redirectUri)) { return $this->authorizationError($redirectUri, (string) $request->query('state', ''), 'invalid_request', 'Invalid redirect_uri.'); } $serverScopes = $this->clientAuthService->allowedScopeNames($client); $resourceOwner = $this->resolveResourceOwner($request); if (! $resourceOwner) { return $this->redirectToFrontendConsent($request, $client); } $scopeFingerprint = $this->tokenService->scopeFingerprint($serverScopes); $consent = OauthConsent::query() ->where('user_id', $resourceOwner->id) ->where('client_id', $client->id) ->where('scope_fingerprint', $scopeFingerprint) ->first(); $prompt = trim((string) $request->query('prompt', '')); $consentRequired = $consent === null || $prompt === 'consent'; if ($consentRequired) { return $this->redirectToFrontendConsent($request, $client); } $code = $this->tokenService->issueAuthorizationCode( user: $resourceOwner, client: $client, scopes: $serverScopes, redirectUri: $redirectUri, nonce: $request->query('nonce') ? (string) $request->query('nonce') : null ); $this->writeOAuthAuthorizationLog($resourceOwner, $request, $client, true); return $this->successRedirect($redirectUri, $code, (string) $request->query('state', '')); } #[Apidoc\Title('OAuth 授权确认'), Apidoc\Method('POST'), Apidoc\Url('/oauth/authorize/decision')] public function decision(Request $request): RedirectResponse|JsonResponse { $validator = Validator::make($request->all(), [ 'approve' => ['required', 'boolean'], 'client_id' => ['required', 'string', 'max:80'], 'redirect_uri' => ['required', 'url:http,https'], 'state' => ['nullable', 'string', 'max:1024'], 'nonce' => ['nullable', 'string', 'max:255'], ]); if ($validator->fails()) { return response()->json([ 'error' => 'invalid_request', 'error_description' => (string) $validator->errors()->first(), ], 400); } $client = $this->clientAuthService->findActiveClientById((string) $request->input('client_id')); if (! $client) { return response()->json([ 'error' => 'invalid_client', 'error_description' => 'Client not found or disabled.', ], 400); } $redirectUri = (string) $request->input('redirect_uri'); if (! $this->clientAuthService->isRedirectUriAllowed($client, $redirectUri)) { return response()->json([ 'error' => 'invalid_request', 'error_description' => 'Invalid redirect_uri.', ], 400); } $resourceOwner = $this->resolveResourceOwner($request); if (! $resourceOwner) { return response()->json([ 'error' => 'access_denied', 'error_description' => 'User is not authenticated.', ], 401); } $serverScopes = $this->clientAuthService->allowedScopeNames($client); if (! $request->boolean('approve')) { $this->writeOAuthAuthorizationLog($resourceOwner, $request, $client, false); return $this->authorizationError( $redirectUri, (string) $request->input('state', ''), 'access_denied', 'Resource owner denied the request.' ); } $scopeFingerprint = $this->tokenService->scopeFingerprint($serverScopes); OauthConsent::query()->updateOrCreate( [ 'user_id' => $resourceOwner->id, 'client_id' => $client->id, 'scope_fingerprint' => $scopeFingerprint, ], [ 'scopes' => $serverScopes, 'granted_at' => now(), ] ); $code = $this->tokenService->issueAuthorizationCode( user: $resourceOwner, client: $client, scopes: $serverScopes, redirectUri: $redirectUri, nonce: $request->input('nonce') ? (string) $request->input('nonce') : null ); $this->writeOAuthAuthorizationLog($resourceOwner, $request, $client, false); return $this->successRedirect($redirectUri, $code, (string) $request->input('state', '')); } private function redirectToFrontendConsent(Request $request, OauthClient $client): RedirectResponse { $frontendConsentUrl = (string) config('oauth.frontend_consent_url'); $userinfoFields = collect($client->allowed_userinfo_fields ?? config('oauth.userinfo_fields', [])) ->map(fn ($field): string => trim((string) $field)) ->filter() ->unique() ->values() ->all(); $remapRules = collect($client->userinfo_claim_remap ?? []) ->mapWithKeys(function ($target, $source): array { $from = trim((string) $source); $to = trim((string) $target); if ($from === '' || $to === '') { return []; } return [$from => $to]; }) ->all(); $query = [ 'return_to' => $this->buildFrontendAuthorizeReturnTo($request), 'client_id' => $client->client_id, 'client_name' => $client->name, 'client_logo' => $client->logo_url, 'state' => (string) $request->query('state', ''), 'nonce' => (string) $request->query('nonce', ''), 'redirect_uri' => (string) $request->query('redirect_uri', ''), 'userinfo_fields' => base64_encode((string) json_encode($userinfoFields, JSON_UNESCAPED_UNICODE)), 'userinfo_remap' => base64_encode((string) json_encode($remapRules, JSON_UNESCAPED_UNICODE)), ]; return redirect()->away($this->appendQueryToFrontendUrl($frontendConsentUrl, $query)); } private function buildFrontendAuthorizeReturnTo(Request $request): string { $base = trim((string) config('oauth.frontend_authorize_proxy_base', '/api')); $base = $base === '' ? '/api' : $base; $base = rtrim($base, '/'); $queryString = (string) ($request->getQueryString() ?? ''); $target = $base.'/oauth/authorize'; return $queryString === '' ? $target : $target.'?'.$queryString; } private function successRedirect(string $redirectUri, string $code, string $state): RedirectResponse { $query = ['code' => $code]; if ($state !== '') { $query['state'] = $state; } return redirect()->away($redirectUri.(str_contains($redirectUri, '?') ? '&' : '?').http_build_query($query)); } /** * @param array $query */ private function appendQueryToFrontendUrl(string $url, array $query): string { $queryString = http_build_query($query); if (! str_contains($url, '#')) { return $url.(str_contains($url, '?') ? '&' : '?').$queryString; } [$base, $fragment] = explode('#', $url, 2); $separator = str_contains($fragment, '?') ? '&' : '?'; return $base.'#'.$fragment.$separator.$queryString; } private function authorizationError(string $redirectUri, string $state, string $error, string $description): RedirectResponse|JsonResponse { if ($this->isValidAbsoluteUrl($redirectUri)) { $query = ['error' => $error, 'error_description' => $description]; if ($state !== '') { $query['state'] = $state; } return redirect()->away($redirectUri.(str_contains($redirectUri, '?') ? '&' : '?').http_build_query($query)); } return response()->json([ 'error' => $error, 'error_description' => $description, ], 400); } private function isValidAbsoluteUrl(string $url): bool { if ($url === '') { return false; } $parsed = parse_url($url); if (! is_array($parsed)) { return false; } return isset($parsed['scheme'], $parsed['host']); } private function resolveResourceOwner(Request $request): ?User { $accessToken = trim((string) ( $request->bearerToken() ?: $request->input('access_token', $request->query('access_token', '')) )); if ($accessToken === '') { return null; } try { $user = JWTAuth::setToken($accessToken)->authenticate(); } catch (\Throwable) { return null; } return $user instanceof User ? $user : null; } private function writeOAuthAuthorizationLog(User $user, Request $request, OauthClient $client, bool $autoApproved): void { AccessLog::query()->create([ 'user_id' => $user->id, 'server_resource_id' => null, 'bastion_account_id' => null, 'protocol' => 'oauth2', 'action' => 'oauth_authorization', 'requested_at' => now(), 'metadata' => [ 'path' => $request->path(), 'method' => $request->method(), 'client_ip' => $request->ip(), 'oauth_client_id' => $client->id, 'oauth_client_name' => $client->name, 'oauth_client_identifier' => $client->client_id, 'redirect_uri' => (string) $request->input('redirect_uri', $request->query('redirect_uri', '')), 'auto_approved' => $autoApproved, ], ]); } }