You've already forked php-auth
generated from siteworxpro/Php-Template
Compare commits
8 Commits
8dbf3c22b6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
f0e191b2cb
|
|||
|
75757f1403
|
|||
|
f28d8f2ec8
|
|||
|
b4c892c104
|
|||
|
5ec683890e
|
|||
|
eaff081e44
|
|||
|
8f5f57f5f6
|
|||
|
96409973bf
|
@@ -1,3 +1,4 @@
|
||||
APP_URL: https://localhost
|
||||
JWT_ISSUER: https://auth.siteworxpro.com/application/o/postman/
|
||||
JWT_AUDIENCE: 1RWyqJFlyA4hmsDzq6kSxs0LXvk7UgEAfgmBCpQ9
|
||||
JWT_SIGNING_KEY: https://auth.siteworxpro.com/application/o/postman/.well-known/openid-configuration
|
||||
|
||||
@@ -105,6 +105,10 @@ create table user_scopes
|
||||
constraint user_scopes_scope_id_fk
|
||||
references scopes
|
||||
on delete cascade,
|
||||
client_id VARCHAR(26) not null
|
||||
constraint user_scopes_client_id_fk
|
||||
references clients
|
||||
on delete cascade,
|
||||
constraint user_scopes_user_id_scope_id_key
|
||||
unique (user_id, scope_id)
|
||||
unique (user_id, scope_id, client_id)
|
||||
);
|
||||
@@ -3,7 +3,7 @@ create table audit_logs
|
||||
id VARCHAR(26) not null
|
||||
constraint audit_logs_pkey
|
||||
primary key,
|
||||
user_id integer,
|
||||
user_id varchar(26) default null,
|
||||
action varchar(255) not null,
|
||||
timestamp timestamptz default current_timestamp,
|
||||
details jsonb
|
||||
|
||||
@@ -14,6 +14,7 @@ use Siteworxpro\App\Controllers\AuthorizeController;
|
||||
use Siteworxpro\App\Controllers\CapabilitiesController;
|
||||
use Siteworxpro\App\Controllers\HealthcheckController;
|
||||
use Siteworxpro\App\Controllers\OpenIdController;
|
||||
use Siteworxpro\App\Controllers\UserInfo;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
|
||||
use Siteworxpro\App\Http\Middleware\JwtMiddleware;
|
||||
@@ -89,6 +90,8 @@ class Api
|
||||
$group->get('/.well-known/openid-configuration', OpenIdController::class . '::get');
|
||||
});
|
||||
|
||||
$this->router->get('/user_info', UserInfo::class . '::get');
|
||||
|
||||
$this->router->middleware(new CorsMiddleware());
|
||||
$this->router->middleware(new JwtMiddleware());
|
||||
$this->router->middleware(new ScopeMiddleware());
|
||||
|
||||
@@ -16,7 +16,7 @@ readonly class Scope
|
||||
*/
|
||||
public function __construct(
|
||||
private array $scopes = [],
|
||||
private string $claim = 'scope',
|
||||
private string $claim = 'scopes',
|
||||
private string $separator = ' '
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Siteworxpro\App\Cli\Commands\OAuth\AddRedirectUri;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\AddScope;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\ClientCapabilities;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\CreateClient;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\DeleteClient;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\ListClients;
|
||||
use Siteworxpro\App\Cli\Commands\Queue\Start;
|
||||
use Siteworxpro\App\Cli\Commands\User\Add;
|
||||
@@ -42,6 +43,7 @@ class App
|
||||
$this->app->addCommand(new AddScope());
|
||||
$this->app->addCommand(new ResetPassword());
|
||||
$this->app->addCommand(new ClientCapabilities());
|
||||
$this->app->addCommand(new DeleteClient());
|
||||
}
|
||||
|
||||
public function run(): int
|
||||
|
||||
@@ -28,13 +28,22 @@ abstract class Command extends \Symfony\Component\Console\Command\Command implem
|
||||
/**
|
||||
* @param ClimateOutput $output
|
||||
* @param ClimateOutput|ArgvInput $input
|
||||
* @param bool $allClients
|
||||
* @return Client | null
|
||||
*/
|
||||
protected function askForClient(ClimateOutput $output, ClimateOutput|ArgvInput $input): ?Client
|
||||
{
|
||||
protected function askForClient(
|
||||
ClimateOutput $output,
|
||||
ClimateOutput|ArgvInput $input,
|
||||
bool $allClients = false
|
||||
): ?Client {
|
||||
if ($allClients) {
|
||||
/** @var Collection<Client> $clients */
|
||||
$clients = Client::all(['id', 'name']);
|
||||
} else {
|
||||
/** @var Collection<Client> $clients */
|
||||
$clients = Client::whereJsonContains('grant_types', 'authorization_code')
|
||||
->get(['id', 'name']);
|
||||
}
|
||||
|
||||
if ($clients->isEmpty()) {
|
||||
$output->error('No OAuth clients available.');
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\App\OAuth\Entities\Scope;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
@@ -30,6 +32,8 @@ class ClientCapabilities extends Command
|
||||
$output->info('[1] Username Password: ' . ($capabilities['userPass'] ? 'Enabled' : 'Disabled'));
|
||||
$output->info('[2] Magic Link: ' . ($capabilities['magicLink'] ? 'Enabled' : 'Disabled'));
|
||||
$output->info('[3] Social Logins: ' . ($capabilities['socials'] ? 'Enabled' : 'Disabled'));
|
||||
$output->info('[4] External Client (require pkce): ' . (!$client->confidential ? 'Enabled' : 'Disabled'));
|
||||
$output->info('[5] Manage Scopes');
|
||||
|
||||
$question = new Question('What do you want to edit: ', '');
|
||||
$selection = $this->helper->ask($input, $output, $question);
|
||||
@@ -48,6 +52,12 @@ class ClientCapabilities extends Command
|
||||
case '3':
|
||||
$output->info('Social Logins cannot be modified via CLI at this time.');
|
||||
break;
|
||||
case '4':
|
||||
$client->confidential = !$client->confidential;
|
||||
break;
|
||||
case '5':
|
||||
$this->manageClientScopes($input, $output, $client);
|
||||
break;
|
||||
default:
|
||||
$output->error('Invalid selection. Please try again.');
|
||||
continue 2;
|
||||
@@ -60,4 +70,48 @@ class ClientCapabilities extends Command
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function manageClientScopes($input, ClimateOutput $output, Client $client): void
|
||||
{
|
||||
$allScopes = Scope::all();
|
||||
|
||||
$output->info('These are scope that are available for this client.');
|
||||
$output->info('Type "exit" to finish managing scopes.');
|
||||
|
||||
while (true) {
|
||||
$clientScopes = $client->scopes()->get();
|
||||
|
||||
$output->info('Available Scopes:');
|
||||
|
||||
/** @var Scope $scope */
|
||||
foreach ($allScopes as $scope) {
|
||||
$status = $clientScopes->contains($scope) ? 'Enabled' : 'Disabled';
|
||||
$output->info("$scope->id - $scope->name $status");
|
||||
}
|
||||
|
||||
$question = new Question('Enter scope ID to toggle (or type "exit" to finish): ', '');
|
||||
$question->setAutocompleterValues($allScopes->pluck('id')->toArray());
|
||||
|
||||
$scopeId = $this->helper->ask($input, $output, $question);
|
||||
|
||||
if (strtolower($scopeId) === 'exit') {
|
||||
break;
|
||||
}
|
||||
|
||||
$scope = $allScopes->where('id', $scopeId)->first();
|
||||
|
||||
if ($scope === null) {
|
||||
$output->error('Scope not found. Please try again.');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($clientScopes->contains($scope)) {
|
||||
$client->disableScope($scope);
|
||||
$output->info("Scope '$scope->name' disabled for client.");
|
||||
} else {
|
||||
$client->enableScope($scope);
|
||||
$output->info("Scope '$scope->name' enabled for client.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
@@ -21,7 +19,7 @@ use Symfony\Component\Console\Question\Question;
|
||||
#[AsCommand(name: 'oauth:client:create', description: 'Create a new OAuth client.')]
|
||||
class CreateClient extends \Siteworxpro\App\Cli\Commands\Command
|
||||
{
|
||||
public function __invoke($input, $output): int
|
||||
public function __invoke($input, ClimateOutput | OutputInterface $output): int
|
||||
{
|
||||
$question = new Question('Enter client name: ');
|
||||
$clientName = $this->helper->ask($input, $output, $question);
|
||||
@@ -31,8 +29,8 @@ class CreateClient extends \Siteworxpro\App\Cli\Commands\Command
|
||||
|
||||
$question = new ChoiceQuestion('Enter client grants', [
|
||||
'authorization_code',
|
||||
'client_credentials',
|
||||
'refresh_token',
|
||||
'client_credentials',
|
||||
'password',
|
||||
], 0);
|
||||
$question->setMultiselect(true);
|
||||
@@ -44,7 +42,22 @@ class CreateClient extends \Siteworxpro\App\Cli\Commands\Command
|
||||
$grantsEnum[] = ClientGrantAlias::from($grant);
|
||||
}
|
||||
|
||||
$command = new CreateClientCommand($clientName, $grantsEnum, $clientDescription);
|
||||
$isExternal = false;
|
||||
if (in_array('authorization_code', $grants)) {
|
||||
$question = $this->helper->ask(
|
||||
$input,
|
||||
$output,
|
||||
new \Symfony\Component\Console\Question\ConfirmationQuestion(
|
||||
'Require PKCE for Authorization Code grant? (y/N): ',
|
||||
false,
|
||||
'/^(y|yes)/i'
|
||||
)
|
||||
);
|
||||
|
||||
$isExternal = $question === 'y' || $question === true;
|
||||
}
|
||||
|
||||
$command = new CreateClientCommand($clientName, $grantsEnum, $clientDescription, $isExternal);
|
||||
try {
|
||||
/** @var Client $client */
|
||||
$client = CommandBus::handle($command);
|
||||
|
||||
58
src/Cli/Commands/OAuth/DeleteClient.php
Normal file
58
src/Cli/Commands/OAuth/DeleteClient.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
#[AsCommand('oauth:client:delete', 'Delete an OAuth client')]
|
||||
class DeleteClient extends Command
|
||||
{
|
||||
public function __invoke(ClimateOutput|ArgvInput $input, $output): int
|
||||
{
|
||||
$client = $this->askForClient($output, $input, true);
|
||||
|
||||
if ($client === null) {
|
||||
$output->red('No client selected, aborting.');
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::FAILURE;
|
||||
}
|
||||
|
||||
$output->red()->bold('You are about to delete the following OAuth client:');
|
||||
$output->red("ID: $client->id");
|
||||
$output->red("Name: $client->name");
|
||||
$output->red("Description: $client->description");
|
||||
|
||||
$output
|
||||
->br()
|
||||
->backgroundRed()
|
||||
->yellow()
|
||||
->bold('This action is irreversible and will remove all associated data.');
|
||||
|
||||
$question = $this->helper->ask(
|
||||
$input,
|
||||
$output,
|
||||
new \Symfony\Component\Console\Question\ConfirmationQuestion(
|
||||
'Are you sure you want to proceed? (y/N): ',
|
||||
false,
|
||||
'/^(y|yes)/i'
|
||||
)
|
||||
);
|
||||
|
||||
if (!$question) {
|
||||
$output->info('Operation cancelled by user.');
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
|
||||
$client->delete();
|
||||
|
||||
$output->green('OAuth client deleted successfully.');
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,8 @@ class ListClients extends Command
|
||||
'Access Token Url' => Config::get('app.url') . '/client/access_token',
|
||||
'OAuth Config Url' => Config::get('app.url') .
|
||||
'/client/' . $client->id . '/.well-known/openid-configuration',
|
||||
'Scopes' => $client->scopes->toArray()
|
||||
'Scopes' => $client->scopes->pluck('name')->toArray(),
|
||||
'Capabilities' => $client->capabilities->toArray(),
|
||||
]);
|
||||
|
||||
return self::SUCCESS;
|
||||
|
||||
@@ -20,7 +20,7 @@ use Symfony\Component\Console\Question\Question as QuestionInput;
|
||||
#[AsCommand('user:add', 'Add a new user associated with an OAuth client')]
|
||||
class Add extends Command
|
||||
{
|
||||
public function __invoke(ClimateOutput|ArgvInput|InputInterface $input, ClimateOutput $output): int
|
||||
public function __invoke(ClimateOutput|ArgvInput|InputInterface $input, $output): int
|
||||
{
|
||||
$client = $this->askForClient($output, $input);
|
||||
if (!$client) {
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Siteworxpro\App\Cli\Commands\User;
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Siteworxpro\App\CommandBus\Commands\SendPasswordReset;
|
||||
use Siteworxpro\App\Mailer\Message;
|
||||
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
@@ -41,9 +42,13 @@ class ResetPassword extends Command
|
||||
}
|
||||
|
||||
if ($input->getOption('send-email')) {
|
||||
CommandBus::handle(new SendPasswordReset($user, $client));
|
||||
/** @var Message $message */
|
||||
$message = CommandBus::handle(new SendPasswordReset($user, $client));
|
||||
$output->info('Password reset email sent to the user.');
|
||||
|
||||
$output->info('Email Subject: ' . $message->getSubject());
|
||||
$output->info('Email Body: ' . $message->getBody());
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ readonly class CreateClient extends Command
|
||||
* @param string $clientName
|
||||
* @param array<ClientGrant | mixed> $clientGrants
|
||||
* @param string $clientDescription
|
||||
* @param bool $isExternal
|
||||
*/
|
||||
public function __construct(
|
||||
private string $clientName,
|
||||
private array $clientGrants = [],
|
||||
private string $clientDescription = ''
|
||||
private string $clientDescription = '',
|
||||
private bool $isExternal = false
|
||||
) {
|
||||
foreach ($this->clientGrants as $grant) {
|
||||
if ($grant instanceof ClientGrant === false) {
|
||||
@@ -32,6 +34,14 @@ readonly class CreateClient extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isExternal(): bool
|
||||
{
|
||||
return $this->isExternal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -16,11 +16,13 @@ class CreateUserHandler
|
||||
{
|
||||
$user = User::create([
|
||||
'email' => $command->getEmail(),
|
||||
'password' => password_hash($command->getPassword(), PASSWORD_BCRYPT),
|
||||
'first_name' => $command->getFirstName(),
|
||||
'last_name' => $command->getLastName(),
|
||||
]);
|
||||
|
||||
$user->password = $command->getPassword();
|
||||
$user->save();
|
||||
|
||||
$clientUser = new ClientUser();
|
||||
$clientUser->client_id = $command->getClient()->id;
|
||||
$clientUser->user_id = $user->id;
|
||||
|
||||
@@ -26,6 +26,7 @@ class CreateClient extends CommandHandler
|
||||
$client->description = $command->getClientDescription();
|
||||
$client->grant_types = new Collection($command->getClientGrants()); // @phpstan-ignore-line assign.propertyType
|
||||
$client->capabilities = new ClientCapabilities();
|
||||
$client->confidential = !$command->isExternal();
|
||||
|
||||
$client->save();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use Siteworxpro\App\CommandBus\Commands\SendPasswordReset;
|
||||
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
||||
use Siteworxpro\App\Helpers\Rand;
|
||||
use Siteworxpro\App\Mailer\Message;
|
||||
use Siteworxpro\App\Models\ClientRedirectUri;
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Mailer;
|
||||
@@ -23,7 +24,7 @@ class SendPasswordResetHandler extends CommandHandler
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function __invoke(Command|SendPasswordReset $command): User
|
||||
public function __invoke(Command|SendPasswordReset $command): Message
|
||||
{
|
||||
if (!$command instanceof SendPasswordReset) {
|
||||
throw new CommandHandlerException('Invalid command type provided to handler.');
|
||||
@@ -31,15 +32,28 @@ class SendPasswordResetHandler extends CommandHandler
|
||||
|
||||
$token = Rand::string(64);
|
||||
|
||||
/** @var ClientRedirectUri $redirectUri */
|
||||
$redirectUri = $command->getClient()->clientRedirectUris->first();
|
||||
|
||||
$content = Twig::render('password-reset.twig', [
|
||||
'user' => $command->getUser(),
|
||||
'resetLink' => Config::get('app.url') . '/reset-password?token=' . $token,
|
||||
'resetLink' => sprintf(
|
||||
'%s/authorize?token=%s&client_id=%s&response_type=code&redirect_uri=%s#/password-reset',
|
||||
Config::get('app.url'),
|
||||
$token,
|
||||
$command->getClient()->client_id,
|
||||
urlencode($redirectUri->redirect_uri)
|
||||
),
|
||||
'client' => $command->getClient()
|
||||
]);
|
||||
|
||||
$from = $command->getClient()->capabilities->toArray()['support_email'] != ''
|
||||
? $command->getClient()->capabilities->toArray()['support_email']
|
||||
: Config::get('app.default_support_email');
|
||||
|
||||
$message = new Message(
|
||||
$command->getUser()->email,
|
||||
$command->getClient()->capabilities->toArray()['support_email'] ?? Config::get('app.default_support_email'),
|
||||
$from,
|
||||
'Password Reset Request',
|
||||
$content
|
||||
);
|
||||
@@ -50,6 +64,6 @@ class SendPasswordResetHandler extends CommandHandler
|
||||
|
||||
Redis::set('password_reset:' . $command->getUser()->id, $token, 'EX', Redis::MINUTE * 15);
|
||||
|
||||
return $command->getUser();
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,12 @@ final class AccessTokenController extends Controller
|
||||
/** @var Response $response */
|
||||
$response = $client
|
||||
->getAuthorizationServer()
|
||||
->respondToAccessTokenRequest($request, JsonResponseFactory::createJsonResponse([]));
|
||||
->respondToAccessTokenRequest(
|
||||
$request,
|
||||
JsonResponseFactory::createJsonResponse([])
|
||||
);
|
||||
|
||||
Dispatcher::push(new Issued($response));
|
||||
Dispatcher::push(new Issued($client, $response));
|
||||
|
||||
return $response;
|
||||
} catch (OAuthServerException $e) {
|
||||
|
||||
@@ -12,7 +12,6 @@ use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
|
||||
use Nyholm\Psr7\Response;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Nyholm\Psr7\Stream;
|
||||
use Siteworxpro\App\Events\Login\LoginAttempt;
|
||||
use Siteworxpro\App\Events\Login\LoginFailed;
|
||||
use Siteworxpro\App\Events\Login\LoginSuccess;
|
||||
use Siteworxpro\App\Helpers\Rand;
|
||||
@@ -35,8 +34,6 @@ final class AuthorizeController extends Controller
|
||||
*/
|
||||
public function post(ServerRequest $request): Response
|
||||
{
|
||||
Dispatcher::push(new LoginAttempt($request));
|
||||
|
||||
$s = $request->getCookieParams()['s'] ?? '';
|
||||
|
||||
$password = $request->getParsedBody()['password'] ?? '';
|
||||
@@ -69,7 +66,7 @@ final class AuthorizeController extends Controller
|
||||
$user = $client->loginUser($email, $password);
|
||||
|
||||
if (!$user) {
|
||||
Dispatcher::push(new LoginFailed($request));
|
||||
Dispatcher::push(new LoginFailed($request, $client));
|
||||
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'success' => false,
|
||||
@@ -84,7 +81,7 @@ final class AuthorizeController extends Controller
|
||||
|
||||
Redis::del('session:' . $s);
|
||||
|
||||
Dispatcher::push(new LoginSuccess($request, $user));
|
||||
Dispatcher::push(new LoginSuccess($request, $client, $user));
|
||||
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'success' => true,
|
||||
|
||||
60
src/Controllers/UserInfo.php
Normal file
60
src/Controllers/UserInfo.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Controllers;
|
||||
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||
use Siteworxpro\App\Attributes\Guards\Scope;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Http\Responses\NotFoundResponse;
|
||||
use Siteworxpro\App\Models\User;
|
||||
|
||||
class UserInfo extends Controller
|
||||
{
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
#[Scope(['profile', 'email', 'openid'])]
|
||||
#[Jwt]
|
||||
public function get(ServerRequest $request): ResponseInterface
|
||||
{
|
||||
$userId = $request->getAttribute('sub');
|
||||
|
||||
/** @var User | null $user */
|
||||
$user = User::find($userId);
|
||||
|
||||
if (!$user) {
|
||||
return JsonResponseFactory::createJsonResponse(
|
||||
new NotFoundResponse($request->getUri()->getPath())
|
||||
);
|
||||
}
|
||||
|
||||
$responseData = [];
|
||||
$scopes = $request->getAttribute('scopes', []);
|
||||
|
||||
$responseData['id'] = $user->id;
|
||||
|
||||
if (in_array('profile', $scopes, true)) {
|
||||
$responseData['name'] = $user->first_name . ' ' . $user->last_name;
|
||||
$responseData['given_name'] = $user->first_name;
|
||||
$responseData['middle_name'] = ''; // todo
|
||||
$responseData['family_name'] = $user->last_name;
|
||||
$responseData['nickname'] = ''; // todo
|
||||
$responseData['preferred_username'] = substr($user->first_name, 0, 1) . $user->last_name;
|
||||
$responseData['picture'] = ''; // todo
|
||||
$responseData['birthdate'] = ''; // todo
|
||||
$responseData['phone_number'] = ''; // todo
|
||||
$responseData['phone_number_verified'] = false; // todo
|
||||
}
|
||||
|
||||
if (in_array('email', $scopes, true)) {
|
||||
$responseData['email'] = $user->email;
|
||||
$responseData['email_verified'] = false; // todo
|
||||
}
|
||||
|
||||
return JsonResponseFactory::createJsonResponse($responseData);
|
||||
}
|
||||
}
|
||||
42
src/EventListeners/AccessTokenIssuedListener.php
Normal file
42
src/EventListeners/AccessTokenIssuedListener.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\EventListeners;
|
||||
|
||||
use Siteworxpro\App\Attributes\Events\ListensFor;
|
||||
use Siteworxpro\App\Events\AccessToken\Issued as IssuedEvent;
|
||||
use Siteworxpro\App\Models\AuditLog;
|
||||
use Siteworxpro\App\Models\Enums\AuditLogAction;
|
||||
|
||||
#[ListensFor(IssuedEvent::class)]
|
||||
class AccessTokenIssuedListener extends Listener
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param string | IssuedEvent $event
|
||||
* @param array $payload
|
||||
* @return AuditLog|null
|
||||
*/
|
||||
public function __invoke(mixed $event, array $payload = []): ?AuditLog
|
||||
{
|
||||
if (is_string($event)) {
|
||||
$event = $payload[0] ?? null;
|
||||
}
|
||||
|
||||
if (!$event instanceof IssuedEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AuditLog::create([
|
||||
'user_id' => null,
|
||||
'action' => AuditLogAction::TOKEN_ISSUED,
|
||||
'details' => [
|
||||
'response_status' => $event->getResponse()->getStatusCode(),
|
||||
'client_id' => $event->getClient()->getIdentifier(),
|
||||
'client_name' => $event->getClient()->getName(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ use Siteworxpro\App\Models\AuditLog;
|
||||
use Siteworxpro\App\Models\Enums\AuditLogAction;
|
||||
|
||||
#[ListensFor(\Siteworxpro\App\Events\Login\LoginFailed::class)]
|
||||
class LoginFailed extends Listener
|
||||
#[ListensFor(\Siteworxpro\App\Events\Login\LoginSuccess::class)]
|
||||
class LoginListener extends Listener
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
@@ -24,15 +25,24 @@ class LoginFailed extends Listener
|
||||
$event = $payload[0] ?? null;
|
||||
}
|
||||
|
||||
if (!$event instanceof \Siteworxpro\App\Events\Login\LoginFailed) {
|
||||
if (
|
||||
!$event instanceof \Siteworxpro\App\Events\Login\LoginFailed
|
||||
&& !$event instanceof \Siteworxpro\App\Events\Login\LoginSuccess
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$action = $event instanceof \Siteworxpro\App\Events\Login\LoginSuccess
|
||||
? AuditLogAction::LOGIN_SUCCESS
|
||||
: AuditLogAction::LOGIN_FAIL;
|
||||
|
||||
return AuditLog::create([
|
||||
'user_id' => $event->getUser()?->id,
|
||||
'action' => AuditLogAction::LOGIN_FAIL,
|
||||
'action' => $action,
|
||||
'details' => [
|
||||
'username_attempted' => $event->getUsernameAttempted(),
|
||||
'client_id' => $event->getClient()->client_id,
|
||||
'client_name' => $event->getClient()->name,
|
||||
'username' => $event->getUsernameAttempted(),
|
||||
'ip_address' => $event->getRequestIp(),
|
||||
],
|
||||
]);
|
||||
@@ -5,10 +5,11 @@ declare(strict_types=1);
|
||||
namespace Siteworxpro\App\Events\AccessToken;
|
||||
|
||||
use Nyholm\Psr7\Response;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
readonly class Issued
|
||||
{
|
||||
public function __construct(private Response $response)
|
||||
public function __construct(private Client $client, private Response $response)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -16,4 +17,9 @@ readonly class Issued
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function getClient(): Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,56 @@ declare(strict_types=1);
|
||||
namespace Siteworxpro\App\Events\Login;
|
||||
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
readonly class LoginAttempt
|
||||
abstract class LoginAttempt
|
||||
{
|
||||
public function __construct(
|
||||
private ServerRequest $request,
|
||||
private readonly ServerRequest $request,
|
||||
private readonly Client $client,
|
||||
private readonly ?User $user = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
public function getRequestIp(): string
|
||||
{
|
||||
if ($this->request->getHeader('X-Forwarded-For')) {
|
||||
$ipAddresses = explode(',', $this->request->getHeaderLine('X-Forwarded-For'));
|
||||
return trim($ipAddresses[0]);
|
||||
}
|
||||
|
||||
if ($this->request->getHeader('X-Real-IP')) {
|
||||
return $this->request->getHeaderLine('X-Real-IP');
|
||||
}
|
||||
|
||||
if ($this->request->getServerParams()['HTTP_CLIENT_IP'] ?? false) {
|
||||
return $this->request->getServerParams()['HTTP_CLIENT_IP'];
|
||||
}
|
||||
|
||||
if ($this->request->getServerParams()['HTTP_X_FORWARDED_FOR'] ?? false) {
|
||||
$ipAddresses = explode(',', $this->request->getServerParams()['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($ipAddresses[0]);
|
||||
}
|
||||
|
||||
return $this->request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
|
||||
}
|
||||
|
||||
public function getUsernameAttempted(): string
|
||||
{
|
||||
return $this->request->getParsedBody()['email'] ?? '';
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Client
|
||||
*/
|
||||
public function getClient(): Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,44 +7,6 @@ namespace Siteworxpro\App\Events\Login;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Siteworxpro\App\Models\User;
|
||||
|
||||
readonly class LoginFailed
|
||||
class LoginFailed extends LoginAttempt
|
||||
{
|
||||
public function __construct(
|
||||
private ServerRequest $request,
|
||||
private ?User $user = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRequestIp(): string
|
||||
{
|
||||
if ($this->request->getHeader('X-Forwarded-For')) {
|
||||
$ipAddresses = explode(',', $this->request->getHeaderLine('X-Forwarded-For'));
|
||||
return trim($ipAddresses[0]);
|
||||
}
|
||||
|
||||
if ($this->request->getHeader('X-Real-IP')) {
|
||||
return $this->request->getHeaderLine('X-Real-IP');
|
||||
}
|
||||
|
||||
if ($this->request->getServerParams()['HTTP_CLIENT_IP'] ?? false) {
|
||||
return $this->request->getServerParams()['HTTP_CLIENT_IP'];
|
||||
}
|
||||
|
||||
if ($this->request->getServerParams()['HTTP_X_FORWARDED_FOR'] ?? false) {
|
||||
$ipAddresses = explode(',', $this->request->getServerParams()['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($ipAddresses[0]);
|
||||
}
|
||||
|
||||
return $this->request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
|
||||
}
|
||||
|
||||
public function getUsernameAttempted(): string
|
||||
{
|
||||
return $this->request->getParsedBody()['email'] ?? '';
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,6 @@ namespace Siteworxpro\App\Events\Login;
|
||||
use Nyholm\Psr7\ServerRequest;
|
||||
use Siteworxpro\App\Models\User;
|
||||
|
||||
readonly class LoginSuccess
|
||||
class LoginSuccess extends LoginAttempt
|
||||
{
|
||||
public function __construct(
|
||||
private ServerRequest $serverRequest,
|
||||
private User $user
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequest
|
||||
{
|
||||
return $this->serverRequest;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use Carbon\Carbon;
|
||||
use Carbon\WrapperClock;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Lcobucci\JWT\JwtFacade;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256 as Hmac256;
|
||||
use Lcobucci\JWT\Signer\Key;
|
||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||
use Lcobucci\JWT\Signer\Rsa\Sha256;
|
||||
use Lcobucci\JWT\Token\InvalidTokenStructure;
|
||||
@@ -26,9 +24,8 @@ use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Siteworxpro\App\Attributes\Guards\Jwt;
|
||||
use Siteworxpro\App\Controllers\Controller;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Guzzle;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
|
||||
/**
|
||||
@@ -91,7 +88,6 @@ class JwtMiddleware extends Middleware
|
||||
|
||||
// Extract Bearer token from Authorization header.
|
||||
$token = str_replace('Bearer ', '', $request->getHeaderLine('Authorization'));
|
||||
|
||||
if (empty($token)) {
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 401,
|
||||
@@ -99,31 +95,39 @@ class JwtMiddleware extends Middleware
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Aggregate required issuers and audience from attributes.
|
||||
$requiredIssuers = [];
|
||||
$requiredAudience = '';
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var Jwt $jwtInstance */
|
||||
$jwtInstance = $attribute->newInstance();
|
||||
|
||||
if ($jwtInstance->getAudience() !== '') {
|
||||
$requiredAudience = $jwtInstance->getAudience();
|
||||
$jwt = explode('.', $token);
|
||||
if (count($jwt) !== 3) {
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 401,
|
||||
'message' => 'Unauthorized: Invalid token format',
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$requiredIssuers[] = $jwtInstance->getIssuer();
|
||||
$payload = json_decode(base64_decode($jwt[1]), true, 512, JSON_THROW_ON_ERROR);
|
||||
$clientId = str_replace(Config::get('app.url') . '/', '', $payload['iss'] ?? '');
|
||||
|
||||
$client = Client::find($clientId);
|
||||
if (!$client) {
|
||||
return JsonResponseFactory::createJsonResponse([
|
||||
'status_code' => 401,
|
||||
'message' => 'Unauthorized: Invalid token issuer',
|
||||
], CodesEnum::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$iss = Config::get('app.url') . '/' . $client->id;
|
||||
$aud = $clientId;
|
||||
|
||||
try {
|
||||
// Parse and validate the token with signature, time, issuer and audience constraints.
|
||||
$key = InMemory::plainText($client->publicKey());
|
||||
$jwt = new JwtFacade()->parse(
|
||||
$token,
|
||||
$this->getSignedWith($token),
|
||||
new SignedWith(new Sha256(), $key),
|
||||
Config::get('jwt.strict_validation') ?
|
||||
new StrictValidAt(new WrapperClock(Carbon::now())) :
|
||||
new LooseValidAt(new WrapperClock(Carbon::now())),
|
||||
new IssuedBy(...$requiredIssuers),
|
||||
new PermittedFor($requiredAudience)
|
||||
new IssuedBy($iss),
|
||||
new PermittedFor($aud)
|
||||
);
|
||||
} catch (RequiredConstraintsViolated $exception) {
|
||||
// Collect human-readable violations to return to the client.
|
||||
@@ -159,164 +163,4 @@ class JwtMiddleware extends Middleware
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the signature validation constraint from configured key.
|
||||
*
|
||||
* - If the configured key content includes the string `PUBLIC KEY`, use RSA SHA-256.
|
||||
* - Otherwise assume an HMAC SHA-256 shared secret.
|
||||
* - Supports raw key strings or `file://` paths.
|
||||
*
|
||||
* @return SignedWith Signature constraint used during JWT parsing.
|
||||
*
|
||||
* @throws \RuntimeException When no signing key is configured.
|
||||
* @throws \JsonException
|
||||
*/
|
||||
private function getSignedWith(string $token): SignedWith
|
||||
{
|
||||
$keyConfig = Config::get('jwt.signing_key');
|
||||
|
||||
if ($keyConfig === null) {
|
||||
throw new \RuntimeException('JWT signing key is not configured.');
|
||||
}
|
||||
|
||||
// file:// path to key
|
||||
if (str_starts_with($keyConfig, 'file://')) {
|
||||
$key = InMemory::file(substr($keyConfig, 7));
|
||||
// openid jwks url
|
||||
} elseif (str_contains($keyConfig, '.well-known/')) {
|
||||
$jwt = explode('.', $token);
|
||||
if (count($jwt) !== 3) {
|
||||
throw new InvalidTokenStructure('Invalid JWT structure for JWKS key retrieval.');
|
||||
}
|
||||
$header = json_decode(base64_decode($jwt[0]), true, 512, JSON_THROW_ON_ERROR);
|
||||
$keyId = $header['kid'] ?? '0'; // Default to '0' if no kid present
|
||||
$key = $this->getJwksKey($keyConfig, $keyId);
|
||||
} else {
|
||||
$key = InMemory::plainText($keyConfig);
|
||||
}
|
||||
|
||||
// Heuristic: if PEM public key content is detected, use RSA; otherwise use HMAC.
|
||||
if (str_contains($key->contents(), 'PUBLIC KEY')) {
|
||||
return new SignedWith(new Sha256(), $key);
|
||||
}
|
||||
|
||||
return new SignedWith(new Hmac256(), $key);
|
||||
}
|
||||
|
||||
private function getJwksKey(string $url, string $keyId): Key
|
||||
{
|
||||
$cached = Redis::get('jwks_key_' . $keyId);
|
||||
if ($cached !== null) {
|
||||
return InMemory::plainText($cached);
|
||||
}
|
||||
|
||||
$openIdConfig = Guzzle::get($url);
|
||||
$body = json_decode($openIdConfig->getBody()->getContents(), true, JSON_THROW_ON_ERROR);
|
||||
$jwksUri = $body['jwks_uri'] ?? '';
|
||||
if (empty($jwksUri)) {
|
||||
throw new \RuntimeException('JWKS URI not found in OpenID configuration.');
|
||||
}
|
||||
|
||||
$jwksResponse = Guzzle::get($jwksUri);
|
||||
$jwksBody = json_decode(
|
||||
$jwksResponse->getBody()->getContents(),
|
||||
true,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
|
||||
// For simplicity, we take the first key in the JWKS.
|
||||
$firstKey = array_filter(
|
||||
$jwksBody['keys'],
|
||||
fn($key) => $key['kid'] === $keyId
|
||||
)[0] ?? $jwksBody['keys'][0] ?? null;
|
||||
|
||||
if (empty($firstKey)) {
|
||||
throw new \RuntimeException('No matching key found in JWKS for key ID: ' . $keyId);
|
||||
}
|
||||
|
||||
$n = $firstKey['n'];
|
||||
$e = $firstKey['e'];
|
||||
$publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" .
|
||||
chunk_split(base64_encode($this->convertJwkToPem($n, $e)), 64) .
|
||||
"-----END PUBLIC KEY-----\n";
|
||||
|
||||
Redis::set('jwks_key_' . $keyId, $publicKeyPem, 'EX', 3600);
|
||||
|
||||
return InMemory::plainText($publicKeyPem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a DER-encoded SubjectPublicKeyInfo from JWK 'n' and 'e'.
|
||||
* Returns raw DER bytes; caller base64-encodes and wraps with PEM headers.
|
||||
*/
|
||||
private function convertJwkToPem(string $n, string $e): string
|
||||
{
|
||||
$modulus = $this->base64UrlDecode($n);
|
||||
$exponent = $this->base64UrlDecode($e);
|
||||
|
||||
$derN = $this->derEncodeInteger($modulus);
|
||||
$derE = $this->derEncodeInteger($exponent);
|
||||
|
||||
// RSAPublicKey (PKCS#1): SEQUENCE { n INTEGER, e INTEGER }
|
||||
$rsaPublicKey = $this->derEncodeSequence($derN . $derE);
|
||||
|
||||
// AlgorithmIdentifier for rsaEncryption: 1.2.840.113549.1.1.1 with NULL
|
||||
$algId = hex2bin('300d06092a864886f70d0101010500');
|
||||
|
||||
// SubjectPublicKey (SPKI) BIT STRING, 0 unused bits + RSAPublicKey
|
||||
$subjectPublicKey = $this->derEncodeBitString($rsaPublicKey);
|
||||
|
||||
// SubjectPublicKeyInfo: SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }
|
||||
return $this->derEncodeSequence($algId . $subjectPublicKey);
|
||||
}
|
||||
|
||||
private function base64UrlDecode(string $data): string
|
||||
{
|
||||
$data = strtr($data, '-_', '+/');
|
||||
$pad = strlen($data) % 4;
|
||||
if ($pad) {
|
||||
$data .= str_repeat('=', 4 - $pad);
|
||||
}
|
||||
return base64_decode($data);
|
||||
}
|
||||
|
||||
private function derEncodeLength(int $len): string
|
||||
{
|
||||
if ($len < 0x80) {
|
||||
return chr($len);
|
||||
}
|
||||
$bytes = '';
|
||||
while ($len > 0) {
|
||||
$bytes = chr($len & 0xFF) . $bytes;
|
||||
$len >>= 8;
|
||||
}
|
||||
return chr(0x80 | strlen($bytes)) . $bytes;
|
||||
}
|
||||
|
||||
private function derEncodeInteger(string $bytes): string
|
||||
{
|
||||
// Remove leading zeroes
|
||||
$bytes = ltrim($bytes, "\x00");
|
||||
if ($bytes === '') {
|
||||
$bytes = "\x00";
|
||||
}
|
||||
// Ensure positive INTEGER (prepend 0x00 if MSB set)
|
||||
if ((ord($bytes[0]) & 0x80) !== 0) {
|
||||
$bytes = "\x00" . $bytes;
|
||||
}
|
||||
return "\x02" . $this->derEncodeLength(strlen($bytes)) . $bytes;
|
||||
}
|
||||
|
||||
private function derEncodeSequence(string $bytes): string
|
||||
{
|
||||
return "\x30" . $this->derEncodeLength(strlen($bytes)) . $bytes;
|
||||
}
|
||||
|
||||
private function derEncodeBitString(string $bytes): string
|
||||
{
|
||||
// 0 unused bits + data
|
||||
$payload = "\x00" . $bytes;
|
||||
return "\x03" . $this->derEncodeLength(strlen($payload)) . $payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<p>Dear {{ user.firstName }},</p>
|
||||
<p>{{ user.first_name }},</p>
|
||||
|
||||
<p>We received a request to reset your password. Click the link below to set a new password:</p>
|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@ namespace Siteworxpro\App\Models;
|
||||
*/
|
||||
class ClientScope extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@ enum AuditLogAction: string
|
||||
case LOGIN_SUCCESS = 'login_success';
|
||||
case LOGIN_FAIL = 'login_fail';
|
||||
case LOGOUT = 'logout';
|
||||
|
||||
case TOKEN_ISSUED = 'token_issued';
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\EntityTrait;
|
||||
use League\OAuth2\Server\Grant\ClientCredentialsGrant as ClientCredentialsGrant;
|
||||
use Random\RandomException;
|
||||
use Siteworxpro\App\Helpers\Rand;
|
||||
use Siteworxpro\App\Models\ClientRedirectUri;
|
||||
@@ -195,6 +196,9 @@ class Client extends Model implements ClientEntityInterface
|
||||
Key::loadFromAsciiSafeString($this->encryption_key)
|
||||
);
|
||||
|
||||
$accessTokenTtl = $this->capabilities->toArray()['tokenSettings']['accessTokenTTL'] ?? 'PT1H';
|
||||
$refreshTokenTtl = $this->capabilities->toArray()['tokenSettings']['refreshTokenTTL'] ?? 'P1M';
|
||||
|
||||
if (!empty($this->grant_types)) {
|
||||
foreach ($this->grant_types as $grantType) {
|
||||
switch ($grantType) {
|
||||
@@ -204,22 +208,22 @@ class Client extends Model implements ClientEntityInterface
|
||||
new \Siteworxpro\App\OAuth\RefreshTokenRepository(),
|
||||
new \DateInterval('PT10M')
|
||||
);
|
||||
$grant->setRefreshTokenTTL(new \DateInterval('P1M'));
|
||||
$grant->setRefreshTokenTTL(new \DateInterval($refreshTokenTtl));
|
||||
break;
|
||||
case 'client_credentials':
|
||||
$grant = new \League\OAuth2\Server\Grant\ClientCredentialsGrant();
|
||||
$grant = new ClientCredentialsGrant();
|
||||
break;
|
||||
case 'refresh_token':
|
||||
$grant = new \League\OAuth2\Server\Grant\RefreshTokenGrant(
|
||||
new \Siteworxpro\App\OAuth\RefreshTokenRepository()
|
||||
);
|
||||
$grant->setRefreshTokenTTL(new \DateInterval('P1M'));
|
||||
$grant->setRefreshTokenTTL(new \DateInterval($refreshTokenTtl));
|
||||
break;
|
||||
default:
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$authorizationServer->enableGrantType($grant);
|
||||
$authorizationServer->enableGrantType($grant, new \DateInterval($accessTokenTtl));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,4 +241,32 @@ class Client extends Model implements ClientEntityInterface
|
||||
|
||||
return $user->verifyPassword($password) ? $user : null;
|
||||
}
|
||||
|
||||
public function disableScope(Scope $scope): void
|
||||
{
|
||||
/** @var ClientScope | null $clientScope */
|
||||
$clientScope = ClientScope::where('client_id', $this->id)
|
||||
->where('scope_id', $scope->id)
|
||||
->first();
|
||||
|
||||
$clientScope?->delete();
|
||||
}
|
||||
|
||||
public function enableScope(Scope $scope): void
|
||||
{
|
||||
$clientScope = new ClientScope();
|
||||
$clientScope->client_id = $this->id;
|
||||
$clientScope->scope_id = $scope->id;
|
||||
|
||||
$clientScope->save();
|
||||
}
|
||||
|
||||
// convert private key to public rsa pem key and return it
|
||||
public function publicKey(): string
|
||||
{
|
||||
$res = openssl_pkey_get_private($this->private_key);
|
||||
$details = openssl_pkey_get_details($res);
|
||||
|
||||
return $details['key'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ class ClientCapabilities implements Arrayable
|
||||
'logoUrl' => null,
|
||||
];
|
||||
|
||||
private array $tokenSettings = [
|
||||
'accessTokenTTL' => 'PT1H',
|
||||
'refreshTokenTTL' => 'P1M',
|
||||
];
|
||||
|
||||
public function __construct(array $capabilities = [])
|
||||
{
|
||||
if (isset($capabilities['userPass'])) {
|
||||
@@ -47,6 +52,10 @@ class ClientCapabilities implements Arrayable
|
||||
if (isset($capabilities['support_email'])) {
|
||||
$this->support_email = (string)$capabilities['support_email'];
|
||||
}
|
||||
|
||||
if (isset($capabilities['tokenSettings']) && is_array($capabilities['tokenSettings'])) {
|
||||
$this->tokenSettings = array_merge($this->tokenSettings, $capabilities['tokenSettings']);
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromJson(string $data): self
|
||||
@@ -66,7 +75,8 @@ class ClientCapabilities implements Arrayable
|
||||
'passkey' => "bool",
|
||||
'socials' => "array",
|
||||
'branding' => "array",
|
||||
'support_email' => "string"
|
||||
'support_email' => "string",
|
||||
'tokenSettings' => "array",
|
||||
])]
|
||||
public function toArray(): array
|
||||
{
|
||||
@@ -77,6 +87,7 @@ class ClientCapabilities implements Arrayable
|
||||
'socials' => $this->socials,
|
||||
'branding' => $this->branding,
|
||||
'support_email' => $this->support_email,
|
||||
'tokenSettings' => $this->tokenSettings,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ class Scope extends Model implements ScopeEntityInterface
|
||||
{
|
||||
use ScopeTrait;
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'string',
|
||||
];
|
||||
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->name;
|
||||
|
||||
Reference in New Issue
Block a user