diff --git a/.env.example b/.env.example index 1688c7aba1..7e17fa9ac2 100755 --- a/.env.example +++ b/.env.example @@ -128,9 +128,8 @@ DB_PASSWORD=secret # Log when a page takes more this this many seconds to load. #SLOW_PAGE_TIME=10 -# How long authentication tokens should last before expiring (in seconds). -# Default is six months. -# 0 here means that tokens do not expire. +# The maximum allowable token duration (in seconds). Default is six months. +# 0 here means that there's no limit on token validity. #TOKEN_DURATION=15811200 # Whitelist of projects that are allowed to have unlimited builds. diff --git a/app/Http/Controllers/AuthTokenController.php b/app/Http/Controllers/AuthTokenController.php index 5ab1ad893f..59574820ca 100644 --- a/app/Http/Controllers/AuthTokenController.php +++ b/app/Http/Controllers/AuthTokenController.php @@ -61,6 +61,7 @@ public function createToken(Request $request): JsonResponse $projectid, $request->input('scope'), $request->input('description'), + $request->date('expiration'), ); } catch (InvalidArgumentException $e) { return response()->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 3b82add369..de0e7e83a9 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -58,6 +58,7 @@ public function userPageContent(): JsonResponse $response['user_name'] = $user->firstname; $response['user_is_admin'] = $user->admin; $response['show_monitor'] = config('queue.default') === 'database'; + $response['max_token_expiration'] = AuthTokenUtil::getMaximumTokenExpiration()->toIso8601String(); if ((bool) config('cdash.user_create_projects')) { $response['user_can_create_projects'] = 1; diff --git a/app/Utils/AuthTokenUtil.php b/app/Utils/AuthTokenUtil.php index 59dcc612e7..f46d163cef 100644 --- a/app/Utils/AuthTokenUtil.php +++ b/app/Utils/AuthTokenUtil.php @@ -28,30 +28,30 @@ class AuthTokenUtil * * @throws InvalidArgumentException */ - public static function generateToken(int $user_id, int $project_id, string $scope, string $description): array + public static function generateToken(int $user_id, int $project_id, string $scope, string $description, ?Carbon $expiration = null): array { + $params = []; + // 86 characters generates more than 512 bits of entropy (and is thus limited by the entropy of the hash) $token = Str::password(86, true, true, false); $params['hash'] = hash('sha512', $token); $params['userid'] = $user_id; - $duration = Config::get('cdash.token_duration'); - $now = time(); - $params['created'] = gmdate(FMT_DATETIME, $now); + $now = Carbon::now(); + $params['created'] = $now->toIso8601String(); - if (!is_numeric($duration) || (int) $duration < 0) { - Log::error("Invalid token_duration configuration {$duration}"); - throw new InvalidArgumentException('Invalid token_duration configuration'); + // The default expiration date is 1 year in the future. + if ($expiration === null) { + $expiration = $now->addYear(); } - if ((int) $duration === 0) { - // this token "never" expires - $params['expires'] = '9999-01-01 00:00:00'; - } else { - $params['expires'] = gmdate(FMT_DATETIME, $now + $duration); + if ($expiration->isNowOrPast()) { + throw new InvalidArgumentException('Token expiration cannot be in the past.'); } + $params['expires'] = $expiration->min(self::getMaximumTokenExpiration())->toIso8601String(); + $params['description'] = $description; if (!self::validScope($scope)) { @@ -283,4 +283,19 @@ public static function getBearerToken(): ?string { return request()->bearerToken(); } + + public static function getMaximumTokenExpiration(): Carbon + { + $maxDuration = Config::get('cdash.token_duration'); + if (!is_numeric($maxDuration) || (int) $maxDuration < 0) { + Log::error("Invalid token_duration configuration {$maxDuration}"); + throw new InvalidArgumentException('Invalid token_duration configuration'); + } + + // A maximum duration of 0 is equivalent to no limit. + if ((int) $maxDuration === 0) { + return (new Carbon())->endOfMillennium(); + } + return Carbon::now()->addSeconds((int) $maxDuration); + } } diff --git a/app/cdash/tests/CMakeLists.txt b/app/cdash/tests/CMakeLists.txt index dfb2c40778..258581381d 100644 --- a/app/cdash/tests/CMakeLists.txt +++ b/app/cdash/tests/CMakeLists.txt @@ -247,6 +247,8 @@ add_feature_test_in_transaction(/Feature/Jobs/PruneAuthTokensTest) add_feature_test_in_transaction(/Feature/UserCommand) +add_feature_test_in_transaction(/Feature/AuthTokenTest) + add_feature_test_in_transaction(/Feature/RemoteWorkers) set_property(TEST /Feature/RemoteWorkers APPEND PROPERTY DISABLED "$>" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ee74a1ce9c..f1bb32915f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6060,12 +6060,6 @@ parameters: count: 1 path: app/Utils/AuthTokenUtil.php - - - rawMessage: Implicit array creation is not allowed - variable $params does not exist. - identifier: variable.implicitArray - count: 1 - path: app/Utils/AuthTokenUtil.php - - rawMessage: 'Method App\Utils\AuthTokenUtil::getUserIdFromRequest() should return int|null but returns mixed.' identifier: return.type @@ -6073,13 +6067,7 @@ parameters: path: app/Utils/AuthTokenUtil.php - - rawMessage: 'Parameter #2 $timestamp of function gmdate expects int|null, float|int given.' - identifier: argument.type - count: 1 - path: app/Utils/AuthTokenUtil.php - - - - rawMessage: 'Part $duration (mixed) of encapsed string cannot be cast to string.' + rawMessage: 'Part $maxDuration (mixed) of encapsed string cannot be cast to string.' identifier: encapsedStringPart.nonString count: 1 path: app/Utils/AuthTokenUtil.php @@ -26817,6 +26805,12 @@ parameters: count: 1 path: tests/Browser/Pages/UsersPageTest.php + - + rawMessage: 'Parameter #1 $time of static method Carbon\Carbon::parse() expects Carbon\Month|Carbon\WeekDay|DateTimeInterface|float|int|string|null, mixed given.' + identifier: argument.type + count: 1 + path: tests/Feature/AuthTokenTest.php + - rawMessage: Access to an undefined property CDash\Model\Build::$Endime. identifier: property.notFound diff --git a/resources/js/vue/components/UserHomepage.vue b/resources/js/vue/components/UserHomepage.vue index 6475f0e84f..6af5de27a1 100644 --- a/resources/js/vue/components/UserHomepage.vue +++ b/resources/js/vue/components/UserHomepage.vue @@ -470,7 +470,7 @@ Submit Only{{ authtoken.projectname && authtoken.projectname.length > 0 ? ' (' + authtoken.projectname + ')' : '' }} - {{ authtoken.expires }} + {{ formatTokenExpiration(authtoken.expires) }} New Token: - + + +