Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .helm/ecamp3/templates/api_configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ metadata:
{{- include "app.commonLabels" . | nindent 4 }}
data:
ADDITIONAL_TRUSTED_HOSTS: {{ .Values.domain | quote }}
{{- if not (.Values.api.authenticationTokenTtl | empty) }}
AUTHENTICATION_TOKEN_TTL: {{ .Values.api.authenticationTokenTtl | quote }}
{{- end }}
COOKIE_PREFIX: {{ include "api.cookiePrefix" . | quote }}
APP_ENV: {{ .Values.api.appEnv | quote }}
APP_DEBUG: {{ .Values.api.appDebug | quote }}
Expand Down
1 change: 1 addition & 0 deletions .helm/ecamp3/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ featureToggle:
checklist: false # enables checklist feature in frontend

api:
authenticationTokenTtl:
subpath: "/api"
image:
repository: "docker.io/ecamp/ecamp3-api"
Expand Down
2 changes: 2 additions & 0 deletions api/.env
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,6 @@ MAIL_FROM_NAME="eCamp v3"
RECAPTCHA_SECRET="disabled"
###< google/recaptcha ###

# Tokens are valid for 12 hours..
AUTHENTICATION_TOKEN_TTL=43200
TRANSLATE_ERRORS_TO_LOCALES="en,de,fr,it,rm"
1 change: 1 addition & 0 deletions api/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"exercise/htmlpurifier-bundle": "5.1",
"friendsofsymfony/http-cache": "3.1.1",
"friendsofsymfony/http-cache-bundle": "3.2.0",
"gesdinet/jwt-refresh-token-bundle": "1.5.0",
"google/recaptcha": "1.3.1",
"guzzlehttp/guzzle": "7.10.0",
"knpuniversity/oauth2-client-bundle": "2.18.4",
Expand Down
82 changes: 81 additions & 1 deletion api/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions api/config/bundles.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Exercise\HTMLPurifierBundle\ExerciseHTMLPurifierBundle;
use Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle;
use FOS\HttpCacheBundle\FOSHttpCacheBundle;
use Gesdinet\JWTRefreshTokenBundle\GesdinetJWTRefreshTokenBundle;
use Hautelook\AliceBundle\HautelookAliceBundle;
use KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle;
use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle;
Expand Down Expand Up @@ -44,4 +45,5 @@
SentryBundle::class => ['all' => true],
TwigExtraBundle::class => ['all' => true],
FOSHttpCacheBundle::class => ['all' => true],
GesdinetJWTRefreshTokenBundle::class => ['all' => true],
];
11 changes: 11 additions & 0 deletions api/config/packages/gesdinet_jwt_refresh_token.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
gesdinet_jwt_refresh_token:
cookie:
enabled: true
same_site: strict
path: /
http_only: true
secure: '%env(bool:COOKIE_SECURE)%'
remove_token_from_body: true
refresh_token_class: App\Entity\RefreshToken
single_use: true
token_parameter_name: '%env(COOKIE_PREFIX)%refresh_token'
4 changes: 1 addition & 3 deletions api/config/packages/lexik_jwt_authentication.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ lexik_jwt_authentication:
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'

# Tokens are valid for 12 hours, should be safe because we never expose the whole token to JavaScript.
# Of course it would be even better to have only short-lived tokens but renew them on every request.
token_ttl: 43200
token_ttl: '%env(AUTHENTICATION_TOKEN_TTL)%'

