From 5dec295c4856a1f946eeef7e166717f93024df34 Mon Sep 17 00:00:00 2001 From: dennis Date: Tue, 1 Oct 2024 21:13:31 +0200 Subject: [PATCH 01/27] enable bearer authentication --- .../websocket/WebsocketConfiguration.java | 6 +- .../artemis/core/security/jwt/JWTFilter.java | 70 ++++++++++++++++--- .../core/web/open/PublicUserJwtResource.java | 4 +- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index 9163cfb7d7f1..03bf213a08d8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -19,7 +19,7 @@ import java.util.regex.Pattern; import jakarta.annotation.Nullable; -import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.constraints.NotNull; import org.slf4j.Logger; @@ -52,7 +52,6 @@ import org.springframework.web.socket.server.HandshakeInterceptor; import org.springframework.web.socket.server.support.DefaultHandshakeHandler; import org.springframework.web.socket.sockjs.transport.handler.WebSocketTransportHandler; -import org.springframework.web.util.WebUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Iterators; @@ -203,8 +202,7 @@ public boolean beforeHandshake(@NotNull ServerHttpRequest request, @NotNull Serv @NotNull Map attributes) { if (request instanceof ServletServerHttpRequest servletRequest) { attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); - Cookie jwtCookie = WebUtils.getCookie(servletRequest.getServletRequest(), JWTFilter.JWT_COOKIE_NAME); - return JWTFilter.isJwtCookieValid(tokenProvider, jwtCookie); + return JWTFilter.extractValidJwt((HttpServletRequest) servletRequest, tokenProvider) != null; } return false; } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index a7373fcd9874..6baa104add6e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -9,6 +9,8 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; +import javax.annotation.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; @@ -31,26 +33,72 @@ public JWTFilter(TokenProvider tokenProvider) { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; - Cookie jwtCookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); - if (isJwtCookieValid(this.tokenProvider, jwtCookie)) { - Authentication authentication = this.tokenProvider.getAuthentication(jwtCookie.getValue()); + + // check if valid JWT token is in the cookie or in the Authorization header + // then proceed to do authentication with this token + String jwtToken = extractValidJwt(httpServletRequest, this.tokenProvider); + if (jwtToken != null) { + Authentication authentication = this.tokenProvider.getAuthentication(jwtToken); SecurityContextHolder.getContext().setAuthentication(authentication); } + filterChain.doFilter(servletRequest, servletResponse); } /** - * Checks if the cookie containing the jwt is valid + * Extracts a valid jwt token from the cookie first or if not set from the Authorization header * - * @param tokenProvider the artemis token provider used to generate and validate jwt's - * @param jwtCookie the cookie containing the jwt - * @return true if the jwt is valid, false if missing or invalid + * @param httpServletRequest the http request + * @param tokenProvider the artemis token provider used to generate and validate jwt's + * @return the valid jwt token or null if not found or invalid + */ + public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) { + Cookie jwtCookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); + String jwtToken; + // returns the first valid jwt token found in the cookie or the Authorization header + if (isJwtValid(tokenProvider, jwtToken = getJwtFromCookie(jwtCookie)) + || isJwtValid(tokenProvider, jwtToken = getJwtFromBearer(httpServletRequest.getHeader("Authorization")))) { + return jwtToken; + } + + return null; + } + + /** + * Extracts the jwt token from the cookie + * + * @param jwtCookie the cookie with Key "jwt" + * @return the jwt token */ - public static boolean isJwtCookieValid(TokenProvider tokenProvider, Cookie jwtCookie) { + private static String getJwtFromCookie(Cookie jwtCookie) { if (jwtCookie == null) { - return false; + return null; } - String jwt = jwtCookie.getValue(); - return StringUtils.hasText(jwt) && tokenProvider.validateTokenForAuthority(jwt); + return jwtCookie.getValue(); + } + + /** + * Extracts the jwt token from the Authorization header + * + * @param jwtBearer the content of the Authorization header + * @return the jwt token + */ + private static String getJwtFromBearer(String jwtBearer) { + if (!StringUtils.hasText(jwtBearer) || !jwtBearer.startsWith("Bearer ")) { + return null; + } + + return jwtBearer.substring(7); + } + + /** + * Checks if the jwt token is valid + * + * @param tokenProvider the artemis token provider used to generate and validate jwt's + * @param jwtToken the jwt token + * @return true if the jwt is valid, false if missing or invalid + */ + public static boolean isJwtValid(TokenProvider tokenProvider, String jwtToken) { + return StringUtils.hasText(jwtToken) && tokenProvider.validateTokenForAuthority(jwtToken); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 44e44a0ff87a..f32d13f006e7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -69,7 +69,7 @@ public PublicUserJwtResource(JWTCookieService jwtCookieService, AuthenticationMa */ @PostMapping("authenticate") @EnforceNothing - public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) { + public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) { var username = loginVM.getUsername(); var password = loginVM.getPassword(); @@ -86,7 +86,7 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @Requ ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); - return ResponseEntity.ok().build(); + return ResponseEntity.ok(responseCookie.getValue()); } catch (BadCredentialsException ex) { log.warn("Wrong credentials during login for user {}", loginVM.getUsername()); From b447b281fee0644ea772b0061bbbd19a169a8094 Mon Sep 17 00:00:00 2001 From: dennis Date: Tue, 1 Oct 2024 22:20:06 +0200 Subject: [PATCH 02/27] Add re-key endpoint --- .../core/web/open/PublicUserJwtResource.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 44e44a0ff87a..c11c35f8a0de 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -27,12 +27,14 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import de.tum.cit.aet.artemis.core.dto.vm.LoginVM; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.security.SecurityUtils; import de.tum.cit.aet.artemis.core.security.UserNotActivatedException; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceNothing; import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; import de.tum.cit.aet.artemis.core.service.connectors.SAML2Service; @@ -94,6 +96,24 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @Requ } } + /** + * Sends the token back as either a cookie or a bearer token + * + * @param response HTTP response + * @return the ResponseEntity with status 200 (ok), 401 (unauthorized) + */ + @PostMapping("re-key") + @EnforceAtLeastStudent + public ResponseEntity reKey(@RequestParam(value = "as-bearer", defaultValue = "false") boolean asBearer, HttpServletResponse response) { + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(true); + if (asBearer) { + return ResponseEntity.ok(responseCookie.getValue()); + } + response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); + + return ResponseEntity.ok().build(); + } + /** * Authorizes a User logged in with SAML2 * From a3d0fbbb66b6bc71e81f7d6da65c54a95ef5d4ef Mon Sep 17 00:00:00 2001 From: dennis Date: Wed, 2 Oct 2024 13:45:30 +0200 Subject: [PATCH 03/27] Implement rabbit feedback --- .../artemis/core/security/jwt/JWTFilter.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index 6baa104add6e..a180abef77bb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -9,8 +9,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; -import javax.annotation.Nullable; - +import org.springframework.lang.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; @@ -46,21 +45,21 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } /** - * Extracts a valid jwt token from the cookie first or if not set from the Authorization header + * // Extracts the first valid jwt token found in the cookie or the Authorization header * * @param httpServletRequest the http request * @param tokenProvider the artemis token provider used to generate and validate jwt's * @return the valid jwt token or null if not found or invalid */ public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) { - Cookie jwtCookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); - String jwtToken; - // returns the first valid jwt token found in the cookie or the Authorization header - if (isJwtValid(tokenProvider, jwtToken = getJwtFromCookie(jwtCookie)) - || isJwtValid(tokenProvider, jwtToken = getJwtFromBearer(httpServletRequest.getHeader("Authorization")))) { + String jwtToken = getJwtFromCookie(WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME)); + if (isJwtValid(tokenProvider, jwtToken)) { + return jwtToken; + } + jwtToken = getJwtFromBearer(httpServletRequest.getHeader("Authorization")); + if (isJwtValid(tokenProvider, jwtToken)) { return jwtToken; } - return null; } @@ -68,9 +67,9 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo * Extracts the jwt token from the cookie * * @param jwtCookie the cookie with Key "jwt" - * @return the jwt token + * @return the jwt token or null if not found */ - private static String getJwtFromCookie(Cookie jwtCookie) { + private static @Nullable String getJwtFromCookie(Cookie jwtCookie) { if (jwtCookie == null) { return null; } @@ -81,14 +80,14 @@ private static String getJwtFromCookie(Cookie jwtCookie) { * Extracts the jwt token from the Authorization header * * @param jwtBearer the content of the Authorization header - * @return the jwt token + * @return the jwt token or null if not found */ - private static String getJwtFromBearer(String jwtBearer) { + private static @Nullable String getJwtFromBearer(String jwtBearer) { if (!StringUtils.hasText(jwtBearer) || !jwtBearer.startsWith("Bearer ")) { return null; } - return jwtBearer.substring(7); + return jwtBearer.substring(7).trim(); } /** @@ -98,7 +97,7 @@ private static String getJwtFromBearer(String jwtBearer) { * @param jwtToken the jwt token * @return true if the jwt is valid, false if missing or invalid */ - public static boolean isJwtValid(TokenProvider tokenProvider, String jwtToken) { + private static boolean isJwtValid(TokenProvider tokenProvider, String jwtToken) { return StringUtils.hasText(jwtToken) && tokenProvider.validateTokenForAuthority(jwtToken); } } From 39e4bad1a39235506b5ccd53080c45b03e83958c Mon Sep 17 00:00:00 2001 From: dennis Date: Fri, 4 Oct 2024 11:47:03 +0200 Subject: [PATCH 04/27] make token theia specific --- .../core/security/jwt/JWTCookieService.java | 11 ++++++ .../core/security/jwt/TokenProvider.java | 19 ++++++++-- .../core/web/open/PublicUserJwtResource.java | 36 +++++++++++++------ 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java index 73320dee2813..f541641c0741 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java @@ -41,6 +41,17 @@ public ResponseCookie buildLoginCookie(boolean rememberMe) { return buildJWTCookie(jwt, duration); } + /** + * Builds the cookie with Theia flag + * + * @param duration the duration of the cookie and the jwt + * @return the login ResponseCookie containing the JWT + */ + public ResponseCookie buildTheiaCookie(long duration) { + String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), duration, "THEIA"); + return buildJWTCookie(jwt, Duration.of(duration, ChronoUnit.MILLIS)); + } + /** * Builds the cookie containing the jwt for a logout and sets it in the response * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index 262ece79700d..65e8c410ee38 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -95,11 +95,26 @@ public long getTokenValidity(boolean rememberMe) { * @return JWT Token */ public String createToken(Authentication authentication, boolean rememberMe) { + return createToken(authentication, getTokenValidity(rememberMe)); + } + + /** + * Create JWT Token a fully populated Authentication object. + * + * @param authentication Authentication Object + * @param duration the Token lifetime + * @param tools tools this token is used for + * @return JWT Token + */ + public String createToken(Authentication authentication, long duration, String... tools) { String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")); + String toolClaims = String.join(",", tools); + long now = (new Date()).getTime(); - Date validity = new Date(now + getTokenValidity(rememberMe)); - return Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities).signWith(key, Jwts.SIG.HS512).expiration(validity).compact(); + Date validity = new Date(now + duration); + return Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities).claim("tools", toolClaims).signWith(key, Jwts.SIG.HS512).expiration(validity) + .compact(); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index c1d6b6506c81..692d70be96db 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -2,6 +2,9 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Date; import java.util.Optional; import jakarta.servlet.ServletException; @@ -27,7 +30,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import de.tum.cit.aet.artemis.core.dto.vm.LoginVM; @@ -37,6 +39,8 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceNothing; import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; +import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; import de.tum.cit.aet.artemis.core.service.connectors.SAML2Service; /** @@ -51,12 +55,15 @@ public class PublicUserJwtResource { private final JWTCookieService jwtCookieService; + private final TokenProvider tokenProvider; + private final AuthenticationManager authenticationManager; private final Optional saml2Service; - public PublicUserJwtResource(JWTCookieService jwtCookieService, AuthenticationManager authenticationManager, Optional saml2Service) { + public PublicUserJwtResource(JWTCookieService jwtCookieService, TokenProvider tokenProvider, AuthenticationManager authenticationManager, Optional saml2Service) { this.jwtCookieService = jwtCookieService; + this.tokenProvider = tokenProvider; this.authenticationManager = authenticationManager; this.saml2Service = saml2Service; } @@ -97,21 +104,30 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @Re } /** - * Sends the token back as either a cookie or a bearer token + * Sends a theia token back as cookie and bearer token * + * @param request HTTP request * @param response HTTP response * @return the ResponseEntity with status 200 (ok), 401 (unauthorized) */ - @PostMapping("re-key") + @PostMapping("theia-token") @EnforceAtLeastStudent - public ResponseEntity reKey(@RequestParam(value = "as-bearer", defaultValue = "false") boolean asBearer, HttpServletResponse response) { - ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(true); - if (asBearer) { - return ResponseEntity.ok(responseCookie.getValue()); + public ResponseEntity reKey(HttpServletRequest request, HttpServletResponse response) { + // remaining time in milliseconds + var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); + if (jwtToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); - return ResponseEntity.ok().build(); + // get validity of the token + long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - new Date().getTime(); + + // 1 day validity + long maxDuration = Duration.ofDays(1).get(ChronoUnit.MILLIS); + ResponseCookie responseCookie = jwtCookieService.buildTheiaCookie(Math.min(tokenRemainingTime, maxDuration)); + + response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); + return ResponseEntity.ok(responseCookie.getValue()); } /** From ddd88023b9e32466b9962458f17d89e5a779fe54 Mon Sep 17 00:00:00 2001 From: dennis Date: Fri, 4 Oct 2024 16:28:18 +0200 Subject: [PATCH 05/27] fix conversion from day to millis --- .../cit/aet/artemis/core/web/open/PublicUserJwtResource.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 692d70be96db..d7f2eea168a4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -3,7 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.time.Duration; -import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.Optional; @@ -123,7 +122,7 @@ public ResponseEntity reKey(HttpServletRequest request, HttpServletRespo long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - new Date().getTime(); // 1 day validity - long maxDuration = Duration.ofDays(1).get(ChronoUnit.MILLIS); + long maxDuration = Duration.ofDays(1).toMillis(); ResponseCookie responseCookie = jwtCookieService.buildTheiaCookie(Math.min(tokenRemainingTime, maxDuration)); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); From 07afea21a66aa07d534d5ff695455ec6344c5005 Mon Sep 17 00:00:00 2001 From: dennis Date: Fri, 4 Oct 2024 21:20:00 +0200 Subject: [PATCH 06/27] only respond as cookie if asked for --- .../aet/artemis/core/web/open/PublicUserJwtResource.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index d7f2eea168a4..7663d1447b53 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import de.tum.cit.aet.artemis.core.dto.vm.LoginVM; @@ -111,7 +112,8 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @Re */ @PostMapping("theia-token") @EnforceAtLeastStudent - public ResponseEntity reKey(HttpServletRequest request, HttpServletResponse response) { + public ResponseEntity getTheiaToken(@RequestParam(name = "as-cookie", defaultValue = "false") boolean asCookie, HttpServletRequest request, + HttpServletResponse response) { // remaining time in milliseconds var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); if (jwtToken == null) { @@ -125,7 +127,9 @@ public ResponseEntity reKey(HttpServletRequest request, HttpServletRespo long maxDuration = Duration.ofDays(1).toMillis(); ResponseCookie responseCookie = jwtCookieService.buildTheiaCookie(Math.min(tokenRemainingTime, maxDuration)); - response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); + if (asCookie) { + response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); + } return ResponseEntity.ok(responseCookie.getValue()); } From b59f8adb3c1fc0fbf44ffdf6c3dc967842b9ac64 Mon Sep 17 00:00:00 2001 From: dennis Date: Sat, 5 Oct 2024 17:41:59 +0200 Subject: [PATCH 07/27] fix class cast error for websockets --- .../websocket/WebsocketConfiguration.java | 3 +-- .../artemis/core/security/jwt/JWTFilter.java | 24 +++++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index 03bf213a08d8..1e38341e6ae9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -19,7 +19,6 @@ import java.util.regex.Pattern; import jakarta.annotation.Nullable; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.constraints.NotNull; import org.slf4j.Logger; @@ -202,7 +201,7 @@ public boolean beforeHandshake(@NotNull ServerHttpRequest request, @NotNull Serv @NotNull Map attributes) { if (request instanceof ServletServerHttpRequest servletRequest) { attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); - return JWTFilter.extractValidJwt((HttpServletRequest) servletRequest, tokenProvider) != null; + return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), tokenProvider) != null; } return false; } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index a180abef77bb..16b5e5d2d42b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -2,6 +2,7 @@ import java.io.IOException; +import jakarta.annotation.Nullable; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; @@ -9,7 +10,6 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; -import org.springframework.lang.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; @@ -33,8 +33,6 @@ public JWTFilter(TokenProvider tokenProvider) { public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; - // check if valid JWT token is in the cookie or in the Authorization header - // then proceed to do authentication with this token String jwtToken = extractValidJwt(httpServletRequest, this.tokenProvider); if (jwtToken != null) { Authentication authentication = this.tokenProvider.getAuthentication(jwtToken); @@ -45,11 +43,11 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } /** - * // Extracts the first valid jwt token found in the cookie or the Authorization header + * Extracts the first valid jwt found in the cookie or the Authorization header * * @param httpServletRequest the http request - * @param tokenProvider the artemis token provider used to generate and validate jwt's - * @return the valid jwt token or null if not found or invalid + * @param tokenProvider the Artemis token provider used to generate and validate jwt's + * @return the valid jwt or null if not found or invalid */ public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) { String jwtToken = getJwtFromCookie(WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME)); @@ -64,10 +62,10 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } /** - * Extracts the jwt token from the cookie + * Extracts the jwt from the cookie * * @param jwtCookie the cookie with Key "jwt" - * @return the jwt token or null if not found + * @return the jwt or null if not found */ private static @Nullable String getJwtFromCookie(Cookie jwtCookie) { if (jwtCookie == null) { @@ -77,10 +75,10 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } /** - * Extracts the jwt token from the Authorization header + * Extracts the jwt from the Authorization header * * @param jwtBearer the content of the Authorization header - * @return the jwt token or null if not found + * @return the jwt or null if not found */ private static @Nullable String getJwtFromBearer(String jwtBearer) { if (!StringUtils.hasText(jwtBearer) || !jwtBearer.startsWith("Bearer ")) { @@ -91,10 +89,10 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } /** - * Checks if the jwt token is valid + * Checks if the jwt is valid * - * @param tokenProvider the artemis token provider used to generate and validate jwt's - * @param jwtToken the jwt token + * @param tokenProvider the Artemis token provider used to generate and validate jwt's + * @param jwtToken the jwt * @return true if the jwt is valid, false if missing or invalid */ private static boolean isJwtValid(TokenProvider tokenProvider, String jwtToken) { From 896fd4c24f9a9fc0d1870962e5e567fb789a9dc5 Mon Sep 17 00:00:00 2001 From: dennis Date: Mon, 7 Oct 2024 10:36:14 +0200 Subject: [PATCH 08/27] let login return json with token --- .../cit/aet/artemis/core/web/open/PublicUserJwtResource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index f32d13f006e7..90020572f571 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import java.util.Map; import java.util.Optional; import jakarta.servlet.ServletException; @@ -69,7 +70,7 @@ public PublicUserJwtResource(JWTCookieService jwtCookieService, AuthenticationMa */ @PostMapping("authenticate") @EnforceNothing - public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) { + public ResponseEntity> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) { var username = loginVM.getUsername(); var password = loginVM.getPassword(); @@ -86,7 +87,7 @@ public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, @Re ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); - return ResponseEntity.ok(responseCookie.getValue()); + return ResponseEntity.ok(Map.of("access_token", responseCookie.getValue())); } catch (BadCredentialsException ex) { log.warn("Wrong credentials during login for user {}", loginVM.getUsername()); From 2e247db4e76a110b1b9e1cb631bcfb38f1d8f2b1 Mon Sep 17 00:00:00 2001 From: dennis Date: Wed, 6 Nov 2024 22:33:24 +0100 Subject: [PATCH 09/27] add server test for bearer token --- .../AuthenticationIntegrationTestHelper.java | 5 +++++ ...InternalAuthenticationIntegrationTest.java | 1 + .../core/security/jwt/JWTFilterTest.java | 19 ++++++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java index bc10fb0d9dce..a9f5f901f48c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java @@ -21,4 +21,9 @@ public static void authenticationCookieAssertions(Cookie cookie, boolean logoutC assertThat(cookie.getValue()).isNotEmpty(); } } + + public static void authenticationBearerTokenAssertions(String authorizationHeader) { + assertThat(authorizationHeader).isNotNull(); + assertThat(authorizationHeader).startsWith("Bearer "); + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java index 84ba3d693707..e415a6039999 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java @@ -227,6 +227,7 @@ void testJWTAuthentication() throws Exception { MockHttpServletResponse response = request.postWithoutResponseBody("/api/public/authenticate", loginVM, HttpStatus.OK, httpHeaders); AuthenticationIntegrationTestHelper.authenticationCookieAssertions(response.getCookie("jwt"), false); + AuthenticationIntegrationTestHelper.authenticationBearerTokenAssertions(response.getHeader("Authorization")); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilterTest.java b/src/test/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilterTest.java index 59392188c127..8c0e934e37b7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilterTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilterTest.java @@ -47,7 +47,7 @@ void setup() { } @Test - void testJWTFilter() throws Exception { + void testJWTFilterCookie() throws Exception { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); String jwt = tokenProvider.createToken(authentication, false); @@ -61,6 +61,23 @@ void testJWTFilter() throws Exception { assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("test-user"); } + @Test + void testJWTFilterBearer() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", + Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + + String jwt = tokenProvider.createToken(authentication, false); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + jwt); + request.setRequestURI("/api/test"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + jwtFilter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(SecurityContextHolder.getContext().getAuthentication().getName()).isEqualTo("test-user"); + } + @Test void testJWTFilterInvalidToken() throws Exception { String jwt = "wrong_jwt"; From 02e63b78214bfe8ac765fe6f507bae015583939e Mon Sep 17 00:00:00 2001 From: dennis Date: Fri, 8 Nov 2024 20:36:39 +0100 Subject: [PATCH 10/27] change theia token to general tool token endpoint --- .../core/security/jwt/JWTCookieService.java | 6 +++--- .../core/security/jwt/TokenProvider.java | 17 ++++++++++------- .../artemis/core/security/jwt/TokenTool.java | 5 +++++ .../core/web/open/PublicUserJwtResource.java | 15 +++++++++------ 4 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenTool.java diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java index f541641c0741..dcfbdd111da0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java @@ -42,13 +42,13 @@ public ResponseCookie buildLoginCookie(boolean rememberMe) { } /** - * Builds the cookie with Theia flag + * Builds a cookie with the tool claim in the jwt * * @param duration the duration of the cookie and the jwt * @return the login ResponseCookie containing the JWT */ - public ResponseCookie buildTheiaCookie(long duration) { - String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), duration, "THEIA"); + public ResponseCookie buildToolCookie(long duration, TokenTool tool) { + String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), duration, tool); return buildJWTCookie(jwt, Duration.of(duration, ChronoUnit.MILLIS)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index 65e8c410ee38..84b0b9025a8a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.stream.Collectors; +import jakarta.annotation.Nullable; import jakarta.annotation.PostConstruct; import javax.crypto.SecretKey; @@ -95,7 +96,7 @@ public long getTokenValidity(boolean rememberMe) { * @return JWT Token */ public String createToken(Authentication authentication, boolean rememberMe) { - return createToken(authentication, getTokenValidity(rememberMe)); + return createToken(authentication, getTokenValidity(rememberMe), null); } /** @@ -103,18 +104,20 @@ public String createToken(Authentication authentication, boolean rememberMe) { * * @param authentication Authentication Object * @param duration the Token lifetime - * @param tools tools this token is used for + * @param tool tool this token is used for. If null, it's a general access token * @return JWT Token */ - public String createToken(Authentication authentication, long duration, String... tools) { + public String createToken(Authentication authentication, long duration, @Nullable TokenTool tool) { String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")); - String toolClaims = String.join(",", tools); - long now = (new Date()).getTime(); Date validity = new Date(now + duration); - return Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities).claim("tools", toolClaims).signWith(key, Jwts.SIG.HS512).expiration(validity) - .compact(); + var jwtBuilder = Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities); + if (tool != null) { + jwtBuilder.claim("tools", tool); + } + + return jwtBuilder.signWith(key, Jwts.SIG.HS512).expiration(validity).compact(); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenTool.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenTool.java new file mode 100644 index 000000000000..a759759ba20f --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenTool.java @@ -0,0 +1,5 @@ +package de.tum.cit.aet.artemis.core.security.jwt; + +public enum TokenTool { + THEIA +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 281bede299fd..518e9fe3a074 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -42,6 +42,7 @@ import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; +import de.tum.cit.aet.artemis.core.security.jwt.TokenTool; import de.tum.cit.aet.artemis.core.service.connectors.SAML2Service; /** @@ -105,16 +106,18 @@ public ResponseEntity> authorize(@Valid @RequestBody LoginVM } /** - * Sends a theia token back as cookie and bearer token + * Sends a tool token back as cookie or bearer token * + * @param tool the tool for which the token is requested + * @param asCookie if true the token is sent back as a cookie * @param request HTTP request * @param response HTTP response - * @return the ResponseEntity with status 200 (ok), 401 (unauthorized) + * @return the ResponseEntity with status 200 (ok), 401 (unauthorized) and depending on the asCookie flag a bearer token in the body */ - @PostMapping("theia-token") + @PostMapping("tool-token") @EnforceAtLeastStudent - public ResponseEntity getTheiaToken(@RequestParam(name = "as-cookie", defaultValue = "false") boolean asCookie, HttpServletRequest request, - HttpServletResponse response) { + public ResponseEntity getToolToken(@RequestParam(name = "tool", required = true) TokenTool tool, + @RequestParam(name = "as-cookie", defaultValue = "false") boolean asCookie, HttpServletRequest request, HttpServletResponse response) { // remaining time in milliseconds var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); if (jwtToken == null) { @@ -126,7 +129,7 @@ public ResponseEntity getTheiaToken(@RequestParam(name = "as-cookie", de // 1 day validity long maxDuration = Duration.ofDays(1).toMillis(); - ResponseCookie responseCookie = jwtCookieService.buildTheiaCookie(Math.min(tokenRemainingTime, maxDuration)); + ResponseCookie responseCookie = jwtCookieService.buildToolCookie(Math.min(tokenRemainingTime, maxDuration), tool); if (asCookie) { response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); From 44ca43cf99dbe9d3392c0c8f98bb14fdaef8b626 Mon Sep 17 00:00:00 2001 From: Dennis Jandow <95364200+janthoXO@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:39:28 +0100 Subject: [PATCH 11/27] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add to docs comment for duration "in milli seconds" Co-authored-by: Johannes Stöhr <38322605+JohannesStoehr@users.noreply.github.com> --- .../tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java | 2 +- .../de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java index dcfbdd111da0..a854a08b5223 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java @@ -44,7 +44,7 @@ public ResponseCookie buildLoginCookie(boolean rememberMe) { /** * Builds a cookie with the tool claim in the jwt * - * @param duration the duration of the cookie and the jwt + * @param duration the duration of the cookie in milli seconds and the jwt * @return the login ResponseCookie containing the JWT */ public ResponseCookie buildToolCookie(long duration, TokenTool tool) { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index 84b0b9025a8a..0781dae2603b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -103,7 +103,7 @@ public String createToken(Authentication authentication, boolean rememberMe) { * Create JWT Token a fully populated Authentication object. * * @param authentication Authentication Object - * @param duration the Token lifetime + * @param duration the Token lifetime in milli seconds * @param tool tool this token is used for. If null, it's a general access token * @return JWT Token */ From 6cab497efc903dad1cb18c871d0e2e7994150696 Mon Sep 17 00:00:00 2001 From: dennis Date: Sat, 9 Nov 2024 23:05:31 +0100 Subject: [PATCH 12/27] add tool token annotation --- .../config/PublicResourcesConfiguration.java | 12 +++- .../security/allowedTools/AllowedTools.java | 13 ++++ .../security/allowedTools/ToolTokenType.java | 5 ++ .../allowedTools/ToolsInterceptor.java | 61 +++++++++++++++++++ .../core/security/jwt/JWTCookieService.java | 4 +- .../core/security/jwt/TokenProvider.java | 8 ++- .../artemis/core/security/jwt/TokenTool.java | 5 -- .../aet/artemis/core/web/CourseResource.java | 3 + .../core/web/open/PublicUserJwtResource.java | 4 +- 9 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedTools.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolTokenType.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenTool.java diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/PublicResourcesConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/PublicResourcesConfiguration.java index e979c66b6183..d038e8af0256 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/PublicResourcesConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/PublicResourcesConfiguration.java @@ -14,10 +14,12 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.http.CacheControl; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolsInterceptor; import tech.jhipster.config.JHipsterProperties; /** @@ -29,8 +31,11 @@ public class PublicResourcesConfiguration implements WebMvcConfigurer { private final JHipsterProperties jHipsterProperties; - public PublicResourcesConfiguration(JHipsterProperties jHipsterProperties) { + private final ToolsInterceptor toolsInterceptor; + + public PublicResourcesConfiguration(JHipsterProperties jHipsterProperties, ToolsInterceptor toolsInterceptor) { this.jHipsterProperties = jHipsterProperties; + this.toolsInterceptor = toolsInterceptor; } @Value("${artemis.file-upload-path}") @@ -108,4 +113,9 @@ private static String getFileSystemPublicSubPathResourceLocation(String... subPa var morePaths = Stream.concat(Stream.of("public"), Arrays.stream(subPaths)).toArray(String[]::new); return "file:" + Path.of(userDir, morePaths) + "/"; } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(toolsInterceptor); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedTools.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedTools.java new file mode 100644 index 000000000000..d183d60b2607 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedTools.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AllowedTools { + + ToolTokenType[] value(); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolTokenType.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolTokenType.java new file mode 100644 index 000000000000..4159656d94bf --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolTokenType.java @@ -0,0 +1,5 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +public enum ToolTokenType { + SCORPIO +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java new file mode 100644 index 000000000000..d5ff87a96fec --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java @@ -0,0 +1,61 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; + +@Component +public class ToolsInterceptor implements HandlerInterceptor { + + private final TokenProvider tokenProvider; + + public ToolsInterceptor(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); + + if (handler instanceof HandlerMethod && jwtToken != null) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + + // Check if the method or its class has the @AllowedTools annotation + AllowedTools allowedToolsAnnotation = method.getAnnotation(AllowedTools.class); + if (allowedToolsAnnotation == null) { + allowedToolsAnnotation = method.getDeclaringClass().getAnnotation(AllowedTools.class); + } + + // Extract the "tools" claim from the JWT token + String toolsClaim = tokenProvider.getClaim(jwtToken, "tools"); + + // If no @AllowedTools annotation is present and the token is a tool token, reject the request + if (allowedToolsAnnotation == null && toolsClaim != null) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied due to 'tools' claim."); + return false; + } + + // If @AllowedTools is present, check if the toolsClaim is among the allowed values + if (allowedToolsAnnotation != null && toolsClaim != null) { + ToolTokenType[] allowedTools = allowedToolsAnnotation.value(); + // no match between allowed tools and tools claim + var toolsClaimList = toolsClaim.split(","); + if (Arrays.stream(allowedTools).noneMatch(tool -> Arrays.asList(toolsClaimList).contains(tool.toString()))) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied due to 'tools' claim."); + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java index dcfbdd111da0..c2dc2cfe72e2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java @@ -14,6 +14,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; + @Profile(PROFILE_CORE) @Service public class JWTCookieService { @@ -47,7 +49,7 @@ public ResponseCookie buildLoginCookie(boolean rememberMe) { * @param duration the duration of the cookie and the jwt * @return the login ResponseCookie containing the JWT */ - public ResponseCookie buildToolCookie(long duration, TokenTool tool) { + public ResponseCookie buildToolCookie(long duration, ToolTokenType tool) { String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), duration, tool); return buildJWTCookie(jwt, Duration.of(duration, ChronoUnit.MILLIS)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index 84b0b9025a8a..2b3915d6c83e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -25,6 +25,7 @@ import org.springframework.util.StringUtils; import de.tum.cit.aet.artemis.core.management.SecurityMetersService; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; @@ -107,7 +108,7 @@ public String createToken(Authentication authentication, boolean rememberMe) { * @param tool tool this token is used for. If null, it's a general access token * @return JWT Token */ - public String createToken(Authentication authentication, long duration, @Nullable TokenTool tool) { + public String createToken(Authentication authentication, long duration, @Nullable ToolTokenType tool) { String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")); long now = (new Date()).getTime(); @@ -188,6 +189,11 @@ private Claims parseClaims(String authToken) { return Jwts.parser().verifyWith(key).build().parseSignedClaims(authToken).getPayload(); } + public String getClaim(String token, String claimName) { + Claims claims = parseClaims(token); + return claims.get(claimName, String.class); + } + public Date getExpirationDate(String authToken) { return parseClaims(authToken).getExpiration(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenTool.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenTool.java deleted file mode 100644 index a759759ba20f..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenTool.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.tum.cit.aet.artemis.core.security.jwt; - -public enum TokenTool { - THEIA -} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index 16b7dd554842..fb1b758a6442 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -94,6 +94,8 @@ import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastEditor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; @@ -685,6 +687,7 @@ public record CourseDropdownDTO(Long id, String title, String courseIcon) { */ @GetMapping("courses/for-dashboard") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity getCoursesForDashboard() { long timeNanoStart = System.nanoTime(); User user = userRepository.getUserWithGroupsAndAuthorities(); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 518e9fe3a074..29d63176f84b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -37,12 +37,12 @@ import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.security.SecurityUtils; import de.tum.cit.aet.artemis.core.security.UserNotActivatedException; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceNothing; import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; -import de.tum.cit.aet.artemis.core.security.jwt.TokenTool; import de.tum.cit.aet.artemis.core.service.connectors.SAML2Service; /** @@ -116,7 +116,7 @@ public ResponseEntity> authorize(@Valid @RequestBody LoginVM */ @PostMapping("tool-token") @EnforceAtLeastStudent - public ResponseEntity getToolToken(@RequestParam(name = "tool", required = true) TokenTool tool, + public ResponseEntity getToolToken(@RequestParam(name = "tool", required = true) ToolTokenType tool, @RequestParam(name = "as-cookie", defaultValue = "false") boolean asCookie, HttpServletRequest request, HttpServletResponse response) { // remaining time in milliseconds var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); From 6e61b267de80ca42721c666b3ef3d50bdc0b1be7 Mon Sep 17 00:00:00 2001 From: dennis Date: Sat, 9 Nov 2024 23:16:31 +0100 Subject: [PATCH 13/27] move interceptor registration --- .../core/config/PublicResourcesConfiguration.java | 12 +----------- .../aet/artemis/core/config/WebConfigurer.java | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/PublicResourcesConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/PublicResourcesConfiguration.java index d038e8af0256..e979c66b6183 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/PublicResourcesConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/PublicResourcesConfiguration.java @@ -14,12 +14,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.http.CacheControl; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import de.tum.cit.aet.artemis.core.security.allowedTools.ToolsInterceptor; import tech.jhipster.config.JHipsterProperties; /** @@ -31,11 +29,8 @@ public class PublicResourcesConfiguration implements WebMvcConfigurer { private final JHipsterProperties jHipsterProperties; - private final ToolsInterceptor toolsInterceptor; - - public PublicResourcesConfiguration(JHipsterProperties jHipsterProperties, ToolsInterceptor toolsInterceptor) { + public PublicResourcesConfiguration(JHipsterProperties jHipsterProperties) { this.jHipsterProperties = jHipsterProperties; - this.toolsInterceptor = toolsInterceptor; } @Value("${artemis.file-upload-path}") @@ -113,9 +108,4 @@ private static String getFileSystemPublicSubPathResourceLocation(String... subPa var morePaths = Stream.concat(Stream.of("public"), Arrays.stream(subPaths)).toArray(String[]::new); return "file:" + Path.of(userDir, morePaths) + "/"; } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(toolsInterceptor); - } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java b/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java index 646fa942e7e0..4fdddc68705e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/WebConfigurer.java @@ -27,7 +27,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolsInterceptor; import de.tum.cit.aet.artemis.core.security.filter.CachingHttpHeadersFilter; import tech.jhipster.config.JHipsterProperties; @@ -36,7 +39,7 @@ */ @Profile(PROFILE_CORE) @Configuration -public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer { +public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer, WebMvcConfigurer { private static final Logger log = LoggerFactory.getLogger(WebConfigurer.class); @@ -44,9 +47,12 @@ public class WebConfigurer implements ServletContextInitializer, WebServerFactor private final JHipsterProperties jHipsterProperties; - public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) { + private final ToolsInterceptor toolsInterceptor; + + public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties, ToolsInterceptor toolsInterceptor) { this.env = env; this.jHipsterProperties = jHipsterProperties; + this.toolsInterceptor = toolsInterceptor; } @Override @@ -125,4 +131,9 @@ public CorsFilter corsFilter() { } return new CorsFilter(source); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(toolsInterceptor); + } } From 149a53a48276ae79d5e584105dd47149e29761f4 Mon Sep 17 00:00:00 2001 From: dennis Date: Mon, 11 Nov 2024 11:54:33 +0100 Subject: [PATCH 14/27] add tool token option to login --- .../core/security/jwt/JWTCookieService.java | 18 ++++++++++++++---- .../core/web/open/PublicUserJwtResource.java | 11 ++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java index 9060e84190b0..88d8d6405d51 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTCookieService.java @@ -38,18 +38,28 @@ public JWTCookieService(TokenProvider tokenProvider, Environment environment) { * @return the login ResponseCookie containing the JWT */ public ResponseCookie buildLoginCookie(boolean rememberMe) { - String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), rememberMe); - Duration duration = Duration.of(tokenProvider.getTokenValidity(rememberMe), ChronoUnit.MILLIS); - return buildJWTCookie(jwt, duration); + return buildLoginCookie(rememberMe, null); + } + + /** + * Builds the cookie containing the jwt for a login + * + * @param rememberMe boolean used to determine the duration of the jwt. + * @param tool the tool claim in the jwt + * @return the login ResponseCookie containing the JWT + */ + public ResponseCookie buildLoginCookie(boolean rememberMe, ToolTokenType tool) { + return buildLoginCookie(tokenProvider.getTokenValidity(rememberMe), tool); } /** * Builds a cookie with the tool claim in the jwt * * @param duration the duration of the cookie in milli seconds and the jwt + * @param tool the tool claim in the jwt * @return the login ResponseCookie containing the JWT */ - public ResponseCookie buildToolCookie(long duration, ToolTokenType tool) { + public ResponseCookie buildLoginCookie(long duration, ToolTokenType tool) { String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), duration, tool); return buildJWTCookie(jwt, Duration.of(duration, ChronoUnit.MILLIS)); } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 29d63176f84b..47ea41c9f2dc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -80,7 +80,8 @@ public PublicUserJwtResource(JWTCookieService jwtCookieService, TokenProvider to */ @PostMapping("authenticate") @EnforceNothing - public ResponseEntity> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, HttpServletResponse response) { + public ResponseEntity> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, + @RequestParam(name = "tool", required = false) ToolTokenType tool, HttpServletResponse response) { var username = loginVM.getUsername(); var password = loginVM.getPassword(); @@ -94,7 +95,7 @@ public ResponseEntity> authorize(@Valid @RequestBody LoginVM SecurityContextHolder.getContext().setAuthentication(authentication); boolean rememberMe = loginVM.isRememberMe() != null && loginVM.isRememberMe(); - ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe); + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe, tool); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); return ResponseEntity.ok(Map.of("access_token", responseCookie.getValue())); @@ -116,7 +117,7 @@ public ResponseEntity> authorize(@Valid @RequestBody LoginVM */ @PostMapping("tool-token") @EnforceAtLeastStudent - public ResponseEntity getToolToken(@RequestParam(name = "tool", required = true) ToolTokenType tool, + public ResponseEntity convertCookieToToolToken(@RequestParam(name = "tool", required = true) ToolTokenType tool, @RequestParam(name = "as-cookie", defaultValue = "false") boolean asCookie, HttpServletRequest request, HttpServletResponse response) { // remaining time in milliseconds var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); @@ -129,7 +130,7 @@ public ResponseEntity getToolToken(@RequestParam(name = "tool", required // 1 day validity long maxDuration = Duration.ofDays(1).toMillis(); - ResponseCookie responseCookie = jwtCookieService.buildToolCookie(Math.min(tokenRemainingTime, maxDuration), tool); + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(Math.min(tokenRemainingTime, maxDuration), tool); if (asCookie) { response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); @@ -169,7 +170,7 @@ public ResponseEntity authorizeSAML2(@RequestBody final String body, HttpS } final boolean rememberMe = Boolean.parseBoolean(body); - ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe); + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(rememberMe, null); response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); return ResponseEntity.ok().build(); From b0ed73d282763baf71e8a596323570f0200a432c Mon Sep 17 00:00:00 2001 From: dennis Date: Mon, 11 Nov 2024 14:51:32 +0100 Subject: [PATCH 15/27] change filter chain to only accept one auth method --- .../websocket/WebsocketConfiguration.java | 5 +- .../artemis/core/security/jwt/JWTFilter.java | 48 ++++++++++++++----- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index 1e38341e6ae9..774128bac56e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.converter.MappingJackson2MessageConverter; @@ -199,9 +200,9 @@ public HandshakeInterceptor httpSessionHandshakeInterceptor() { @Override public boolean beforeHandshake(@NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response, @NotNull WebSocketHandler wsHandler, @NotNull Map attributes) { - if (request instanceof ServletServerHttpRequest servletRequest) { + if (request instanceof ServletServerHttpRequest servletRequest && response instanceof ServletServerHttpResponse servletResponse) { attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); - return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), tokenProvider) != null; + return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), servletResponse.getServletResponse(), tokenProvider) != null; } return false; } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index 16b5e5d2d42b..5a41c8fda077 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -9,6 +9,7 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -32,33 +33,54 @@ public JWTFilter(TokenProvider tokenProvider) { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; - String jwtToken = extractValidJwt(httpServletRequest, this.tokenProvider); + String jwtToken = extractValidJwt(httpServletRequest, httpServletResponse, this.tokenProvider); if (jwtToken != null) { Authentication authentication = this.tokenProvider.getAuthentication(jwtToken); SecurityContextHolder.getContext().setAuthentication(authentication); } + else if (httpServletResponse.getStatus() == HttpServletResponse.SC_BAD_REQUEST) { + return; // Stop further processing if a bad request status is set + } filterChain.doFilter(servletRequest, servletResponse); } /** - * Extracts the first valid jwt found in the cookie or the Authorization header + * Extracts the valid jwt found in the cookie or the Authorization header * * @param httpServletRequest the http request * @param tokenProvider the Artemis token provider used to generate and validate jwt's * @return the valid jwt or null if not found or invalid */ - public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) { - String jwtToken = getJwtFromCookie(WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME)); - if (isJwtValid(tokenProvider, jwtToken)) { - return jwtToken; + public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, TokenProvider tokenProvider) { + var cookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); + var authHeader = httpServletRequest.getHeader("Authorization"); + + if (cookie == null && authHeader == null) { + return null; + } + + if (cookie != null && authHeader != null) { + // Single Method Enforcement: Only one method of authentication is allowed + httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return null; } - jwtToken = getJwtFromBearer(httpServletRequest.getHeader("Authorization")); - if (isJwtValid(tokenProvider, jwtToken)) { - return jwtToken; + + String jwtToken; + if (cookie != null) { + jwtToken = getJwtFromCookie(cookie); + } + else { + jwtToken = getJwtFromBearer(authHeader); + } + + if (!isJwtValid(tokenProvider, jwtToken)) { + return null; } - return null; + + return jwtToken; } /** @@ -67,7 +89,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo * @param jwtCookie the cookie with Key "jwt" * @return the jwt or null if not found */ - private static @Nullable String getJwtFromCookie(Cookie jwtCookie) { + private static @Nullable String getJwtFromCookie(@Nullable Cookie jwtCookie) { if (jwtCookie == null) { return null; } @@ -80,7 +102,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo * @param jwtBearer the content of the Authorization header * @return the jwt or null if not found */ - private static @Nullable String getJwtFromBearer(String jwtBearer) { + private static @Nullable String getJwtFromBearer(@Nullable String jwtBearer) { if (!StringUtils.hasText(jwtBearer) || !jwtBearer.startsWith("Bearer ")) { return null; } @@ -95,7 +117,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo * @param jwtToken the jwt * @return true if the jwt is valid, false if missing or invalid */ - private static boolean isJwtValid(TokenProvider tokenProvider, String jwtToken) { + private static boolean isJwtValid(TokenProvider tokenProvider, @Nullable String jwtToken) { return StringUtils.hasText(jwtToken) && tokenProvider.validateTokenForAuthority(jwtToken); } } From 8df1a6b8ecfe153e5fa96869f28c447f2798b2cd Mon Sep 17 00:00:00 2001 From: dennis Date: Mon, 11 Nov 2024 14:57:30 +0100 Subject: [PATCH 16/27] write filter a bit nicer --- .../tum/cit/aet/artemis/core/security/jwt/JWTFilter.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index 5a41c8fda077..29436c348310 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -68,13 +68,7 @@ else if (httpServletResponse.getStatus() == HttpServletResponse.SC_BAD_REQUEST) return null; } - String jwtToken; - if (cookie != null) { - jwtToken = getJwtFromCookie(cookie); - } - else { - jwtToken = getJwtFromBearer(authHeader); - } + String jwtToken = cookie != null ? getJwtFromCookie(cookie) : getJwtFromBearer(authHeader); if (!isJwtValid(tokenProvider, jwtToken)) { return null; From 9f61018f257e5859a4b3ba33e1266e00dba4b8ef Mon Sep 17 00:00:00 2001 From: dennis Date: Tue, 12 Nov 2024 13:08:44 +0100 Subject: [PATCH 17/27] annotate endpoints required by Scorpio --- .../java/de/tum/cit/aet/artemis/core/web/CourseResource.java | 1 + .../cit/aet/artemis/exercise/web/ParticipationResource.java | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java index fb1b758a6442..76aae6ed5def 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/CourseResource.java @@ -623,6 +623,7 @@ public ResponseEntity> getCoursesForEnrollment() { // TODO: we should rename this into courses/{courseId}/details @GetMapping("courses/{courseId}/for-dashboard") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity getCourseForDashboard(@PathVariable long courseId) { long timeNanoStart = System.nanoTime(); log.debug("REST request to get one course {} with exams, lectures, exercises, participations, submissions and results, etc.", courseId); diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java index 4d549bb3fe66..94742de8655f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/web/ParticipationResource.java @@ -55,6 +55,8 @@ import de.tum.cit.aet.artemis.core.repository.CourseRepository; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; @@ -219,6 +221,7 @@ public ParticipationResource(ParticipationService participationService, Programm */ @PostMapping("exercises/{exerciseId}/participations") @EnforceAtLeastStudentInExercise + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity startParticipation(@PathVariable Long exerciseId) throws URISyntaxException { log.debug("REST request to start Exercise : {}", exerciseId); Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); @@ -775,6 +778,7 @@ public ResponseEntity getParticipationBuildArtifact(@PathVariable Long p */ @GetMapping("exercises/{exerciseId}/participation") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity getParticipationForCurrentUser(@PathVariable Long exerciseId, Principal principal) { log.debug("REST request to get Participation for Exercise : {}", exerciseId); Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId); From 5908f73f4743561d0483b7909e7960ef592e220b Mon Sep 17 00:00:00 2001 From: dennis Date: Tue, 12 Nov 2024 14:22:02 +0100 Subject: [PATCH 18/27] change bad request behaviour --- .../websocket/WebsocketConfiguration.java | 14 ++++++++++---- .../security/allowedTools/ToolsInterceptor.java | 9 ++++++++- .../artemis/core/security/jwt/JWTFilter.java | 17 ++++++++++------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index f72a42a87d68..4a5dfdacfa24 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -27,11 +27,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatusCode; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.converter.MappingJackson2MessageConverter; @@ -199,9 +199,15 @@ public HandshakeInterceptor httpSessionHandshakeInterceptor() { @Override public boolean beforeHandshake(@NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response, @NotNull WebSocketHandler wsHandler, @NotNull Map attributes) { - if (request instanceof ServletServerHttpRequest servletRequest && response instanceof ServletServerHttpResponse servletResponse) { - attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); - return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), servletResponse.getServletResponse(), tokenProvider) != null; + if (request instanceof ServletServerHttpRequest servletRequest) { + try { + attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); + return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), tokenProvider) != null; + } + catch (IllegalArgumentException e) { + response.setStatusCode(HttpStatusCode.valueOf(400)); + return false; + } } return false; } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java index d5ff87a96fec..0d257d287575 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java @@ -24,7 +24,14 @@ public ToolsInterceptor(TokenProvider tokenProvider) { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); + String jwtToken; + try { + jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); + } + catch (IllegalArgumentException e) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return false; + } if (handler instanceof HandlerMethod && jwtToken != null) { HandlerMethod handlerMethod = (HandlerMethod) handler; diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index 29436c348310..e5438d1f214f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -34,15 +34,19 @@ public JWTFilter(TokenProvider tokenProvider) { public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + String jwtToken; + try { + jwtToken = extractValidJwt(httpServletRequest, this.tokenProvider); + } + catch (IllegalArgumentException e) { + httpServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } - String jwtToken = extractValidJwt(httpServletRequest, httpServletResponse, this.tokenProvider); if (jwtToken != null) { Authentication authentication = this.tokenProvider.getAuthentication(jwtToken); SecurityContextHolder.getContext().setAuthentication(authentication); } - else if (httpServletResponse.getStatus() == HttpServletResponse.SC_BAD_REQUEST) { - return; // Stop further processing if a bad request status is set - } filterChain.doFilter(servletRequest, servletResponse); } @@ -54,7 +58,7 @@ else if (httpServletResponse.getStatus() == HttpServletResponse.SC_BAD_REQUEST) * @param tokenProvider the Artemis token provider used to generate and validate jwt's * @return the valid jwt or null if not found or invalid */ - public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, TokenProvider tokenProvider) { + public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) { var cookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); var authHeader = httpServletRequest.getHeader("Authorization"); @@ -64,8 +68,7 @@ else if (httpServletResponse.getStatus() == HttpServletResponse.SC_BAD_REQUEST) if (cookie != null && authHeader != null) { // Single Method Enforcement: Only one method of authentication is allowed - httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); - return null; + throw new IllegalArgumentException("Only one method of authentication is allowed"); } String jwtToken = cookie != null ? getJwtFromCookie(cookie) : getJwtFromBearer(authHeader); From 7a69620b9fb72432443eef4e00c7ea644edfb014 Mon Sep 17 00:00:00 2001 From: artemis_admin Date: Tue, 19 Nov 2024 22:01:00 +0100 Subject: [PATCH 19/27] fix server tests --- .../allowedTools/ToolsInterceptor.java | 2 +- .../core/security/jwt/TokenProvider.java | 4 +-- .../AuthenticationIntegrationTestHelper.java | 20 +++++++++++ ...InternalAuthenticationIntegrationTest.java | 34 +++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java index 0d257d287575..c1cdcece08f3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java @@ -44,7 +44,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } // Extract the "tools" claim from the JWT token - String toolsClaim = tokenProvider.getClaim(jwtToken, "tools"); + String toolsClaim = tokenProvider.getClaim(jwtToken, "tools", String.class); // If no @AllowedTools annotation is present and the token is a tool token, reject the request if (allowedToolsAnnotation == null && toolsClaim != null) { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index df0cacd25c63..c74003bb2ab5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -189,9 +189,9 @@ private Claims parseClaims(String authToken) { return Jwts.parser().verifyWith(key).build().parseSignedClaims(authToken).getPayload(); } - public String getClaim(String token, String claimName) { + public T getClaim(String token, String claimName, Class claimType) { Claims claims = parseClaims(token); - return claims.get(claimName, String.class); + return claims.get(claimName, claimType); } public Date getExpirationDate(String authToken) { diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java index a9f5f901f48c..9a6746c44cd1 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java @@ -2,8 +2,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Date; + import jakarta.servlet.http.Cookie; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; + public class AuthenticationIntegrationTestHelper { public static void authenticationCookieAssertions(Cookie cookie, boolean logoutCookie) { @@ -26,4 +31,19 @@ public static void authenticationBearerTokenAssertions(String authorizationHeade assertThat(authorizationHeader).isNotNull(); assertThat(authorizationHeader).startsWith("Bearer "); } + + public static void toolTokenAssertions(TokenProvider tokenProvider, String token, long initialLifetime, ToolTokenType... tools) { + assertThat(token).isNotNull(); + + String[] toolClaims = tokenProvider.getClaim(token, "tools", String.class).split(","); + assertThat(toolClaims).isNotEmpty(); + for (ToolTokenType tool : tools) { + assertThat(toolClaims).contains(tool.toString()); + } + + var lifetime = tokenProvider.getExpirationDate(token).getTime() - new Date().getTime(); + // assert that the token has a lifetime of less than a day + assertThat(lifetime).isLessThan(24 * 60 * 60 * 1000); + assertThat(lifetime).isLessThan(initialLifetime); + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java index b0aa31fb76d3..e80004aff322 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java @@ -6,13 +6,17 @@ import static de.tum.cit.aet.artemis.core.domain.Authority.USER_AUTHORITY; import static de.tum.cit.aet.artemis.core.user.util.UserFactory.USER_PASSWORD; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.ZonedDateTime; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import jakarta.servlet.http.Cookie; import jakarta.validation.constraints.NotNull; import org.junit.jupiter.api.AfterEach; @@ -22,9 +26,11 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.util.LinkedMultiValueMap; import de.tum.cit.aet.artemis.core.connector.GitlabRequestMockProvider; import de.tum.cit.aet.artemis.core.domain.Authority; @@ -35,6 +41,9 @@ import de.tum.cit.aet.artemis.core.repository.AuthorityRepository; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.security.SecurityUtils; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; +import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; import de.tum.cit.aet.artemis.core.service.user.PasswordService; import de.tum.cit.aet.artemis.core.util.CourseFactory; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; @@ -50,6 +59,12 @@ class InternalAuthenticationIntegrationTest extends AbstractSpringIntegrationJen @Autowired private PasswordService passwordService; + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private JWTCookieService jwtCookieService; + @Autowired private ProgrammingExerciseTestRepository programmingExerciseRepository; @@ -230,6 +245,25 @@ void testJWTAuthentication() throws Exception { AuthenticationIntegrationTestHelper.authenticationBearerTokenAssertions(response.getHeader("Authorization")); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testTheiaTokenGeneration() throws Exception { + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(true); + + Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue()); + cookie.setMaxAge((int) responseCookie.getMaxAge().toMillis()); + + var initialLifetime = tokenProvider.getExpirationDate(cookie.getValue()).getTime() - new Date().getTime(); + + LinkedMultiValueMap params = new LinkedMultiValueMap<>(); + params.add("tool", ToolTokenType.SCORPIO.toString()); + + var responseBody = request.performMvcRequest(post("/api/public/tool-token").cookie(cookie).params(params)).andExpect(status().isOk()).andReturn().getResponse() + .getContentAsString(); + + AuthenticationIntegrationTestHelper.toolTokenAssertions(tokenProvider, responseBody, initialLifetime, ToolTokenType.SCORPIO); + } + @Test @WithAnonymousUser void testJWTAuthenticationLogoutAnonymous() throws Exception { From 9ca745bdb1c15f3180d7ee93937e22ee774b7b60 Mon Sep 17 00:00:00 2001 From: artemis_admin Date: Tue, 19 Nov 2024 22:31:02 +0100 Subject: [PATCH 20/27] adjust tests --- .../websocket/WebsocketConfiguration.java | 14 ++++++++++---- .../artemis/core/security/jwt/JWTFilter.java | 17 ++++++++++------- .../core/security/jwt/TokenProvider.java | 5 +++++ .../AuthenticationIntegrationTestHelper.java | 5 ----- .../InternalAuthenticationIntegrationTest.java | 13 ++++++++++++- .../core/security/jwt/JWTFilterTest.java | 17 +++++++++++++++++ 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java index f72a42a87d68..4a5dfdacfa24 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/config/websocket/WebsocketConfiguration.java @@ -27,11 +27,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatusCode; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.converter.MappingJackson2MessageConverter; @@ -199,9 +199,15 @@ public HandshakeInterceptor httpSessionHandshakeInterceptor() { @Override public boolean beforeHandshake(@NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response, @NotNull WebSocketHandler wsHandler, @NotNull Map attributes) { - if (request instanceof ServletServerHttpRequest servletRequest && response instanceof ServletServerHttpResponse servletResponse) { - attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); - return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), servletResponse.getServletResponse(), tokenProvider) != null; + if (request instanceof ServletServerHttpRequest servletRequest) { + try { + attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress()); + return JWTFilter.extractValidJwt(servletRequest.getServletRequest(), tokenProvider) != null; + } + catch (IllegalArgumentException e) { + response.setStatusCode(HttpStatusCode.valueOf(400)); + return false; + } } return false; } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index 29436c348310..e5438d1f214f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -34,15 +34,19 @@ public JWTFilter(TokenProvider tokenProvider) { public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + String jwtToken; + try { + jwtToken = extractValidJwt(httpServletRequest, this.tokenProvider); + } + catch (IllegalArgumentException e) { + httpServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } - String jwtToken = extractValidJwt(httpServletRequest, httpServletResponse, this.tokenProvider); if (jwtToken != null) { Authentication authentication = this.tokenProvider.getAuthentication(jwtToken); SecurityContextHolder.getContext().setAuthentication(authentication); } - else if (httpServletResponse.getStatus() == HttpServletResponse.SC_BAD_REQUEST) { - return; // Stop further processing if a bad request status is set - } filterChain.doFilter(servletRequest, servletResponse); } @@ -54,7 +58,7 @@ else if (httpServletResponse.getStatus() == HttpServletResponse.SC_BAD_REQUEST) * @param tokenProvider the Artemis token provider used to generate and validate jwt's * @return the valid jwt or null if not found or invalid */ - public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, TokenProvider tokenProvider) { + public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) { var cookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); var authHeader = httpServletRequest.getHeader("Authorization"); @@ -64,8 +68,7 @@ else if (httpServletResponse.getStatus() == HttpServletResponse.SC_BAD_REQUEST) if (cookie != null && authHeader != null) { // Single Method Enforcement: Only one method of authentication is allowed - httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); - return null; + throw new IllegalArgumentException("Only one method of authentication is allowed"); } String jwtToken = cookie != null ? getJwtFromCookie(cookie) : getJwtFromBearer(authHeader); diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index 262ece79700d..044d897d12c7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -170,6 +170,11 @@ private Claims parseClaims(String authToken) { return Jwts.parser().verifyWith(key).build().parseSignedClaims(authToken).getPayload(); } + public T getClaim(String token, String claimName, Class claimType) { + Claims claims = parseClaims(token); + return claims.get(claimName, claimType); + } + public Date getExpirationDate(String authToken) { return parseClaims(authToken).getExpiration(); } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java index a9f5f901f48c..bc10fb0d9dce 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java @@ -21,9 +21,4 @@ public static void authenticationCookieAssertions(Cookie cookie, boolean logoutC assertThat(cookie.getValue()).isNotEmpty(); } } - - public static void authenticationBearerTokenAssertions(String authorizationHeader) { - assertThat(authorizationHeader).isNotNull(); - assertThat(authorizationHeader).startsWith("Bearer "); - } } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java index b0aa31fb76d3..a1c12ac43cf9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java @@ -10,6 +10,7 @@ import java.time.ZonedDateTime; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -26,6 +27,9 @@ import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + import de.tum.cit.aet.artemis.core.connector.GitlabRequestMockProvider; import de.tum.cit.aet.artemis.core.domain.Authority; import de.tum.cit.aet.artemis.core.domain.Course; @@ -35,6 +39,7 @@ import de.tum.cit.aet.artemis.core.repository.AuthorityRepository; import de.tum.cit.aet.artemis.core.security.Role; import de.tum.cit.aet.artemis.core.security.SecurityUtils; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; import de.tum.cit.aet.artemis.core.service.user.PasswordService; import de.tum.cit.aet.artemis.core.util.CourseFactory; import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise; @@ -50,6 +55,9 @@ class InternalAuthenticationIntegrationTest extends AbstractSpringIntegrationJen @Autowired private PasswordService passwordService; + @Autowired + private TokenProvider tokenProvider; + @Autowired private ProgrammingExerciseTestRepository programmingExerciseRepository; @@ -227,7 +235,10 @@ void testJWTAuthentication() throws Exception { MockHttpServletResponse response = request.postWithoutResponseBody("/api/public/authenticate", loginVM, HttpStatus.OK, httpHeaders); AuthenticationIntegrationTestHelper.authenticationCookieAssertions(response.getCookie("jwt"), false); - AuthenticationIntegrationTestHelper.authenticationBearerTokenAssertions(response.getHeader("Authorization")); + + var responseBody = new ObjectMapper().readValue(response.getContentAsString(), new TypeReference>() { + }); + assertThat(tokenProvider.validateTokenForAuthority(responseBody.get("access_token").toString())).isTrue(); } @Test diff --git a/src/test/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilterTest.java b/src/test/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilterTest.java index 8c0e934e37b7..b50a63de8202 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilterTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilterTest.java @@ -66,6 +66,23 @@ void testJWTFilterBearer() throws Exception { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + String jwt = tokenProvider.createToken(authentication, false); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(new Cookie(JWTFilter.JWT_COOKIE_NAME, jwt)); + request.addHeader("Authorization", "Bearer " + jwt); + request.setRequestURI("/api/test"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain filterChain = new MockFilterChain(); + jwtFilter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void testJWTFilterCookieAndBearer() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", + Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + String jwt = tokenProvider.createToken(authentication, false); MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", "Bearer " + jwt); From 3e93d8c67d0397c52071d2c2204d423d2d65fa49 Mon Sep 17 00:00:00 2001 From: artemis_admin Date: Tue, 19 Nov 2024 22:50:46 +0100 Subject: [PATCH 21/27] include rabbit feedback --- .../aet/artemis/core/security/jwt/JWTFilter.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java index e5438d1f214f..ff1ddcaaf3e3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/JWTFilter.java @@ -24,6 +24,10 @@ public class JWTFilter extends GenericFilterBean { public static final String JWT_COOKIE_NAME = "jwt"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + + private static final String BEARER_PREFIX = "Bearer "; + private final TokenProvider tokenProvider; public JWTFilter(TokenProvider tokenProvider) { @@ -60,7 +64,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo */ public static @Nullable String extractValidJwt(HttpServletRequest httpServletRequest, TokenProvider tokenProvider) { var cookie = WebUtils.getCookie(httpServletRequest, JWT_COOKIE_NAME); - var authHeader = httpServletRequest.getHeader("Authorization"); + var authHeader = httpServletRequest.getHeader(AUTHORIZATION_HEADER); if (cookie == null && authHeader == null) { return null; @@ -68,7 +72,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo if (cookie != null && authHeader != null) { // Single Method Enforcement: Only one method of authentication is allowed - throw new IllegalArgumentException("Only one method of authentication is allowed"); + throw new IllegalArgumentException("Multiple authentication methods detected: Both JWT cookie and Bearer token are present"); } String jwtToken = cookie != null ? getJwtFromCookie(cookie) : getJwtFromBearer(authHeader); @@ -100,11 +104,12 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo * @return the jwt or null if not found */ private static @Nullable String getJwtFromBearer(@Nullable String jwtBearer) { - if (!StringUtils.hasText(jwtBearer) || !jwtBearer.startsWith("Bearer ")) { + if (!StringUtils.hasText(jwtBearer) || !jwtBearer.startsWith(BEARER_PREFIX)) { return null; } - return jwtBearer.substring(7).trim(); + String token = jwtBearer.substring(BEARER_PREFIX.length()).trim(); + return StringUtils.hasText(token) ? token : null; } /** From ca0aa3285cc1b930e35bf4e0d10d1322c605724c Mon Sep 17 00:00:00 2001 From: artemis_admin Date: Wed, 20 Nov 2024 15:52:10 +0100 Subject: [PATCH 22/27] include test for allowedTools annotation --- .../allowedTools/ToolsInterceptor.java | 4 ++ .../aet/artemis/core/web/TokenResource.java | 72 +++++++++++++++++++ .../core/web/open/PublicUserJwtResource.java | 42 +---------- ...InternalAuthenticationIntegrationTest.java | 2 +- .../allowedTools/AllowedToolsResource.java | 30 ++++++++ .../allowedTools/AllowedToolsTest.java | 69 ++++++++++++++++++ 6 files changed, 177 insertions(+), 42 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsResource.java create mode 100644 src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsTest.java diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java index c1cdcece08f3..d9c3288e4955 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/allowedTools/ToolsInterceptor.java @@ -1,11 +1,14 @@ package de.tum.cit.aet.artemis.core.security.allowedTools; +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + import java.lang.reflect.Method; import java.util.Arrays; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @@ -13,6 +16,7 @@ import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; +@Profile(PROFILE_CORE) @Component public class ToolsInterceptor implements HandlerInterceptor { diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java new file mode 100644 index 000000000000..e8a0e3f3f104 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java @@ -0,0 +1,72 @@ +package de.tum.cit.aet.artemis.core.web; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import java.time.Duration; +import java.util.Date; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; +import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; +import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; + +@Profile(PROFILE_CORE) +@RestController +@RequestMapping("api/") +public class TokenResource { + + private final JWTCookieService jwtCookieService; + + private final TokenProvider tokenProvider; + + public TokenResource(JWTCookieService jwtCookieService, TokenProvider tokenProvider) { + this.jwtCookieService = jwtCookieService; + this.tokenProvider = tokenProvider; + } + + /** + * Sends a tool token back as cookie or bearer token + * + * @param tool the tool for which the token is requested + * @param asCookie if true the token is sent back as a cookie + * @param request HTTP request + * @param response HTTP response + * @return the ResponseEntity with status 200 (ok), 401 (unauthorized) and depending on the asCookie flag a bearer token in the body + */ + @PostMapping("tool-token") + @EnforceAtLeastStudent + public ResponseEntity convertCookieToToolToken(@RequestParam(name = "tool", required = true) ToolTokenType tool, + @RequestParam(name = "as-cookie", defaultValue = "false") boolean asCookie, HttpServletRequest request, HttpServletResponse response) { + // remaining time in milliseconds + var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); + if (jwtToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + // get validity of the token + long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - new Date().getTime(); + + // 1 day validity + long maxDuration = Duration.ofDays(1).toMillis(); + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(Math.min(tokenRemainingTime, maxDuration), tool); + + if (asCookie) { + response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); + } + return ResponseEntity.ok(responseCookie.getValue()); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index 47ea41c9f2dc..b0d3bb93a324 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -2,8 +2,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; -import java.time.Duration; -import java.util.Date; import java.util.Map; import java.util.Optional; @@ -38,11 +36,8 @@ import de.tum.cit.aet.artemis.core.security.SecurityUtils; import de.tum.cit.aet.artemis.core.security.UserNotActivatedException; import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceNothing; import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; -import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; -import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; import de.tum.cit.aet.artemis.core.service.connectors.SAML2Service; /** @@ -57,15 +52,12 @@ public class PublicUserJwtResource { private final JWTCookieService jwtCookieService; - private final TokenProvider tokenProvider; - private final AuthenticationManager authenticationManager; private final Optional saml2Service; - public PublicUserJwtResource(JWTCookieService jwtCookieService, TokenProvider tokenProvider, AuthenticationManager authenticationManager, Optional saml2Service) { + public PublicUserJwtResource(JWTCookieService jwtCookieService, AuthenticationManager authenticationManager, Optional saml2Service) { this.jwtCookieService = jwtCookieService; - this.tokenProvider = tokenProvider; this.authenticationManager = authenticationManager; this.saml2Service = saml2Service; } @@ -106,38 +98,6 @@ public ResponseEntity> authorize(@Valid @RequestBody LoginVM } } - /** - * Sends a tool token back as cookie or bearer token - * - * @param tool the tool for which the token is requested - * @param asCookie if true the token is sent back as a cookie - * @param request HTTP request - * @param response HTTP response - * @return the ResponseEntity with status 200 (ok), 401 (unauthorized) and depending on the asCookie flag a bearer token in the body - */ - @PostMapping("tool-token") - @EnforceAtLeastStudent - public ResponseEntity convertCookieToToolToken(@RequestParam(name = "tool", required = true) ToolTokenType tool, - @RequestParam(name = "as-cookie", defaultValue = "false") boolean asCookie, HttpServletRequest request, HttpServletResponse response) { - // remaining time in milliseconds - var jwtToken = JWTFilter.extractValidJwt(request, tokenProvider); - if (jwtToken == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - - // get validity of the token - long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - new Date().getTime(); - - // 1 day validity - long maxDuration = Duration.ofDays(1).toMillis(); - ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(Math.min(tokenRemainingTime, maxDuration), tool); - - if (asCookie) { - response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); - } - return ResponseEntity.ok(responseCookie.getValue()); - } - /** * Authorizes a User logged in with SAML2 * diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java index 31e3bfc01975..5f4557083d75 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java @@ -265,7 +265,7 @@ void testScorpioTokenGeneration() throws Exception { LinkedMultiValueMap params = new LinkedMultiValueMap<>(); params.add("tool", ToolTokenType.SCORPIO.toString()); - var responseBody = request.performMvcRequest(post("/api/public/tool-token").cookie(cookie).params(params)).andExpect(status().isOk()).andReturn().getResponse() + var responseBody = request.performMvcRequest(post("/api/tool-token").cookie(cookie).params(params)).andExpect(status().isOk()).andReturn().getResponse() .getContentAsString(); AuthenticationIntegrationTestHelper.toolTokenAssertions(tokenProvider, responseBody, initialLifetime, ToolTokenType.SCORPIO); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsResource.java b/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsResource.java new file mode 100644 index 000000000000..2f8e63ed11a6 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsResource.java @@ -0,0 +1,30 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; + +@Profile(PROFILE_CORE) +@RestController +@RequestMapping("api/test/") +public class AllowedToolsResource { + + @GetMapping("testAllowedToolTokenScorpio") + @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) + public ResponseEntity testAllowedToolTokenScorpio() { + return ResponseEntity.ok().build(); + } + + @GetMapping("testNoAllowedToolToken") + @EnforceAtLeastStudent + public ResponseEntity testNoAllowedToolToken() { + return ResponseEntity.ok().build(); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsTest.java b/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsTest.java new file mode 100644 index 000000000000..c9a988cf336a --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/core/security/allowedTools/AllowedToolsTest.java @@ -0,0 +1,69 @@ +package de.tum.cit.aet.artemis.core.security.allowedTools; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Collections; + +import jakarta.servlet.http.Cookie; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.jwt.JWTFilter; +import de.tum.cit.aet.artemis.core.security.jwt.TokenProvider; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; + +class AllowedToolsTest extends AbstractSpringIntegrationIndependentTest { + + private static final String TEST_PREFIX = "allowedtools"; + + @Autowired + private TokenProvider tokenProvider; + + @BeforeEach + void initTestCase() { + userUtilService.addUsers(TEST_PREFIX, 1, 0, 0, 0); + } + + @Test + void testAllowedToolsRouteWithToolToken() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", + Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + + String jwt = tokenProvider.createToken(authentication, 24 * 60 * 60 * 1000, ToolTokenType.SCORPIO); + Cookie cookie = new Cookie(JWTFilter.JWT_COOKIE_NAME, jwt); + + request.performMvcRequest(get("/api/test/testAllowedToolTokenScorpio").cookie(cookie)).andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testAllowedToolsRouteWithGeneralToken() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", + Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + + String jwt = tokenProvider.createToken(authentication, 24 * 60 * 60 * 1000, null); + Cookie cookie = new Cookie(JWTFilter.JWT_COOKIE_NAME, jwt); + + request.performMvcRequest(get("/api/test/testAllowedToolTokenScorpio").cookie(cookie)).andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void testAllowedToolsRouteWithDifferentToolToken() throws Exception { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("test-user", "test-password", + Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority()))); + + String jwt = tokenProvider.createToken(authentication, 24 * 60 * 60 * 1000, ToolTokenType.SCORPIO); + Cookie cookie = new Cookie(JWTFilter.JWT_COOKIE_NAME, jwt); + + request.performMvcRequest(get("/api/test/testNoAllowedToolToken").cookie(cookie)).andExpect(status().isForbidden()); + } + +} From f2f28511f4ec359848d21d790bbabcc658d5cf8f Mon Sep 17 00:00:00 2001 From: artemis_admin Date: Wed, 20 Nov 2024 18:14:03 +0100 Subject: [PATCH 23/27] change from Date to ZonedDateTime --- .../cit/aet/artemis/core/security/jwt/TokenProvider.java | 7 ++++--- .../de/tum/cit/aet/artemis/core/web/TokenResource.java | 3 ++- .../AuthenticationIntegrationTestHelper.java | 3 ++- .../InternalAuthenticationIntegrationTest.java | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index c74003bb2ab5..d86ec0e7f0cd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -3,6 +3,8 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Date; import java.util.List; @@ -111,14 +113,13 @@ public String createToken(Authentication authentication, boolean rememberMe) { public String createToken(Authentication authentication, long duration, @Nullable ToolTokenType tool) { String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")); - long now = (new Date()).getTime(); - Date validity = new Date(now + duration); + var validity = ZonedDateTime.now().plus(duration, ChronoUnit.MILLIS); var jwtBuilder = Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities); if (tool != null) { jwtBuilder.claim("tools", tool); } - return jwtBuilder.signWith(key, Jwts.SIG.HS512).expiration(validity).compact(); + return jwtBuilder.signWith(key, Jwts.SIG.HS512).expiration(Date.from(validity.toInstant())).compact(); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java index e8a0e3f3f104..091c830fd6be 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.time.Duration; +import java.time.ZonedDateTime; import java.util.Date; import jakarta.servlet.http.HttpServletRequest; @@ -58,7 +59,7 @@ public ResponseEntity convertCookieToToolToken(@RequestParam(name = "too } // get validity of the token - long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - new Date().getTime(); + long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - Date.from(ZonedDateTime.now().toInstant()).getTime(); // 1 day validity long maxDuration = Duration.ofDays(1).toMillis(); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java index 0d201f065b6a..1df474260bb7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.time.ZonedDateTime; import java.util.Date; import jakarta.servlet.http.Cookie; @@ -36,7 +37,7 @@ public static void toolTokenAssertions(TokenProvider tokenProvider, String token assertThat(toolClaims).contains(tool.toString()); } - var lifetime = tokenProvider.getExpirationDate(token).getTime() - new Date().getTime(); + var lifetime = tokenProvider.getExpirationDate(token).getTime() - Date.from(ZonedDateTime.now().toInstant()).getTime(); // assert that the token has a lifetime of less than a day assertThat(lifetime).isLessThan(24 * 60 * 60 * 1000); assertThat(lifetime).isLessThan(initialLifetime); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java index 5f4557083d75..b290d5ca793a 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java @@ -260,7 +260,7 @@ void testScorpioTokenGeneration() throws Exception { Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue()); cookie.setMaxAge((int) responseCookie.getMaxAge().toMillis()); - var initialLifetime = tokenProvider.getExpirationDate(cookie.getValue()).getTime() - new Date().getTime(); + var initialLifetime = tokenProvider.getExpirationDate(cookie.getValue()).getTime() - Date.from(ZonedDateTime.now().toInstant()).getTime(); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); params.add("tool", ToolTokenType.SCORPIO.toString()); From fe7af6d6c8345db09a5152ae007d0114536a3d8c Mon Sep 17 00:00:00 2001 From: artemis_admin Date: Wed, 20 Nov 2024 22:44:07 +0100 Subject: [PATCH 24/27] use system.currenttimemillis instead of zonedtimedate --- .../cit/aet/artemis/core/security/jwt/TokenProvider.java | 6 ++---- .../java/de/tum/cit/aet/artemis/core/web/TokenResource.java | 4 +--- .../authentication/AuthenticationIntegrationTestHelper.java | 5 +---- .../InternalAuthenticationIntegrationTest.java | 3 +-- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java index d86ec0e7f0cd..9463e850322c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/security/jwt/TokenProvider.java @@ -3,8 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.nio.charset.StandardCharsets; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Date; import java.util.List; @@ -113,13 +111,13 @@ public String createToken(Authentication authentication, boolean rememberMe) { public String createToken(Authentication authentication, long duration, @Nullable ToolTokenType tool) { String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")); - var validity = ZonedDateTime.now().plus(duration, ChronoUnit.MILLIS); + var validity = System.currentTimeMillis() + duration; var jwtBuilder = Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities); if (tool != null) { jwtBuilder.claim("tools", tool); } - return jwtBuilder.signWith(key, Jwts.SIG.HS512).expiration(Date.from(validity.toInstant())).compact(); + return jwtBuilder.signWith(key, Jwts.SIG.HS512).expiration(new Date(validity)).compact(); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java index 091c830fd6be..54b3ad20a07c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/TokenResource.java @@ -3,8 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.Date; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -59,7 +57,7 @@ public ResponseEntity convertCookieToToolToken(@RequestParam(name = "too } // get validity of the token - long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - Date.from(ZonedDateTime.now().toInstant()).getTime(); + long tokenRemainingTime = tokenProvider.getExpirationDate(jwtToken).getTime() - System.currentTimeMillis(); // 1 day validity long maxDuration = Duration.ofDays(1).toMillis(); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java index 1df474260bb7..c757a43b8fa8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/AuthenticationIntegrationTestHelper.java @@ -2,9 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.time.ZonedDateTime; -import java.util.Date; - import jakarta.servlet.http.Cookie; import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; @@ -37,7 +34,7 @@ public static void toolTokenAssertions(TokenProvider tokenProvider, String token assertThat(toolClaims).contains(tool.toString()); } - var lifetime = tokenProvider.getExpirationDate(token).getTime() - Date.from(ZonedDateTime.now().toInstant()).getTime(); + var lifetime = tokenProvider.getExpirationDate(token).getTime() - System.currentTimeMillis(); // assert that the token has a lifetime of less than a day assertThat(lifetime).isLessThan(24 * 60 * 60 * 1000); assertThat(lifetime).isLessThan(initialLifetime); diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java index b290d5ca793a..c4937a84814e 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/InternalAuthenticationIntegrationTest.java @@ -10,7 +10,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.ZonedDateTime; -import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -260,7 +259,7 @@ void testScorpioTokenGeneration() throws Exception { Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue()); cookie.setMaxAge((int) responseCookie.getMaxAge().toMillis()); - var initialLifetime = tokenProvider.getExpirationDate(cookie.getValue()).getTime() - Date.from(ZonedDateTime.now().toInstant()).getTime(); + var initialLifetime = tokenProvider.getExpirationDate(cookie.getValue()).getTime() - System.currentTimeMillis(); LinkedMultiValueMap params = new LinkedMultiValueMap<>(); params.add("tool", ToolTokenType.SCORPIO.toString()); From 1b257edd9aff3c6a5c95daf616f570bbf865de07 Mon Sep 17 00:00:00 2001 From: artemis_admin Date: Wed, 20 Nov 2024 23:55:12 +0100 Subject: [PATCH 25/27] allow scorpio to get vcs access token --- .../java/de/tum/cit/aet/artemis/core/web/AccountResource.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 997574c76da7..2e767839ac55 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -38,6 +38,8 @@ import de.tum.cit.aet.artemis.core.exception.EmailAlreadyUsedException; import de.tum.cit.aet.artemis.core.exception.PasswordViolatesRequirementsException; import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.service.AccountService; import de.tum.cit.aet.artemis.core.service.FilePathService; @@ -221,6 +223,7 @@ public ResponseEntity deleteVcsAccessToken() { */ @GetMapping("account/participation-vcs-access-token") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity getVcsAccessToken(@RequestParam("participationId") Long participationId) { User user = userRepository.getUser(); @@ -237,6 +240,7 @@ public ResponseEntity getVcsAccessToken(@RequestParam("participationId") */ @PutMapping("account/participation-vcs-access-token") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity createVcsAccessToken(@RequestParam("participationId") Long participationId) { User user = userRepository.getUser(); From e57a66157d9981dec9488abb481f915884bd0068 Mon Sep 17 00:00:00 2001 From: artemis_admin Date: Tue, 26 Nov 2024 15:34:15 +0100 Subject: [PATCH 26/27] allow login and logout with scorpio --- .../cit/aet/artemis/core/web/open/PublicUserJwtResource.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java index b0d3bb93a324..ce61f2998317 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/open/PublicUserJwtResource.java @@ -35,6 +35,7 @@ import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.security.SecurityUtils; import de.tum.cit.aet.artemis.core.security.UserNotActivatedException; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceNothing; import de.tum.cit.aet.artemis.core.security.jwt.JWTCookieService; @@ -72,6 +73,7 @@ public PublicUserJwtResource(JWTCookieService jwtCookieService, AuthenticationMa */ @PostMapping("authenticate") @EnforceNothing + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity> authorize(@Valid @RequestBody LoginVM loginVM, @RequestHeader("User-Agent") String userAgent, @RequestParam(name = "tool", required = false) ToolTokenType tool, HttpServletResponse response) { @@ -146,6 +148,7 @@ public ResponseEntity authorizeSAML2(@RequestBody final String body, HttpS */ @PostMapping("logout") @EnforceNothing + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity logout(HttpServletRequest request, HttpServletResponse response) throws ServletException { request.logout(); // Logout needs to build the same cookie (secure, httpOnly and sameSite='Lax') or browsers will ignore the header and not unset the cookie From 44489c30fb6bf9577b2174b3e059957fae3b8c85 Mon Sep 17 00:00:00 2001 From: dennis Date: Thu, 5 Dec 2024 11:32:04 +0100 Subject: [PATCH 27/27] allow scorpio to fetch feedback --- .../de/tum/cit/aet/artemis/assessment/web/ResultResource.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java index 431fb66373e8..981d41147368 100644 --- a/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/assessment/web/ResultResource.java @@ -43,6 +43,8 @@ import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.core.security.Role; +import de.tum.cit.aet.artemis.core.security.allowedTools.AllowedTools; +import de.tum.cit.aet.artemis.core.security.allowedTools.ToolTokenType; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastTutor; @@ -166,6 +168,7 @@ public ResponseEntity getResult(@PathVariable Long participationId, @Pat */ @GetMapping("participations/{participationId}/results/{resultId}/details") @EnforceAtLeastStudent + @AllowedTools(ToolTokenType.SCORPIO) public ResponseEntity> getResultDetails(@PathVariable Long participationId, @PathVariable Long resultId) { log.debug("REST request to get details of Result : {}", resultId); Result result = resultRepository.findByIdWithEagerFeedbacksElseThrow(resultId);