174 lines
5.2 KiB
PHP
174 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\OAuth;
|
|
|
|
use App\Exceptions\OAuthProtocolException;
|
|
use App\Models\OauthClient;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
class OAuthClientAuthService
|
|
{
|
|
public function authenticateConfidentialClient(Request $request): OauthClient
|
|
{
|
|
[$clientId, $clientSecret] = $this->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<int, string>
|
|
*/
|
|
public function allowedScopeNames(OauthClient $client): array
|
|
{
|
|
return $client->scopes
|
|
->pluck('name')
|
|
->map(fn ($scope): string => trim((string) $scope))
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $requestedScopes
|
|
* @param array<int, string> $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<int, string> $scopes
|
|
*/
|
|
public function buildScopeString(array $scopes): string
|
|
{
|
|
return collect($scopes)
|
|
->map(fn (string $scope): string => trim($scope))
|
|
->filter()
|
|
->unique()
|
|
->values()
|
|
->implode(' ');
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
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();
|
|
}
|
|
}
|