# Read the JWT token from a split cookie: The [api-domain]_jwt_hp and [api-domain]_jwt_s cookies are combined with a period (.)
# to form the full JWT token.
Expand Down
4 changes: 4 additions & 0 deletions api/config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ security:
lazy: true
provider: app_user_provider
user_checker: App\Security\UserStatusChecker
entry_point: jwt
json_login:
check_path: /authentication_token
username_path: identifier
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
jwt: ~
refresh_jwt:
check_path: /token/refresh
custom_authenticators:
- App\Security\OAuth\GoogleAuthenticator
- App\Security\OAuth\HitobitoAuthenticator
Expand All @@ -46,6 +49,7 @@ security:
- { path: ^/auth, roles: PUBLIC_ACCESS } # OAuth and resend password endpoints
- { path: ^/content_types, roles: PUBLIC_ACCESS } # Content types is more or less static and the same for all camps
- { path: ^/invitations/.*/(find|reject), roles: PUBLIC_ACCESS }
- { path: ^/token/refresh, roles: PUBLIC_ACCESS }
- { path: ^/users$, methods: [POST], roles: PUBLIC_ACCESS } # register
- { path: ^/users/.*/activate$, methods: [PATCH], roles: PUBLIC_ACCESS }
- { path: .*, roles: [ROLE_USER] } # Protect all other routes must be at the end
Expand Down
3 changes: 3 additions & 0 deletions api/config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
authentication_token:
path: /authentication_token
methods: ['POST']
api_refresh_token:
path: /token/refresh
methods: ['POST']
6 changes: 6 additions & 0 deletions api/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ services:
App\OpenApi\OAuthDecorator:
decorates: 'api_platform.openapi.factory'

App\OpenApi\RefreshTokenDecorator:
decorates: 'api_platform.openapi.factory'
arguments:
- '@.inner'
- '%env(COOKIE_PREFIX)%'

App\OAuth\UrlGeneratorDecorator:
class: App\OAuth\UrlGeneratorDecorator
arguments:
Expand Down
26 changes: 26 additions & 0 deletions api/migrations/schema/Version20250809140557.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250809140557 extends AbstractMigration {
public function getDescription(): string {
return 'Add refresh tokens table';
}

public function up(Schema $schema): void {
$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))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_9BACE7E1C74F2195 ON refresh_tokens (refresh_token)');
}

public function down(Schema $schema): void {
$this->addSql('DROP TABLE refresh_tokens');
}
}
10 changes: 10 additions & 0 deletions api/src/Entity/RefreshToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken as BaseRefreshToken;

