Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/Http/Controllers/Api/UserApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ public function updateMe()
return $this->update(Auth::user()->getId());
}

public function revokeAllMyTokens()
{
if (!Auth::check())
return $this->error403();

$this->service->revokeAllGrantsOnSessionRevocation(Auth::user()->getId());
return $this->deleted();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public function updateMyPic(){
if (!Auth::check())
return $this->error403();
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
**/

use App\Http\Controllers\OpenId\DiscoveryController;
use App\Jobs\RevokeUserGrantsOnExplicitLogout;
use App\Http\Controllers\OpenId\OpenIdController;
use App\Http\Controllers\Traits\JsonResponses;
use App\Http\Utils\CountryList;
Expand Down Expand Up @@ -673,7 +674,7 @@ public function getIdentity($identifier)
public function logout()
{
$user = $this->auth_service->getCurrentUser();
//RevokeUserGrantsOnExplicitLogout::dispatch($user)->afterResponse();
// RevokeUserGrantsOnExplicitLogout::dispatch($user)->afterResponse();
$this->auth_service->logout();
Session::flush();
Session::regenerate();
Expand Down
122 changes: 122 additions & 0 deletions app/Jobs/RevokeUserGrants.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php namespace App\Jobs;
/*
* Copyright 2024 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use Auth\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use OAuth2\Services\ITokenService;
use Utils\IPHelper;

/**
* Class RevokeUserGrants
* @package App\Jobs
*/
abstract class RevokeUserGrants implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public $tries = 5;

public $timeout = 0;

/**
* @var int
*/
private int $user_id;

/**
* @var string|null
*/
private ?string $client_id;

/**
* @var string
*/
private string $reason;

/**
* @var string
*/
private string $client_ip;

/**
* @param User $user
* @param string|null $client_id null = revoke across all clients
* @param string $reason audit message suffix
*/
public function __construct(User $user, ?string $client_id, string $reason)
{
$this->user_id = $user->getId();
$this->client_id = $client_id;
$this->reason = $reason;
$this->client_ip = IPHelper::getUserIp();
Log::debug(sprintf(
"RevokeUserGrants::constructor user %s client_id %s reason %s",
$this->user_id,
$client_id ?? 'N/A',
$reason
));
}

public function handle(ITokenService $service): void
{
Log::debug("RevokeUserGrants::handle");

try {
$service->revokeUsersToken($this->user_id, $this->client_id);
} catch (\Exception $ex) {
Log::warning(sprintf("RevokeUserGrants::handle attempt %d failed: %s",
$this->attempts(), $ex->getMessage()));
throw $ex;
}

$scope = !empty($this->client_id)
? sprintf("client %s", $this->client_id)
: "all clients";

$action = sprintf(
"Revoking all grants for user %s on %s due to %s.",
$this->user_id,
$scope,
$this->reason
);

AddUserAction::dispatch($this->user_id, $this->client_ip, $action);

// Emit to OTEL audit log (Elasticsearch) if enabled
if (config('opentelemetry.enabled', false)) {
EmitAuditLogJob::dispatch('audit.security.tokens_revoked', [
'audit.action' => 'revoke_tokens',
'audit.entity' => 'User',
'audit.entity_id' => (string) $this->user_id,
'audit.timestamp' => now()->toISOString(),
'audit.description' => $action,
'audit.reason' => $this->reason,
'audit.scope' => $scope,
'auth.user.id' => $this->user_id,
'client.ip' => $this->client_ip,
'elasticsearch.index' => config('opentelemetry.logs.elasticsearch_index', 'logs-audit'),
]);
}
}

public function failed(\Throwable $exception): void
{
Log::error(sprintf("RevokeUserGrants::failed %s", $exception->getMessage()));
}
}
67 changes: 7 additions & 60 deletions app/Jobs/RevokeUserGrantsOnExplicitLogout.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,71 +13,18 @@
**/

use Auth\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use OAuth2\Services\ITokenService;
use Utils\IPHelper;

/**
* Class RevokeUserGrants
* Class RevokeUserGrantsOnExplicitLogout
* Revokes all OAuth2 grants for a user when they explicitly log out.
* @package App\Jobs
*/
class RevokeUserGrantsOnExplicitLogout implements ShouldQueue
class RevokeUserGrantsOnExplicitLogout extends RevokeUserGrants
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
const REASON = 'explicit logout';

public $tries = 5;

public $timeout = 0;

/**
* @var int
*/
private $user_id;

/**
* @var string
*/
private $client_id;


/**
* @param User $user
* @param string|null $client_id
*/
public function __construct(User $user, ?string $client_id = null){
$this->user_id = $user->getId();
$this->client_id = $client_id;
Log::debug(sprintf("RevokeUserGrants::constructor user %s client id %s", $this->user_id, !empty($client_id)? $client_id :"N/A"));
}

public function handle(ITokenService $service){
Log::debug(sprintf("RevokeUserGrants::handle"));

if(empty($this->client_id)) {
return;
}
try{
$action = sprintf
(
"Revoking all grants for user %s on %s due explicit Log out.",
$this->user_id, sprintf("Client %s", $this->client_id)
);

AddUserAction::dispatch($this->user_id, IPHelper::getUserIp(), $action);
$service->revokeUsersToken($this->user_id, $this->client_id);
}
catch (\Exception $ex) {
Log::error($ex);
}
}

public function failed(\Throwable $exception)
public function __construct(User $user, ?string $client_id = null)
{
Log::error(sprintf( "RevokeUserGrants::failed %s", $exception->getMessage()));
parent::__construct($user, $client_id, self::REASON);
}
}
}
30 changes: 30 additions & 0 deletions app/Jobs/RevokeUserGrantsOnPasswordChange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php namespace App\Jobs;
/*
* Copyright 2024 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use Auth\User;

/**
* Class RevokeUserGrantsOnPasswordChange
* Revokes all OAuth2 grants for a user across all clients after a password change.
* @package App\Jobs
*/
class RevokeUserGrantsOnPasswordChange extends RevokeUserGrants
{
const REASON = 'password change';

public function __construct(User $user)
{
parent::__construct($user, null, self::REASON);
}
}
30 changes: 30 additions & 0 deletions app/Jobs/RevokeUserGrantsOnSessionRevocation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php namespace App\Jobs;
/*
* Copyright 2024 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use Auth\User;

/**
* Class RevokeUserGrantsOnSessionRevocation
* Revokes all OAuth2 grants for a user when they explicitly sign out all other devices.
* @package App\Jobs
*/
class RevokeUserGrantsOnSessionRevocation extends RevokeUserGrants
{
const REASON = 'user-initiated session revocation';

public function __construct(User $user)
{
parent::__construct($user, null, self::REASON);
}
}
5 changes: 4 additions & 1 deletion app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use App\Events\UserLocked;
use App\Events\UserPasswordResetRequestCreated;
use App\Events\UserPasswordResetSuccessful;
use App\Jobs\RevokeUserGrantsOnPasswordChange;
use App\Events\UserSpamStateUpdated;
use App\Audit\AuditContext;
use App\libs\Auth\Repositories\IUserPasswordResetRequestRepository;
Expand Down Expand Up @@ -57,7 +58,7 @@ final class EventServiceProvider extends ServiceProvider
'Illuminate\Database\Events\QueryExecuted' => [
],
'Illuminate\Auth\Events\Logout' => [
//'App\Listeners\OnUserLogout',
// 'App\Listeners\OnUserLogout',
],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
'Illuminate\Auth\Events\Login' => [
'App\Listeners\OnUserLogin',
Expand Down Expand Up @@ -169,6 +170,8 @@ public function boot()
if(is_null($user)) return;
if(!$user instanceof User) return;
Mail::queue(new UserPasswordResetMail($user));
// Revoke all OAuth2 tokens for this user across all clients
RevokeUserGrantsOnPasswordChange::dispatch($user)->afterResponse();
});

