BastionSSO/app/Services/OAuth/OAuthClientAuthService.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();
}
}