#[ORM\Entity]
#[ORM\Table(name: 'refresh_tokens')]
class RefreshToken extends BaseRefreshToken {}
2 changes: 1 addition & 1 deletion api/src/OpenApi/JwtDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function __invoke(array $context = []): OpenApi {
tags: ['Login'],
responses: [
'204' => [
'description' => "Get a JWT token split across the two cookies {$cookiePrefix}jwt_hp and {$cookiePrefix}jwt_s",
'description' => "Get a JWT token split across the two cookies {$cookiePrefix}jwt_hp and {$cookiePrefix}jwt_s. Also returns a refresh token in {$cookiePrefix}refresh_token",
],
],
summary: 'Log in using email and password.',
Expand Down
33 changes: 33 additions & 0 deletions api/src/OpenApi/RefreshTokenDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\OpenApi;

use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\OpenApi;

final class RefreshTokenDecorator implements OpenApiFactoryInterface {
public function __construct(private OpenApiFactoryInterface $decorated, private string $cookiePrefix) {}

public function __invoke(array $context = []): OpenApi {
$openApi = ($this->decorated)($context);

$cookiePrefix = $this->cookiePrefix;
$pathItem = new Model\PathItem(
ref: 'JWT Token Refresh',
post: new Model\Operation(
operationId: 'postRefreshToken',
tags: ['JWT Refresh'],
responses: [
'204' => [
'description' => "Get a refreshed JWT token split across the two cookies {$cookiePrefix}jwt_hp and {$cookiePrefix}jwt_s. Also returns a new refresh token {$cookiePrefix}refresh_token.",
],
],
summary: 'Refresh token.',
),
);
$openApi->getPaths()->addPath('/token/refresh', $pathItem);

return $openApi;
}
}
1 change: 1 addition & 0 deletions api/src/Serializer/Normalizer/UriTemplateNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public function normalize($data, $format = null, array $context = []): array|\Ar
$result['_links']['oauthPbsmidata'] = ['href' => $this->urlGenerator->generate('connect_pbsmidata_start').'{?callback}', 'templated' => true];
$result['_links']['oauthCevidb'] = ['href' => $this->urlGenerator->generate('connect_cevidb_start').'{?callback}', 'templated' => true];
$result['_links']['oauthJubladb'] = ['href' => $this->urlGenerator->generate('connect_jubladb_start').'{?callback}', 'templated' => true];
$result['_links']['refreshToken'] = ['href' => $this->urlGenerator->generate('api_refresh_token')];
$result['_links']['resetPassword'] = ['href' => $this->urlGenerator->generate('_api_/auth/reset_password{._format}_post').'{/id}', 'templated' => true];
$result['_links']['resendActivation'] = ['href' => $this->urlGenerator->generate('_api_/auth/resend_activation{._format}_post'), 'templated' => false];

Expand Down
14 changes: 14 additions & 0 deletions api/symfony.lock
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,20 @@
"gedmo/doctrine-extensions": {
"version": "v3.0.5"
},
"gesdinet/jwt-refresh-token-bundle": {
"version": "1.5",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.0",
"ref": "2390b4ed5c195e0b3f6dea45221f3b7c0af523a0"
},
"files": [
"config/packages/gesdinet_jwt_refresh_token.yaml",
"config/routes/gesdinet_jwt_refresh_token.yaml",
"src/Entity/RefreshToken.php"
]
},
"google/recaptcha": {
"version": "1.2",
"recipe": {
Expand Down
1 change: 1 addition & 0 deletions api/tests/Api/FirewallTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ private static function isProtectedByFirewall(mixed $endpoint): bool {
'/auth/resend_activation' => false,
'/content_types' => false,
'/invitations' => false,
'/token/refresh' => false,
default => true
};
}
Expand Down
1 change: 1 addition & 0 deletions api/tests/Api/SnapshotTests/EndpointPerformanceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ private static function getCollectionEndpoints() {
'/auth/resend_activation' => false,
'/invitations' => false,
'/personal_invitations' => false,
'/token/refresh' => false,
default => true
};
});
Expand Down
1 change: 1 addition & 0 deletions api/tests/Api/SnapshotTests/ResponseSnapshotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public static function getCollectionEndpoints() {
'/auth/resend_activation' => false,
'/invitations' => false,
'/personal_invitations' => false,
'/token/refresh' => false,
'/users' => false,
default => true
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34692,6 +34692,16 @@ paths:
summary: 'Updates the ScheduleEntry resource.'
tags:
- ScheduleEntry
/token/refresh:
post:
operationId: postRefreshToken
responses:
204:
description: 'Get a refreshed JWT token split across the two cookies example_com_jwt_hp and example_com_jwt_s. Also returns a new refresh token example_com_refresh_token.'
summary: 'Refresh token.'
tags:
- 'JWT Refresh'
ref: 'JWT Token Refresh'
/users:
get:
deprecated: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@
"href": "/profiles{/id}{?user.collaborations.camp,user.collaborations.camp[],user,user[]}",
"templated": true
},
"refreshToken": {
"href": "/token/refresh"
},
"resendActivation": {
"href": "/auth/resend_activation",
"templated": false
Expand Down
6 changes: 6 additions & 0 deletions api/tests/Serializer/Normalizer/UriTemplateNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ protected function setUp(): void {
case 'connect_jubladb_start':
return '/auth/jubladb';

case 'api_refresh_token':
return '/token/refresh';

case '_api_/auth/resend_activation{._format}_post':
return '/auth/resend_activation';

Expand Down Expand Up @@ -91,6 +94,9 @@ protected function setUp(): void {
'href' => '/auth/reset_password{/id}',
'templated' => true,
],
'refreshToken' => [
'href' => '/token/refresh',
],
];
}

Expand Down
Loading
Loading