Skip to content

Commit d3359d2

Browse files
committed
refresh the access token in the background
Use gesdinet/jwt-refresh-token-bundle to achieve this. The access token lifetime was reduced to 30 mins according to https://cloud.google.com/apigee/docs/api-platform/antipatterns/oauth-long-expiration as a starting point. Do not log out if the refresh in the background fails, the user might want to backup the content he is currently editing. One proposed pattern is to intercept 401 responses and refresh the token when the requests fail. Because we often have multiple requests running, this did not work well. Now we have a timeout in the background which refreshes the access token periodically. closes #4898
1 parent a3c0f62 commit d3359d2

21 files changed

+480
-6
lines changed

api/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"exercise/htmlpurifier-bundle": "5.1",
1616
"friendsofsymfony/http-cache": "3.1.1",
1717
"friendsofsymfony/http-cache-bundle": "3.2.0",
18+
"gesdinet/jwt-refresh-token-bundle": "1.5.0",
1819
"google/recaptcha": "1.3.1",
1920
"guzzlehttp/guzzle": "7.9.3",
2021
"knpuniversity/oauth2-client-bundle": "2.18.3",

api/composer.lock

Lines changed: 81 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/config/bundles.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Exercise\HTMLPurifierBundle\ExerciseHTMLPurifierBundle;
77
use Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle;
88
use FOS\HttpCacheBundle\FOSHttpCacheBundle;
9+
use Gesdinet\JWTRefreshTokenBundle\GesdinetJWTRefreshTokenBundle;
910
use Hautelook\AliceBundle\HautelookAliceBundle;
1011
use KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle;
1112
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
@@ -44,4 +45,5 @@
4445
SentryBundle::class => ['all' => true],
4546
TwigExtraBundle::class => ['all' => true],
4647
FOSHttpCacheBundle::class => ['all' => true],
48+
GesdinetJWTRefreshTokenBundle::class => ['all' => true],
4749
];
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
gesdinet_jwt_refresh_token:
2+
cookie:
3+
enabled: true
4+
same_site: strict
5+
path: /
6+
http_only: true
7+
secure: '%env(bool:COOKIE_SECURE)%'
8+
remove_token_from_body: true
9+
refresh_token_class: App\Entity\RefreshToken
10+
single_use: true
11+
token_parameter_name: '%env(COOKIE_PREFIX)%refresh_token'

api/config/packages/lexik_jwt_authentication.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ lexik_jwt_authentication:
77
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
88
pass_phrase: '%env(JWT_PASSPHRASE)%'
99

10-
# Tokens are valid for 12 hours, should be safe because we never expose the whole token to JavaScript.
11-
# Of course it would be even better to have only short-lived tokens but renew them on every request.
12-
token_ttl: 43200
10+
# Tokens are valid for 30 minutes.
11+
token_ttl: 1800
1312

1413
# Read the JWT token from a split cookie: The [api-domain]_jwt_hp and [api-domain]_jwt_s cookies are combined with a period (.)
1514
# to form the full JWT token.

api/config/packages/security.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@ security:
2828
lazy: true
2929
provider: app_user_provider
3030
user_checker: App\Security\UserStatusChecker
31+
entry_point: jwt
3132
json_login:
3233
check_path: /authentication_token
3334
username_path: identifier
3435
password_path: password
3536
success_handler: lexik_jwt_authentication.handler.authentication_success
3637
failure_handler: lexik_jwt_authentication.handler.authentication_failure
3738
jwt: ~
39+
refresh_jwt:
40+
check_path: /token/refresh
3841
custom_authenticators:
3942
- App\Security\OAuth\GoogleAuthenticator
4043
- App\Security\OAuth\HitobitoAuthenticator
@@ -46,6 +49,7 @@ security:
4649
- { path: ^/auth, roles: PUBLIC_ACCESS } # OAuth and resend password endpoints
4750
- { path: ^/content_types, roles: PUBLIC_ACCESS } # Content types is more or less static and the same for all camps
4851
- { path: ^/invitations/.*/(find|reject), roles: PUBLIC_ACCESS }
52+
- { path: ^/token/refresh, roles: PUBLIC_ACCESS }
4953
- { path: ^/users$, methods: [POST], roles: PUBLIC_ACCESS } # register
5054
- { path: ^/users/.*/activate$, methods: [PATCH], roles: PUBLIC_ACCESS }
5155
- { path: .*, roles: [ROLE_USER] } # Protect all other routes must be at the end

api/config/routes.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
authentication_token:
55
path: /authentication_token
66
methods: ['POST']
7+
api_refresh_token:
8+
path: /token/refresh

api/config/services.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ services:
103103
App\OpenApi\OAuthDecorator:
104104
decorates: 'api_platform.openapi.factory'
105105

106+
App\OpenApi\RefreshTokenDecorator:
107+
decorates: 'api_platform.openapi.factory'
108+
arguments:
109+
- '@.inner'
110+
- '%env(COOKIE_PREFIX)%'
111+
106112
App\OAuth\UrlGeneratorDecorator:
107113
class: App\OAuth\UrlGeneratorDecorator
108114
arguments:
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20250809140557 extends AbstractMigration {
14+
public function getDescription(): string {
15+
return 'Add refresh tokens table';
16+
}
17+
18+
public function up(Schema $schema): void {
19+
$this->addSql('CREATE TABLE refresh_tokens (refresh_token VARCHAR(128) NOT NULL, username VARCHAR(255) NOT NULL, valid TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, PRIMARY KEY (id))');
20+
$this->addSql('CREATE UNIQUE INDEX UNIQ_9BACE7E1C74F2195 ON refresh_tokens (refresh_token)');
21+
}
22+
23+
public function down(Schema $schema): void {
24+
$this->addSql('DROP TABLE refresh_tokens');
25+
}
26+
}

api/src/Entity/RefreshToken.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Entity;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken as BaseRefreshToken;
7+
8+
#[ORM\Entity]
9+
#[ORM\Table(name: 'refresh_tokens')]
10+
class RefreshToken extends BaseRefreshToken {}

0 commit comments

Comments
 (0)