extractClientCredentials($request); if ($clientId === '') { throw new OAuthProtocolException('invalid_client', 'Missing client_id.', 401); } if ($clientSecret === '') { throw new OAuthProtocolException('invalid_client', 'Missing client_secret.', 401); } $client = OauthClient::query() ->with(['scopes' => function ($query) { $query->where('is_active', true); }]) ->where('client_id', $clientId) ->first(); if (! $client || ! $client->is_active) { throw new OAuthProtocolException('invalid_client', 'Client authentication failed.', 401); } if (! $client->is_confidential) { throw new OAuthProtocolException('unauthorized_client', 'Only confidential clients are supported.', 400); } $secretHash = (string) ($client->client_secret_hash ?? ''); if ($secretHash === '' || ! Hash::check($clientSecret, $secretHash)) { throw new OAuthProtocolException('invalid_client', 'Client authentication failed.', 401); } return $client; } /** * @return array{0:string,1:string} */ public function extractClientCredentials(Request $request): array { $header = (string) $request->header('Authorization', ''); if (str_starts_with(strtolower($header), 'basic ')) { $encoded = trim(substr($header, 6)); $decoded = base64_decode($encoded, true); if ($decoded !== false && str_contains($decoded, ':')) { [$id, $secret] = explode(':', $decoded, 2); return [trim($id), $secret]; } } return [ trim((string) $request->input('client_id', '')), (string) $request->input('client_secret', ''), ]; } public function findActiveClientById(string $clientId): ?OauthClient { if ($clientId === '') { return null; } return OauthClient::query() ->with(['scopes' => function ($query) { $query->where('is_active', true); }]) ->where('client_id', $clientId) ->where('is_active', true) ->first(); } /** * @return array */ public function allowedScopeNames(OauthClient $client): array { return $client->scopes ->pluck('name') ->map(fn ($scope): string => trim((string) $scope)) ->filter() ->values() ->all(); } /** * @param array $requestedScopes * @param array $allowedScopes */ public function isScopeSubset(array $requestedScopes, array $allowedScopes): bool { $allowed = collect($allowedScopes)->map(fn (string $scope): string => trim($scope))->filter()->all(); foreach ($requestedScopes as $scope) { if (! in_array($scope, $allowed, true)) { return false; } } return true; } public function isRedirectUriAllowed(OauthClient $client, string $redirectUri): bool { if ($redirectUri === '') { return false; } $requested = parse_url($redirectUri); $requestedHost = strtolower((string) ($requested['host'] ?? '')); if ($requestedHost === '') { return false; } $registeredUris = collect($client->redirect_uris ?? []) ->map(fn ($uri): string => trim((string) $uri)) ->filter() ->values(); if ($registeredUris->isEmpty()) { return false; } $policy = (string) config('oauth.redirect_uri_policy', 'same_domain'); if ($policy === 'same_domain') { return $registeredUris->contains(function (string $uri) use ($requestedHost): bool { $parsed = parse_url($uri); $host = strtolower((string) ($parsed['host'] ?? '')); return $host !== '' && $host === $requestedHost; }); } return $registeredUris->contains($redirectUri); } /** * @param array $scopes */ public function buildScopeString(array $scopes): string { return collect($scopes) ->map(fn (string $scope): string => trim($scope)) ->filter() ->unique() ->values() ->implode(' '); } /** * @return array */ public function parseScopes(string $scopeValue): array { if (trim($scopeValue) === '') { return []; } return Collection::make(preg_split('/\s+/', trim($scopeValue)) ?: []) ->map(fn ($scope): string => trim((string) $scope)) ->filter() ->unique() ->values() ->all(); } }