Event::listen(OAuth2ClientLocked::class, function($event){
Expand Down
27 changes: 25 additions & 2 deletions app/Services/OpenId/UserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use App\Events\UserEmailUpdated;
use App\Events\UserPasswordResetSuccessful;
use App\Jobs\AddUserAction;
use App\Jobs\RevokeUserGrantsOnSessionRevocation;
use App\Jobs\PublishUserDeleted;
use App\Jobs\PublishUserUpdated;
use App\libs\Auth\Factories\UserFactory;
Expand Down Expand Up @@ -292,7 +293,9 @@ public function create(array $payload): IEntity
*/
public function update(int $id, array $payload): IEntity
{
$user = $this->tx_service->transaction(function () use ($id, $payload) {
$password_changed = false;

$user = $this->tx_service->transaction(function () use ($id, $payload, &$password_changed) {

$user = $this->repository->getById($id);

Expand Down Expand Up @@ -372,12 +375,17 @@ public function update(int $id, array $payload): IEntity

if ($former_password != $user->getPassword()) {
Log::warning(sprintf("UserService::update use id %s - password changed", $id));
Event::dispatch(new UserPasswordResetSuccessful($user->getId()));
$password_changed = true;
}

return $user;
});

if ($password_changed) {
request()->session()->regenerate();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mulldug please use

 Session::regenerate() 

so we are consistent with the rest of the code base

Event::dispatch(new UserPasswordResetSuccessful($user->getId()));
}

try {
if (Config::get("queue.enable_message_broker", false) == true)
PublishUserUpdated::dispatch($user)->onConnection('message_broker');
Expand Down Expand Up @@ -473,6 +481,21 @@ public function updateProfilePhoto($user_id, UploadedFile $file, $max_file_size
return $user;
}

public function revokeAllGrantsOnSessionRevocation(int $user_id): void
{
$user = $this->tx_service->transaction(function () use ($user_id) {
$user = $this->repository->getById($user_id);
if (!$user instanceof User)
throw new EntityNotFoundException("User not found.");

$user->setRememberToken(\Illuminate\Support\Str::random(60));

return $user;
});

RevokeUserGrantsOnSessionRevocation::dispatch($user)->afterResponse();
}

public function notifyMonitoredSecurityGroupActivity
(
string $action,
Expand Down
2 changes: 2 additions & 0 deletions app/libs/Auth/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -1578,6 +1578,8 @@ public function setPassword(string $password): void
$this->password_salt = AuthHelper::generateSalt(self::SaltLen, $this->password_enc);
$this->password = AuthHelper::encrypt_password($password, $this->password_salt, $this->password_enc);

$this->setRememberToken(\Illuminate\Support\Str::random(60));

$action = 'User set new password.';
$current_user = Auth::user();
if($current_user instanceof User) {
Expand Down
Loading
Loading