Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programming exercises: Add tool token support #9408

Open
wants to merge 47 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5dec295
enable bearer authentication
janthoXO Oct 1, 2024
b447b28
Add re-key endpoint
janthoXO Oct 1, 2024
a3d0fbb
Implement rabbit feedback
janthoXO Oct 2, 2024
069f2eb
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
janthoXO Oct 4, 2024
39e4bad
make token theia specific
janthoXO Oct 4, 2024
ddd8802
fix conversion from day to millis
janthoXO Oct 4, 2024
07afea2
only respond as cookie if asked for
janthoXO Oct 4, 2024
b59f8ad
fix class cast error for websockets
janthoXO Oct 5, 2024
896fd4c
let login return json with token
janthoXO Oct 7, 2024
2e247db
add server test for bearer token
janthoXO Nov 6, 2024
e8bde67
Merge branch 'develop' into feature/bearer-support
iyannsch Nov 7, 2024
c36c6ec
Merge branch 'develop' into feature/re-key
iyannsch Nov 7, 2024
8743e73
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
iyannsch Nov 7, 2024
02e63b7
change theia token to general tool token endpoint
janthoXO Nov 8, 2024
44ca43c
Apply suggestions from code review
janthoXO Nov 8, 2024
6cab497
add tool token annotation
janthoXO Nov 9, 2024
e2bb2d0
Merge remote-tracking branch 'origin/feature/re-key' into feature/re-key
janthoXO Nov 9, 2024
6e61b26
move interceptor registration
janthoXO Nov 9, 2024
149a53a
add tool token option to login
janthoXO Nov 11, 2024
b0ed73d
change filter chain to only accept one auth method
janthoXO Nov 11, 2024
6b19a87
Merge remote-tracking branch 'origin/feature/bearer-support' into fea…
janthoXO Nov 11, 2024
8df1a6b
write filter a bit nicer
janthoXO Nov 11, 2024
7fe83a9
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
janthoXO Nov 12, 2024
9f61018
annotate endpoints required by Scorpio
janthoXO Nov 12, 2024
5908f73
change bad request behaviour
janthoXO Nov 12, 2024
288a1eb
Merge branch 'develop' of github.com:ls1intum/Artemis into feature/be…
iyannsch Nov 18, 2024
81f4739
Merge branch 'develop' of github.com:ls1intum/Artemis into feature/re…
iyannsch Nov 18, 2024
7a69620
fix server tests
Nov 19, 2024
5803813
Merge remote-tracking branch 'origin/feature/re-key' into feature/re-key
Nov 19, 2024
1cf7281
Merge branch 'develop' of github.com:ls1intum/Artemis into feature/be…
Nov 19, 2024
4dabd91
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
Nov 19, 2024
52b816b
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
Nov 19, 2024
9ca745b
adjust tests
Nov 19, 2024
277f8b1
merge from bearer support
Nov 19, 2024
3e93d8c
include rabbit feedback
Nov 19, 2024
8715557
Merge branch 'feature/bearer-support' of github.com:ls1intum/Artemis …
Nov 19, 2024
ca0aa32
include test for allowedTools annotation
Nov 20, 2024
f2f2851
change from Date to ZonedDateTime
Nov 20, 2024
fe7af6d
use system.currenttimemillis instead of zonedtimedate
Nov 20, 2024
28d0c44
Merge branch 'develop' of github.com:ls1intum/Artemis into feature/re…
Nov 20, 2024
1b257ed
allow scorpio to get vcs access token
Nov 20, 2024
e57a661
allow login and logout with scorpio
Nov 26, 2024
7172f9c
Merge branch 'develop' of github.com:ls1intum/Artemis into feature/be…
iyannsch Nov 26, 2024
30a8652
Merge branch 'feature/bearer-support' into feature/re-key
iyannsch Nov 26, 2024
44489c3
allow scorpio to fetch feedback
janthoXO Dec 5, 2024
504b2d3
merge
Dec 9, 2024
02c62a5
Merge branch 'develop' into feature/re-key
janthoXO Dec 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -166,6 +168,7 @@ public ResponseEntity<Result> getResult(@PathVariable Long participationId, @Pat
*/
@GetMapping("participations/{participationId}/results/{resultId}/details")
@EnforceAtLeastStudent
@AllowedTools(ToolTokenType.SCORPIO)
public ResponseEntity<List<Feedback>> getResultDetails(@PathVariable Long participationId, @PathVariable Long resultId) {
log.debug("REST request to get details of Result : {}", resultId);
Result result = resultRepository.findByIdWithEagerFeedbacksElseThrow(resultId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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;

Expand All @@ -37,17 +40,20 @@
*/
@Profile(PROFILE_CORE)
@Configuration
public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer<WebServerFactory> {
public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer<WebServerFactory>, WebMvcConfigurer {

private static final Logger log = LoggerFactory.getLogger(WebConfigurer.class);

private final Environment env;

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
Expand Down Expand Up @@ -126,4 +132,9 @@ public CorsFilter corsFilter() {
}
return new CorsFilter(source);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(toolsInterceptor);
}
Comment on lines +136 to +139
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider restricting interceptor to specific paths.

The interceptor is currently registered globally. Consider restricting it to relevant API paths to avoid unnecessary overhead and potential security implications.

 @Override
 public void addInterceptors(InterceptorRegistry registry) {
-    registry.addInterceptor(toolsInterceptor);
+    registry.addInterceptor(toolsInterceptor)
+        .addPathPatterns("/api/**")
+        .excludePathPatterns("/api/public/**");
 }

Committable suggestion skipped: line range outside the PR's diff.

}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tum.cit.aet.artemis.core.security.allowedTools;

public enum ToolTokenType {
SCORPIO
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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;

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 {

private final TokenProvider tokenProvider;

public ToolsInterceptor(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
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;
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", String.class);

// 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -36,9 +38,30 @@ 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 buildLoginCookie(long duration, ToolTokenType tool) {
String jwt = tokenProvider.createToken(SecurityContextHolder.getContext().getAuthentication(), duration, tool);
return buildJWTCookie(jwt, Duration.of(duration, ChronoUnit.MILLIS));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,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;
Expand Down Expand Up @@ -95,11 +97,27 @@ public long getTokenValidity(boolean rememberMe) {
* @return JWT Token
*/
public String createToken(Authentication authentication, boolean rememberMe) {
return createToken(authentication, getTokenValidity(rememberMe), null);
}

/**
* Create JWT Token a fully populated <code>Authentication</code> object.
*
* @param authentication Authentication Object
* @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
*/
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 + getTokenValidity(rememberMe));
return Jwts.builder().subject(authentication.getName()).claim(AUTHORITIES_KEY, authorities).signWith(key, Jwts.SIG.HS512).expiration(validity).compact();
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(new Date(validity)).compact();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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;
Expand Down Expand Up @@ -172,6 +174,7 @@ public ResponseEntity<Void> deleteVcsAccessToken() {
*/
@GetMapping("account/participation-vcs-access-token")
@EnforceAtLeastStudent
@AllowedTools(ToolTokenType.SCORPIO)
public ResponseEntity<String> getVcsAccessToken(@RequestParam("participationId") Long participationId) {
User user = userRepository.getUser();

Expand All @@ -188,6 +191,7 @@ public ResponseEntity<String> getVcsAccessToken(@RequestParam("participationId")
*/
@PutMapping("account/participation-vcs-access-token")
@EnforceAtLeastStudent
@AllowedTools(ToolTokenType.SCORPIO)
public ResponseEntity<String> createVcsAccessToken(@RequestParam("participationId") Long participationId) {
User user = userRepository.getUser();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,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;
Expand Down Expand Up @@ -584,6 +586,7 @@ public ResponseEntity<List<Course>> getCoursesForEnrollment() {
// TODO: we should rename this into courses/{courseId}/details
@GetMapping("courses/{courseId}/for-dashboard")
@EnforceAtLeastStudent
@AllowedTools(ToolTokenType.SCORPIO)
public ResponseEntity<CourseForDashboardDTO> 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);
Expand Down Expand Up @@ -648,6 +651,7 @@ public record CourseDropdownDTO(Long id, String title, String courseIcon) {
*/
@GetMapping("courses/for-dashboard")
@EnforceAtLeastStudent
@AllowedTools(ToolTokenType.SCORPIO)
public ResponseEntity<CoursesForDashboardDTO> getCoursesForDashboard() {
long timeNanoStart = System.nanoTime();
User user = userRepository.getUserWithGroupsAndAuthorities();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 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<String> 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() - System.currentTimeMillis();

// 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());
}
}
Loading
Loading