implement on frontend
All checks were successful
CI / Maven unit tests (push) Successful in 15s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped

This commit is contained in:
2026-10-15 21:10:22 +01:00
parent cf951afbae
commit 1e6af005a9
8 changed files with 194 additions and 41 deletions

View File

@@ -1,10 +1,12 @@
package com.ddf.vodsystem.configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -24,7 +26,7 @@ public class SecurityConfig {
/**
* Builds the security filter chain: stateless sessions, CSRF disabled, the {@link JwtFilter}
* installed, and per-endpoint authorization rules.
* installed, {@code 401} for unauthenticated requests, and per-endpoint authorization rules.
*
* @param http the HTTP security builder to configure
* @return the built {@link SecurityFilterChain}
@@ -41,11 +43,14 @@ public class SecurityConfig {
.requestMatchers("/api/v1/media/compress").permitAll() // compression available without authenticated
.requestMatchers("/api/v1/jobs/**").permitAll() // same with jobs
.requestMatchers("/api/v1/media/**").authenticated()
.requestMatchers("/api/v1/users/me", "/api/v1/users/logout").authenticated()
.requestMatchers("/api/v1/users/login").permitAll()
.requestMatchers("/api/v1/users/me").authenticated()
// authenticated by the refresh token cookie, not the (possibly expired) access token
.requestMatchers("/api/v1/users/login", "/api/v1/users/refresh", "/api/v1/users/logout").permitAll()
.requestMatchers("/api/v1/stream/start", "/api/v1/stream/stop", "/api/v1/stream/heartbeat").permitAll()
.anyRequest().authenticated()
)
// 401 rather than the default 403, so clients know to refresh their session
.exceptionHandling(e -> e.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);

View File

@@ -1,6 +1,7 @@
package com.ddf.vodsystem.controllers;
import com.ddf.vodsystem.dto.APIResponse;
import com.ddf.vodsystem.controllers.dto.SessionResponse;
import com.ddf.vodsystem.controllers.dto.Token;
import com.ddf.vodsystem.dto.TokenPackage;
import com.ddf.vodsystem.dto.properties.AuthProperties;
@@ -64,18 +65,18 @@ public class UserController {
* @param token the payload carrying the Google ID token to verify
* @param request the incoming request, used to decide the cookies' {@code Secure} flag
* @param response the response the cookies are written to
* @return {@code 200 OK} with no data
* @return {@code 200 OK} wrapping the access token lifetime as a {@link SessionResponse}
* @throws NotAuthenticated if the Google ID token is rejected
*/
@PostMapping("/login")
public ResponseEntity<APIResponse<Void>> login(@RequestBody Token token,
public ResponseEntity<APIResponse<SessionResponse>> login(@RequestBody Token token,
HttpServletRequest request,
HttpServletResponse response) {
TokenPackage tokenPackage = userService.login(token.token());
addCookies(request, response, tokenPackage.accessToken(), tokenPackage.refreshToken());
return ResponseEntity.ok(
new APIResponse<>(SUCCESS, "Logged in successfully", null)
new APIResponse<>(SUCCESS, "Logged in successfully", sessionResponse())
);
}
@@ -85,11 +86,11 @@ public class UserController {
* @param refreshToken the refresh token from the {@code refreshToken} cookie, if present
* @param request the incoming request, used to decide the cookies' {@code Secure} flag
* @param response the response the new cookies are written to
* @return {@code 200 OK} with no data
* @return {@code 200 OK} wrapping the new access token lifetime as a {@link SessionResponse}
* @throws NotAuthenticated if the refresh token is missing or rejected
*/
@PostMapping("/refresh")
public ResponseEntity<APIResponse<Void>> refresh(
public ResponseEntity<APIResponse<SessionResponse>> refresh(
@CookieValue(name = "refreshToken", required = false) String refreshToken,
HttpServletRequest request,
HttpServletResponse response) {
@@ -97,7 +98,7 @@ public class UserController {
addCookies(request, response, tokenPackage.accessToken(), tokenPackage.refreshToken());
return ResponseEntity.ok(
new APIResponse<>(SUCCESS, "Refreshed tokens successfully", null)
new APIResponse<>(SUCCESS, "Refreshed tokens successfully", sessionResponse())
);
}
@@ -123,6 +124,10 @@ public class UserController {
);
}
private SessionResponse sessionResponse() {
return new SessionResponse(jwtExpiration.getSeconds());
}
private void addCookies(
HttpServletRequest request,
HttpServletResponse response,

View File

@@ -0,0 +1,11 @@
package com.ddf.vodsystem.controllers.dto;
/**
* Session metadata returned after login or refresh. The tokens themselves are only ever sent as
* HttpOnly cookies; this tells the client when to refresh them.
*
* @param accessTokenExpiresIn seconds until the access token cookie expires
*/
public record SessionResponse(
long accessTokenExpiresIn
) {}

View File

@@ -53,7 +53,7 @@ public class JwtFilter extends OncePerRequestFilter {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
jwt = Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.filter(cookie -> "accessToken".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);