-
Notifications
You must be signed in to change notification settings - Fork 81
Allow users to set auth token expiration #3479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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.'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error should probably bubble up to the user. AFAICT, it appears in the network and console tabs, but the page isn't updated to tell the user why their token wasn't created. |
||
| } | ||
|
|
||
| $params['expires'] = $expiration->min(self::getMaximumTokenExpiration())->toIso8601String(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should a token set to some to beyond the maximum also be an invalid argument? Creating a token for a year or two from now and creating it with a different date without an indication of the difference seems like a recipe for confusion. It might be a personal preference rather than a universal experience
|
||
|
|
||
| $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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -470,7 +470,7 @@ | |
| Submit Only{{ authtoken.projectname && authtoken.projectname.length > 0 ? ' (' + authtoken.projectname + ')' : '' }} | ||
| </td> | ||
| <td align="center"> | ||
| {{ authtoken.expires }} | ||
| {{ formatTokenExpiration(authtoken.expires) }} | ||
| </td> | ||
| <td align="center"> | ||
| <span | ||
|
|
@@ -523,7 +523,7 @@ | |
| >New Token:</label> | ||
| <input | ||
| id="tokenDescription" | ||
| v-model="cdash.tokendescription" | ||
| v-model="tokendescription" | ||
| type="text" | ||
| name="tokenDescription" | ||
| class="form-control" | ||
|
|
@@ -555,11 +555,18 @@ | |
| </option> | ||
| </select> | ||
| </td> | ||
| <td /> | ||
| <td> | ||
| <input | ||
| v-model="tokenexpiration" | ||
| type="date" | ||
| :min="minTokenExpiration" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| :max="maxTokenExpiration" | ||
| > | ||
| </td> | ||
| <td align="center"> | ||
| <button | ||
| class="btn btn-default" | ||
| :disabled="!cdash.tokendescription" | ||
| :disabled="!tokendescription" | ||
| @click="generateToken()" | ||
| > | ||
| Generate Token | ||
|
|
@@ -697,6 +704,7 @@ | |
| <script> | ||
| import ApiLoader from './shared/ApiLoader'; | ||
| import LoadingIndicator from './shared/LoadingIndicator.vue'; | ||
| import {DateTime} from 'luxon'; | ||
| import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'; | ||
| import { | ||
| faBell, | ||
|
|
@@ -718,6 +726,8 @@ export default { | |
| loading: true, | ||
| errored: false, | ||
| tokenscope: 'full_access', | ||
| tokendescription: null, | ||
| tokenexpiration: null, | ||
| }; | ||
| }, | ||
|
|
||
|
|
@@ -732,6 +742,14 @@ export default { | |
| faUserPen, | ||
| }; | ||
| }, | ||
|
|
||
| minTokenExpiration() { | ||
| return DateTime.now().plus({ days: 1 }).toISODate(); | ||
| }, | ||
|
|
||
| maxTokenExpiration() { | ||
| return DateTime.fromISO(this.cdash.max_token_expiration).toISODate(); | ||
| }, | ||
| }, | ||
|
|
||
| mounted () { | ||
|
|
@@ -741,8 +759,9 @@ export default { | |
| methods: { | ||
| generateToken: function () { | ||
| const parameters = { | ||
| description: this.cdash.tokendescription, | ||
| description: this.tokendescription, | ||
| scope: this.tokenscope === 'full_access' ? 'full_access' : 'submit_only', | ||
| expiration: DateTime.fromISO(this.tokenexpiration).toISO(), | ||
| projectid: this.tokenscope === 'full_access' || this.tokenscope === 'submit_only' ? -1 : this.tokenscope, | ||
| }; | ||
| this.$axios.post('/api/authtokens/create', parameters) | ||
|
|
@@ -758,9 +777,6 @@ export default { | |
| } | ||
| }); | ||
|
|
||
| // A terrible hack to format the date the same way the DB returns them on initial page load | ||
| authtoken.expires = authtoken.expires.replace('T', ' '); | ||
|
|
||
| this.cdash.authtokens.push(authtoken); | ||
| }) | ||
| .catch(error => { | ||
|
|
@@ -804,6 +820,10 @@ export default { | |
| this.error = error.toString(); | ||
| } | ||
| }, | ||
|
|
||
| formatTokenExpiration(timestampString) { | ||
| return DateTime.fromISO(timestampString, {setZone: true}).toFormat('yyyy-MM-dd'); | ||
| }, | ||
| }, | ||
| }; | ||
| </script> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| <?php | ||
|
|
||
| namespace Tests\Feature; | ||
|
|
||
| use App\Models\AuthToken; | ||
| use Illuminate\Foundation\Testing\DatabaseTransactions; | ||
| use Illuminate\Support\Carbon; | ||
| use Illuminate\Support\Str; | ||
| use PHPUnit\Framework\Attributes\DataProvider; | ||
| use Tests\TestCase; | ||
| use Tests\Traits\CreatesUsers; | ||
|
|
||
| class AuthTokenTest extends TestCase | ||
| { | ||
| use CreatesUsers; | ||
| use DatabaseTransactions; | ||
|
|
||
| /** | ||
| * @return array<array<mixed>> | ||
| */ | ||
| public static function setTokenExpirationCases(): array | ||
| { | ||
| $oneYear = Carbon::now()->addYear(); | ||
| $oneMonth = Carbon::now()->addMonth(); | ||
| $oneMonthAgo = Carbon::now()->subMonth(); | ||
|
|
||
| return [ | ||
| // Defaults to 1 year if no expiration provided | ||
| [null, $oneYear, 0, true], | ||
| // Basic case with no limit | ||
| [$oneMonth, $oneMonth, 0, true], | ||
| // Sets to limit if expiration beyond limit | ||
| [$oneYear, $oneMonth, (int) Carbon::now()->diffInUTCSeconds($oneMonth), true], | ||
| // Test expiration in the past | ||
| [$oneMonthAgo, Carbon::now(), 0, false], | ||
| ]; | ||
| } | ||
|
|
||
| #[DataProvider('setTokenExpirationCases')] | ||
| public function testSetTokenExpiration( | ||
| ?Carbon $expirationParam, | ||
| Carbon $expectedExpiration, | ||
| int $tokenDuration, | ||
| bool $shouldPass, | ||
| ): void { | ||
| config(['cdash.token_duration' => $tokenDuration]); | ||
| $admin = $this->makeAdminUser(); | ||
|
|
||
| $response = $this->actingAs($admin)->post('/api/authtokens/create', [ | ||
| 'description' => Str::uuid()->toString(), | ||
| 'scope' => AuthToken::SCOPE_FULL_ACCESS, | ||
| 'expiration' => $expirationParam, | ||
| ]); | ||
|
|
||
| if ($shouldPass) { | ||
| $response->assertOk(); | ||
| $actualExpiration = Carbon::parse($response->json('token.expires')); | ||
| self::assertEqualsWithDelta($expectedExpiration->timestamp, $actualExpiration->timestamp, 10); | ||
| } else { | ||
| $response->assertBadRequest(); | ||
| } | ||
| } | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default expiration date seems to be 6 months. This token was created today with no entries in the expiration date widget. It also states 6 months in the .env description for token length