From e53d4e4cd2297e98daadb7d32d647fe425b06a51 Mon Sep 17 00:00:00 2001 From: Torsten Krohn Date: Fri, 1 Apr 2022 15:02:24 +0200 Subject: [PATCH 1/3] feat: design jwt endpoint --- api/videoservice.yaml | 45 +++++++++++++++++++ .../api/controller/VideoController.java | 7 +++ 2 files changed, 52 insertions(+) diff --git a/api/videoservice.yaml b/api/videoservice.yaml index fef35c8..82e4c3e 100644 --- a/api/videoservice.yaml +++ b/api/videoservice.yaml @@ -69,6 +69,41 @@ paths: description: INTERNAL SERVER ERROR - server encountered unexpected condition security: - Bearer: [ ] + /videocalls/{roomId}/jwt: + get: + tags: + - video-controller + summary: 'Gets the moderatorToken (for consultants) or the invitationToken (for others) + [Authorization: Role: consultant, anonymous]' + operationId: getWebToken + parameters: + - name: RCUserId + in: header + required: true + schema: + type: string + - name: roomId + in: path + required: true + schema: + type: string + responses: + 200: + description: OK - web token returned + content: + 'application/json': + schema: + $ref: '#/components/schemas/VideoCallInfoDTO' + 400: + description: BAD REQUEST - invalid/incomplete request or body object + 401: + description: UNAUTHORIZED - no/invalid Keycloak token + 403: + description: FORBIDDEN - no/invalid CSRF token + 404: + description: NOT FOUND - rcGroupId unknown + 500: + description: INTERNAL SERVER ERROR - server encountered unexpected condition components: schemas: @@ -105,6 +140,16 @@ components: initiatorUsername: type: string example: "consultant23" + VideoCallInfoDTO: + type: object + required: + - domain + - jwt + properties: + domain: + type: string + jwt: + type: string securitySchemes: Bearer: diff --git a/src/main/java/de/caritas/cob/videoservice/api/controller/VideoController.java b/src/main/java/de/caritas/cob/videoservice/api/controller/VideoController.java index 89eafa2..8218933 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/controller/VideoController.java +++ b/src/main/java/de/caritas/cob/videoservice/api/controller/VideoController.java @@ -4,9 +4,11 @@ import de.caritas.cob.videoservice.api.model.CreateVideoCallDTO; import de.caritas.cob.videoservice.api.model.CreateVideoCallResponseDTO; import de.caritas.cob.videoservice.api.model.RejectVideoCallDTO; +import de.caritas.cob.videoservice.api.model.VideoCallInfoDTO; import de.caritas.cob.videoservice.api.service.RejectVideoCallService; import de.caritas.cob.videoservice.generated.api.controller.VideocallsApi; import io.swagger.annotations.Api; +import java.util.Optional; import javax.validation.Valid; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -51,4 +53,9 @@ public ResponseEntity rejectVideoCall(@Valid RejectVideoCallDTO rejectVide this.rejectVideoCallService.rejectVideoCall(rejectVideoCallDto); return new ResponseEntity<>(HttpStatus.OK); } + + @Override + public ResponseEntity getWebToken(String rcUserId, String groupId) { + return ResponseEntity.of(Optional.empty()); + } } From 359048b34f39d4abc463aa9292f55031daaf65b5 Mon Sep 17 00:00:00 2001 From: Torsten Krohn Date: Fri, 1 Apr 2022 17:24:33 +0200 Subject: [PATCH 2/3] feat: implement web token creation --- .../api/authorization/AuthenticatedUser.java | 11 +++++++ .../api/controller/VideoController.java | 9 ++++-- .../video/VideoCallUrlGeneratorService.java | 14 +++++++++ .../video/jwt/TokenGeneratorService.java | 31 +++++++++++++++++-- .../config/AuthenticatedUserConfig.java | 3 ++ .../config/security/WebSecurityConfig.java | 2 ++ .../api/controller/VideoControllerIT.java | 4 +++ 7 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/caritas/cob/videoservice/api/authorization/AuthenticatedUser.java b/src/main/java/de/caritas/cob/videoservice/api/authorization/AuthenticatedUser.java index a799351..acb7aa9 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/authorization/AuthenticatedUser.java +++ b/src/main/java/de/caritas/cob/videoservice/api/authorization/AuthenticatedUser.java @@ -1,5 +1,9 @@ package de.caritas.cob.videoservice.api.authorization; +import static java.util.Objects.nonNull; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Set; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -23,4 +27,11 @@ public class AuthenticatedUser { @NonNull private String accessToken; + + private Set roles; + + @JsonIgnore + public boolean isConsultant() { + return nonNull(roles) && roles.contains(Authority.CONSULTANT.name()); + } } diff --git a/src/main/java/de/caritas/cob/videoservice/api/controller/VideoController.java b/src/main/java/de/caritas/cob/videoservice/api/controller/VideoController.java index 8218933..4995085 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/controller/VideoController.java +++ b/src/main/java/de/caritas/cob/videoservice/api/controller/VideoController.java @@ -6,9 +6,9 @@ import de.caritas.cob.videoservice.api.model.RejectVideoCallDTO; import de.caritas.cob.videoservice.api.model.VideoCallInfoDTO; import de.caritas.cob.videoservice.api.service.RejectVideoCallService; +import de.caritas.cob.videoservice.api.service.video.VideoCallUrlGeneratorService; import de.caritas.cob.videoservice.generated.api.controller.VideocallsApi; import io.swagger.annotations.Api; -import java.util.Optional; import javax.validation.Valid; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -27,6 +27,7 @@ public class VideoController implements VideocallsApi { private final @NonNull StartVideoCallFacade startVideoCallFacade; private final @NonNull RejectVideoCallService rejectVideoCallService; + private final @NonNull VideoCallUrlGeneratorService videoCallUrlGeneratorService; /** * Starts a new video call. @@ -55,7 +56,9 @@ public ResponseEntity rejectVideoCall(@Valid RejectVideoCallDTO rejectVide } @Override - public ResponseEntity getWebToken(String rcUserId, String groupId) { - return ResponseEntity.of(Optional.empty()); + public ResponseEntity getWebToken(String rcUserId, String roomId) { + var videoCallInfo = videoCallUrlGeneratorService.generateJwt(roomId); + + return ResponseEntity.ok(videoCallInfo); } } diff --git a/src/main/java/de/caritas/cob/videoservice/api/service/video/VideoCallUrlGeneratorService.java b/src/main/java/de/caritas/cob/videoservice/api/service/video/VideoCallUrlGeneratorService.java index 175a7db..5c81e62 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/service/video/VideoCallUrlGeneratorService.java +++ b/src/main/java/de/caritas/cob/videoservice/api/service/video/VideoCallUrlGeneratorService.java @@ -1,6 +1,7 @@ package de.caritas.cob.videoservice.api.service.video; import de.caritas.cob.videoservice.api.exception.httpresponse.InternalServerErrorException; +import de.caritas.cob.videoservice.api.model.VideoCallInfoDTO; import de.caritas.cob.videoservice.api.service.video.jwt.TokenGeneratorService; import de.caritas.cob.videoservice.api.service.video.jwt.model.VideoCallUrls; import java.net.MalformedURLException; @@ -57,4 +58,17 @@ private String buildUrl(String uuid, String token) { } } + /** + * Generate JWT. + * + * @param roomId room id + * @return VideoCallInfoDTO + */ + public VideoCallInfoDTO generateJwt(String roomId) { + var videoCallInfo = new VideoCallInfoDTO(); + videoCallInfo.setJwt(tokenGeneratorService.generateToken(roomId)); + videoCallInfo.setDomain(videoCallServerUrl); + + return videoCallInfo; + } } diff --git a/src/main/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorService.java b/src/main/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorService.java index 7849439..7238001 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorService.java +++ b/src/main/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorService.java @@ -60,6 +60,18 @@ public void initAlgorithm() { this.algorithm = Algorithm.HMAC256(this.secret); } + /** + * Generate token. + * + * @param roomId room id + * @return token + */ + public String generateToken(String roomId) { + return authenticatedUser.isConsultant() + ? generateModeratorToken(roomId) + : generateNonModeratorToken(roomId); + } + /** * Generates the {@link VideoCallToken} for anonymous user and asker (containing user name). * @@ -69,12 +81,12 @@ public void initAlgorithm() { */ public VideoCallToken generateNonModeratorToken(String roomId, String askerName) { return VideoCallToken.builder() - .guestToken(buildGuestJwt(roomId)) + .guestToken(generateNonModeratorToken(roomId)) .userRelatedToken(buildUserRelatedJwt(roomId, askerName)) .build(); } - private String buildGuestJwt(String roomId) { + public String generateNonModeratorToken(String roomId) { return buildBasicJwt(roomId) .sign(algorithm); } @@ -128,6 +140,21 @@ public String generateModeratorToken(String roomId, String guestVideoCallUrl) { return buildModeratorJwt(roomId, guestVideoCallUrl); } + /** + * Generate moderator token. + * + * @param roomId room id + * @return token + */ + public String generateModeratorToken(String roomId) { + var userContext = createUserContext(authenticatedUser.getUsername()); + + return buildBasicJwt(roomId) + .withClaim(MODERATOR_CLAIM, true) + .withClaim(CONTEXT_CLAIM, userContext) + .sign(algorithm); + } + private String buildModeratorJwt(String roomId, String guestVideoCallUrl) { return buildBasicJwt(roomId) .withClaim(MODERATOR_CLAIM, true) diff --git a/src/main/java/de/caritas/cob/videoservice/config/AuthenticatedUserConfig.java b/src/main/java/de/caritas/cob/videoservice/config/AuthenticatedUserConfig.java index 957a01f..04102e8 100644 --- a/src/main/java/de/caritas/cob/videoservice/config/AuthenticatedUserConfig.java +++ b/src/main/java/de/caritas/cob/videoservice/config/AuthenticatedUserConfig.java @@ -43,6 +43,9 @@ public AuthenticatedUser getAuthenticatedUser() { authenticatedUser.setUserId(getUserAttribute(claimMap, CLAIM_NAME_USER_ID)); authenticatedUser.setUsername(getUserAttribute(claimMap, CLAIM_NAME_USERNAME)); + var roles = keycloakSecContext.getToken().getRealmAccess().getRoles(); + authenticatedUser.setRoles(roles); + return authenticatedUser; } diff --git a/src/main/java/de/caritas/cob/videoservice/config/security/WebSecurityConfig.java b/src/main/java/de/caritas/cob/videoservice/config/security/WebSecurityConfig.java index c6c9b74..bd4c7a5 100644 --- a/src/main/java/de/caritas/cob/videoservice/config/security/WebSecurityConfig.java +++ b/src/main/java/de/caritas/cob/videoservice/config/security/WebSecurityConfig.java @@ -64,6 +64,8 @@ protected void configure(HttpSecurity http) throws Exception { .hasAuthority(CONSULTANT.getAuthority()) .antMatchers("/videocalls/reject") .hasAnyAuthority(USER.getAuthority()) + .antMatchers("/videocalls/*/jwt") + .hasAnyAuthority(CONSULTANT.getAuthority()) //TODO: add anonymous .anyRequest().denyAll() .and() .exceptionHandling() diff --git a/src/test/java/de/caritas/cob/videoservice/api/controller/VideoControllerIT.java b/src/test/java/de/caritas/cob/videoservice/api/controller/VideoControllerIT.java index 8125422..cbcb3bb 100644 --- a/src/test/java/de/caritas/cob/videoservice/api/controller/VideoControllerIT.java +++ b/src/test/java/de/caritas/cob/videoservice/api/controller/VideoControllerIT.java @@ -21,6 +21,7 @@ import de.caritas.cob.videoservice.api.facade.StartVideoCallFacade; import de.caritas.cob.videoservice.api.model.RejectVideoCallDTO; import de.caritas.cob.videoservice.api.service.RejectVideoCallService; +import de.caritas.cob.videoservice.api.service.video.VideoCallUrlGeneratorService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -48,6 +49,9 @@ public class VideoControllerIT { @MockBean private RoleAuthorizationAuthorityMapper roleAuthorizationAuthorityMapper; + @MockBean + private VideoCallUrlGeneratorService videoCallUrlGeneratorService; + @Test public void createVideoCall_Should_ReturnCreated_When_EverythingSucceeded() throws Exception { From cb2eca3a26a063fd58e2bd0a02b873d46ee113b4 Mon Sep 17 00:00:00 2001 From: hill-daniel <20245376+hill-daniel@users.noreply.github.com> Date: Tue, 5 Apr 2022 12:45:01 +0200 Subject: [PATCH 3/3] feat: enable web token creation for anonymous users --- .../api/authorization/Authority.java | 14 +- .../api/authorization/UserRole.java | 15 ++ ...{AuthenticatedUser.java => VideoUser.java} | 4 +- .../api/facade/StartVideoCallFacade.java | 4 +- .../SecurityHeaderSupplier.java | 4 +- .../video/jwt/TokenGeneratorService.java | 20 ++- .../config/AuthenticatedUserConfig.java | 39 ++++- .../config/security/WebSecurityConfig.java | 2 +- .../resources/application-testing.properties | 7 + .../VideoControllerAuthorizationIT.java | 136 +++++++++++++----- .../api/facade/StartVideoCallFacadeTest.java | 4 +- .../SecurityHeaderSupplierTest.java | 6 +- .../video/jwt/TokenGeneratorServiceTest.java | 28 +++- .../api/testhelper/PathConstants.java | 4 + .../api/testhelper/TestConstants.java | 5 +- 15 files changed, 223 insertions(+), 69 deletions(-) create mode 100644 src/main/java/de/caritas/cob/videoservice/api/authorization/UserRole.java rename src/main/java/de/caritas/cob/videoservice/api/authorization/{AuthenticatedUser.java => VideoUser.java} (85%) diff --git a/src/main/java/de/caritas/cob/videoservice/api/authorization/Authority.java b/src/main/java/de/caritas/cob/videoservice/api/authorization/Authority.java index ab01735..b39ca7e 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/authorization/Authority.java +++ b/src/main/java/de/caritas/cob/videoservice/api/authorization/Authority.java @@ -7,14 +7,14 @@ */ public enum Authority { - CONSULTANT("consultant", "AUTHORIZATION_CONSULTANT_DEFAULT"), - USER("user", "AUTHORIZATION_USER_DEFAULT"); + CONSULTANT(UserRole.CONSULTANT, "AUTHORIZATION_CONSULTANT_DEFAULT"), + USER(UserRole.USER, "AUTHORIZATION_USER_DEFAULT"); - private final String roleName; + private final UserRole role; private final String authorityName; - Authority(final String roleName, final String authorityName) { - this.roleName = roleName; + Authority(final UserRole role, final String authorityName) { + this.role = role; this.authorityName = authorityName; } @@ -26,14 +26,14 @@ public enum Authority { */ public static Authority fromRoleName(String roleName) { return Stream.of(values()) - .filter(authority -> authority.roleName.equals(roleName)) + .filter(authority -> authority.role.getValue().equals(roleName)) .findFirst() .orElse(null); } /** * Returns the authority name for the given {@link Authority}. - * + * * @return authority name for the given {@link Authority} **/ public String getAuthority() { diff --git a/src/main/java/de/caritas/cob/videoservice/api/authorization/UserRole.java b/src/main/java/de/caritas/cob/videoservice/api/authorization/UserRole.java new file mode 100644 index 0000000..efd7594 --- /dev/null +++ b/src/main/java/de/caritas/cob/videoservice/api/authorization/UserRole.java @@ -0,0 +1,15 @@ +package de.caritas.cob.videoservice.api.authorization; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum UserRole { + + USER("user"), + CONSULTANT("consultant"); + + private final String value; + +} diff --git a/src/main/java/de/caritas/cob/videoservice/api/authorization/AuthenticatedUser.java b/src/main/java/de/caritas/cob/videoservice/api/authorization/VideoUser.java similarity index 85% rename from src/main/java/de/caritas/cob/videoservice/api/authorization/AuthenticatedUser.java rename to src/main/java/de/caritas/cob/videoservice/api/authorization/VideoUser.java index acb7aa9..91a0504 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/authorization/AuthenticatedUser.java +++ b/src/main/java/de/caritas/cob/videoservice/api/authorization/VideoUser.java @@ -17,7 +17,7 @@ @NoArgsConstructor @Getter @Setter -public class AuthenticatedUser { +public class VideoUser { @NonNull private String userId; @@ -32,6 +32,6 @@ public class AuthenticatedUser { @JsonIgnore public boolean isConsultant() { - return nonNull(roles) && roles.contains(Authority.CONSULTANT.name()); + return nonNull(roles) && roles.contains(UserRole.CONSULTANT.getValue()); } } diff --git a/src/main/java/de/caritas/cob/videoservice/api/facade/StartVideoCallFacade.java b/src/main/java/de/caritas/cob/videoservice/api/facade/StartVideoCallFacade.java index 8c2ea52..9a99043 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/facade/StartVideoCallFacade.java +++ b/src/main/java/de/caritas/cob/videoservice/api/facade/StartVideoCallFacade.java @@ -3,7 +3,7 @@ import static de.caritas.cob.videoservice.api.service.session.SessionStatus.IN_PROGRESS; import static java.util.Collections.singletonList; -import de.caritas.cob.videoservice.api.authorization.AuthenticatedUser; +import de.caritas.cob.videoservice.api.authorization.VideoUser; import de.caritas.cob.videoservice.api.exception.httpresponse.BadRequestException; import de.caritas.cob.videoservice.api.model.CreateVideoCallResponseDTO; import de.caritas.cob.videoservice.api.service.LogService; @@ -31,7 +31,7 @@ public class StartVideoCallFacade { private final @NonNull SessionService sessionService; private final @NonNull LiveEventNotificationService liveEventNotificationService; - private final @NonNull AuthenticatedUser authenticatedUser; + private final @NonNull VideoUser authenticatedUser; private final @NonNull VideoCallUrlGeneratorService videoCallUrlGeneratorService; private final @NonNull UuidRegistry uuidRegistry; private final @NonNull StatisticsService statisticsService; diff --git a/src/main/java/de/caritas/cob/videoservice/api/service/securityheader/SecurityHeaderSupplier.java b/src/main/java/de/caritas/cob/videoservice/api/service/securityheader/SecurityHeaderSupplier.java index 181c302..7d50b7b 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/service/securityheader/SecurityHeaderSupplier.java +++ b/src/main/java/de/caritas/cob/videoservice/api/service/securityheader/SecurityHeaderSupplier.java @@ -1,6 +1,6 @@ package de.caritas.cob.videoservice.api.service.securityheader; -import de.caritas.cob.videoservice.api.authorization.AuthenticatedUser; +import de.caritas.cob.videoservice.api.authorization.VideoUser; import java.util.UUID; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -13,7 +13,7 @@ @RequiredArgsConstructor public class SecurityHeaderSupplier { - private final @NonNull AuthenticatedUser authenticatedUser; + private final @NonNull VideoUser authenticatedUser; @Value("${csrf.header.property}") private String csrfHeaderProperty; diff --git a/src/main/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorService.java b/src/main/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorService.java index 7238001..280f1ca 100644 --- a/src/main/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorService.java +++ b/src/main/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorService.java @@ -7,7 +7,7 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator.Builder; import com.auth0.jwt.algorithms.Algorithm; -import de.caritas.cob.videoservice.api.authorization.AuthenticatedUser; +import de.caritas.cob.videoservice.api.authorization.VideoUser; import de.caritas.cob.videoservice.api.exception.httpresponse.InternalServerErrorException; import de.caritas.cob.videoservice.api.service.decoder.UsernameDecoder; import de.caritas.cob.videoservice.api.service.video.jwt.model.VideoCallToken; @@ -16,7 +16,8 @@ import java.util.HashMap; import java.util.Map; import lombok.NonNull; -import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; @@ -25,11 +26,16 @@ /** * JWT token generator service. */ -@RequiredArgsConstructor @Service public class TokenGeneratorService { - private final @NonNull AuthenticatedUser authenticatedUser; + @Autowired + public TokenGeneratorService( + @NonNull @Qualifier("AuthenticatedOrAnonymousUser") VideoUser authenticatedUser) { + this.videoUser = authenticatedUser; + } + + private final @NonNull VideoUser videoUser; private static final String CONTEXT_CLAIM = "context"; private static final String ROOM_CLAIM = "room"; @@ -67,7 +73,7 @@ public void initAlgorithm() { * @return token */ public String generateToken(String roomId) { - return authenticatedUser.isConsultant() + return videoUser.isConsultant() ? generateModeratorToken(roomId) : generateNonModeratorToken(roomId); } @@ -147,7 +153,7 @@ public String generateModeratorToken(String roomId, String guestVideoCallUrl) { * @return token */ public String generateModeratorToken(String roomId) { - var userContext = createUserContext(authenticatedUser.getUsername()); + var userContext = createUserContext(videoUser.getUsername()); return buildBasicJwt(roomId) .withClaim(MODERATOR_CLAIM, true) @@ -158,7 +164,7 @@ public String generateModeratorToken(String roomId) { private String buildModeratorJwt(String roomId, String guestVideoCallUrl) { return buildBasicJwt(roomId) .withClaim(MODERATOR_CLAIM, true) - .withClaim(CONTEXT_CLAIM, createUserContext(authenticatedUser.getUsername())) + .withClaim(CONTEXT_CLAIM, createUserContext(videoUser.getUsername())) .withClaim(GUEST_URL_CLAIM, guestVideoCallUrl) .sign(algorithm); } diff --git a/src/main/java/de/caritas/cob/videoservice/config/AuthenticatedUserConfig.java b/src/main/java/de/caritas/cob/videoservice/config/AuthenticatedUserConfig.java index 04102e8..3e491b6 100644 --- a/src/main/java/de/caritas/cob/videoservice/config/AuthenticatedUserConfig.java +++ b/src/main/java/de/caritas/cob/videoservice/config/AuthenticatedUserConfig.java @@ -2,14 +2,17 @@ import static java.util.Objects.isNull; -import de.caritas.cob.videoservice.api.authorization.AuthenticatedUser; +import de.caritas.cob.videoservice.api.authorization.VideoUser; import de.caritas.cob.videoservice.api.exception.KeycloakException; +import java.security.Principal; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.web.context.WebApplicationContext; @@ -17,28 +20,50 @@ import org.springframework.web.context.request.ServletRequestAttributes; /** - * Configuration for the {@link AuthenticatedUser}. + * Configuration for the {@link VideoUser}. */ @Configuration public class AuthenticatedUserConfig { private static final String CLAIM_NAME_USER_ID = "userId"; private static final String CLAIM_NAME_USERNAME = "username"; + private static final VideoUser ANONYMOUS_USER = new VideoUser(); /** * Returns the currently authenticated user. * - * @return {@link AuthenticatedUser} + * @return {@link VideoUser} */ @Bean + @Primary @Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) - public AuthenticatedUser getAuthenticatedUser() { + public VideoUser getAuthenticatedUser() { + var userPrincipal = getRequest().getUserPrincipal(); + return createAuthenticatedUser(userPrincipal); + } - var keycloakSecContext = ((KeycloakAuthenticationToken) getRequest().getUserPrincipal()) - .getAccount().getKeycloakSecurityContext(); + /** + * Returns the currently authenticated user, or an anonymous representation. + * + * @return {@link VideoUser} + */ + @Bean + @Qualifier("AuthenticatedOrAnonymousUser") + @Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) + public VideoUser getAuthenticatedOrAnonymousUser() { + var userPrincipal = getRequest().getUserPrincipal(); + if (isNull(userPrincipal)) { + return ANONYMOUS_USER; + } + return createAuthenticatedUser(userPrincipal); + } + + private VideoUser createAuthenticatedUser(Principal userPrincipal) { + var keycloakUser = (KeycloakAuthenticationToken) userPrincipal; + var keycloakSecContext = keycloakUser.getAccount().getKeycloakSecurityContext(); Map claimMap = keycloakSecContext.getToken().getOtherClaims(); - var authenticatedUser = new AuthenticatedUser(); + var authenticatedUser = new VideoUser(); authenticatedUser.setAccessToken(getUserAccessToken(keycloakSecContext)); authenticatedUser.setUserId(getUserAttribute(claimMap, CLAIM_NAME_USER_ID)); authenticatedUser.setUsername(getUserAttribute(claimMap, CLAIM_NAME_USERNAME)); diff --git a/src/main/java/de/caritas/cob/videoservice/config/security/WebSecurityConfig.java b/src/main/java/de/caritas/cob/videoservice/config/security/WebSecurityConfig.java index bd4c7a5..3849497 100644 --- a/src/main/java/de/caritas/cob/videoservice/config/security/WebSecurityConfig.java +++ b/src/main/java/de/caritas/cob/videoservice/config/security/WebSecurityConfig.java @@ -65,7 +65,7 @@ protected void configure(HttpSecurity http) throws Exception { .antMatchers("/videocalls/reject") .hasAnyAuthority(USER.getAuthority()) .antMatchers("/videocalls/*/jwt") - .hasAnyAuthority(CONSULTANT.getAuthority()) //TODO: add anonymous + .permitAll() .anyRequest().denyAll() .and() .exceptionHandling() diff --git a/src/main/resources/application-testing.properties b/src/main/resources/application-testing.properties index 427036d..e30421a 100644 --- a/src/main/resources/application-testing.properties +++ b/src/main/resources/application-testing.properties @@ -13,3 +13,10 @@ csrf.cookie.property=csrfCookie # Statistics statistics.enabled=true + +# JWT +video.call.security.jwt.audience=test_server +video.call.security.jwt.issuer=test_app_client +video.call.security.jwt.subject=meet.jitsi +video.call.security.jwt.secret=95148E36-19AA-4A4B-8F5A-FC6245A36912 +video.call.security.jwt.validity.hours=3 \ No newline at end of file diff --git a/src/test/java/de/caritas/cob/videoservice/api/controller/VideoControllerAuthorizationIT.java b/src/test/java/de/caritas/cob/videoservice/api/controller/VideoControllerAuthorizationIT.java index 448519d..42344ff 100644 --- a/src/test/java/de/caritas/cob/videoservice/api/controller/VideoControllerAuthorizationIT.java +++ b/src/test/java/de/caritas/cob/videoservice/api/controller/VideoControllerAuthorizationIT.java @@ -1,5 +1,6 @@ package de.caritas.cob.videoservice.api.controller; +import static de.caritas.cob.videoservice.api.testhelper.PathConstants.PATH_GET_WEB_TOKEN; import static de.caritas.cob.videoservice.api.testhelper.PathConstants.PATH_REJECT_VIDEO_CALL; import static de.caritas.cob.videoservice.api.testhelper.PathConstants.PATH_START_VIDEO_CALL; import static de.caritas.cob.videoservice.api.testhelper.RequestBodyConstants.VALID_START_VIDEO_CALL_BODY; @@ -9,6 +10,7 @@ import static de.caritas.cob.videoservice.api.testhelper.TestConstants.CSRF_COOKIE; import static de.caritas.cob.videoservice.api.testhelper.TestConstants.CSRF_HEADER; import static de.caritas.cob.videoservice.api.testhelper.TestConstants.CSRF_VALUE; +import static de.caritas.cob.videoservice.api.testhelper.TestConstants.RC_CHAT_ROOM_ID; import static de.caritas.cob.videoservice.api.testhelper.TestConstants.RC_USER_ID_HEADER; import static de.caritas.cob.videoservice.api.testhelper.TestConstants.RC_USER_ID_VALUE; import static de.caritas.cob.videoservice.api.testhelper.TestConstants.SESSION_ID; @@ -19,6 +21,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -26,6 +29,7 @@ import de.caritas.cob.videoservice.api.facade.StartVideoCallFacade; import de.caritas.cob.videoservice.api.model.RejectVideoCallDTO; import de.caritas.cob.videoservice.api.service.RejectVideoCallService; +import de.caritas.cob.videoservice.api.service.video.jwt.TokenGeneratorService; import javax.servlet.http.Cookie; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,6 +38,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; @@ -54,8 +59,12 @@ public class VideoControllerAuthorizationIT { @MockBean private RejectVideoCallService rejectVideoCallService; + @MockBean + private TokenGeneratorService tokenGeneratorService; + private final Cookie csrfCookie = new Cookie(CSRF_COOKIE, CSRF_VALUE); + @Test @WithMockUser(authorities = AUTHORITY_CONSULTANT) public void createVideoCall_Should_ReturnCreated_When_EverythingSucceeded() throws Exception { @@ -64,12 +73,12 @@ public void createVideoCall_Should_ReturnCreated_When_EverythingSucceeded() thro CREATE_VIDEO_CALL_RESPONSE_DTO); mvc.perform(post(PATH_START_VIDEO_CALL) - .cookie(csrfCookie) - .header(CSRF_HEADER, CSRF_VALUE) - .header(RC_USER_ID_HEADER, RC_USER_ID_VALUE) - .contentType(MediaType.APPLICATION_JSON) - .content(VALID_START_VIDEO_CALL_BODY) - .accept(MediaType.APPLICATION_JSON)) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .header(RC_USER_ID_HEADER, RC_USER_ID_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .content(VALID_START_VIDEO_CALL_BODY) + .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()); } @@ -81,11 +90,11 @@ public void createVideoCall_Should_ReturnUnauthorized_When_AuthorizationIsMissin CREATE_VIDEO_CALL_RESPONSE_DTO); mvc.perform(post(PATH_START_VIDEO_CALL) - .cookie(csrfCookie) - .header(CSRF_HEADER, CSRF_VALUE) - .contentType(MediaType.APPLICATION_JSON) - .content(VALID_START_VIDEO_CALL_BODY) - .accept(MediaType.APPLICATION_JSON)) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .content(VALID_START_VIDEO_CALL_BODY) + .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isUnauthorized()); } @@ -95,11 +104,11 @@ public void createVideoCall_Should_ReturnForbiddenAndCallNoMethods_WhenNoConsult throws Exception { mvc.perform(post(PATH_START_VIDEO_CALL) - .cookie(csrfCookie) - .header(CSRF_HEADER, CSRF_VALUE) - .contentType(MediaType.APPLICATION_JSON) - .content(VALID_START_VIDEO_CALL_BODY) - .accept(MediaType.APPLICATION_JSON)) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .content(VALID_START_VIDEO_CALL_BODY) + .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isForbidden()); verifyNoMoreInteractions(startVideoCallFacade); @@ -111,9 +120,9 @@ public void createVideoCall_Should_ReturnForbiddenAndCallNoMethods_WhenNoCsrfTok throws Exception { mvc.perform(post(PATH_START_VIDEO_CALL) - .contentType(MediaType.APPLICATION_JSON) - .content(VALID_START_VIDEO_CALL_BODY) - .accept(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .content(VALID_START_VIDEO_CALL_BODY) + .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isForbidden()); verifyNoMoreInteractions(startVideoCallFacade); @@ -129,9 +138,9 @@ public void rejectVideoCall_Should_ReturnForbiddenAndCallNoMethods_WhenNoCsrfTok .initiatorRcUserId("rcUserId")); mvc.perform(post(PATH_REJECT_VIDEO_CALL) - .contentType(MediaType.APPLICATION_JSON) - .content(content) - .accept(MediaType.APPLICATION_JSON)) + .contentType(MediaType.APPLICATION_JSON) + .content(content) + .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isForbidden()); verifyNoMoreInteractions(rejectVideoCallService); @@ -147,11 +156,11 @@ public void rejectVideoCall_Should_ReturnForbiddenAndCallNoMethods_WhenNoAuthori .initiatorRcUserId("rcUserId")); mvc.perform(post(PATH_REJECT_VIDEO_CALL) - .cookie(csrfCookie) - .header(CSRF_HEADER, CSRF_VALUE) - .contentType(MediaType.APPLICATION_JSON) - .content(content) - .accept(MediaType.APPLICATION_JSON)) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .content(content) + .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isForbidden()); verifyNoMoreInteractions(rejectVideoCallService); @@ -167,14 +176,77 @@ public void rejectVideoCall_Should_ReturnOkAndCallService_WhenUserRole() .initiatorRcUserId("rcUserId")); mvc.perform(post(PATH_REJECT_VIDEO_CALL) - .cookie(csrfCookie) - .header(CSRF_HEADER, CSRF_VALUE) - .contentType(MediaType.APPLICATION_JSON) - .content(content) - .accept(MediaType.APPLICATION_JSON)) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .content(content) + .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); verify(rejectVideoCallService, times(1)).rejectVideoCall(any()); } + @Test + @WithAnonymousUser + public void getWebToken_should_generate_token_for_anonymous_user() throws Exception { + mvc.perform(get(PATH_GET_WEB_TOKEN) + .header(RC_USER_ID_HEADER, RC_USER_ID_VALUE) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + verify(tokenGeneratorService).generateToken(RC_CHAT_ROOM_ID); + } + + @Test + @WithAnonymousUser + public void getWebToken_should_return_forbidden_for_request_without_csrf() throws Exception { + mvc.perform(get(PATH_GET_WEB_TOKEN) + .header(RC_USER_ID_HEADER, RC_USER_ID_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @WithAnonymousUser + public void getWebToken_should_return_bad_request_for_request_without_rocket_chat_user_id() + throws Exception { + mvc.perform(get(PATH_GET_WEB_TOKEN) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(authorities = {AUTHORITY_USER}) + public void getWebToken_should_generate_token_for_user() throws Exception { + mvc.perform(get(PATH_GET_WEB_TOKEN) + .header(RC_USER_ID_HEADER, RC_USER_ID_VALUE) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + verify(tokenGeneratorService).generateToken(RC_CHAT_ROOM_ID); + } + + @Test + @WithMockUser(authorities = {AUTHORITY_CONSULTANT}) + public void getWebToken_should_generate_token_for_consultant() throws Exception { + mvc.perform(get(PATH_GET_WEB_TOKEN) + .header(RC_USER_ID_HEADER, RC_USER_ID_VALUE) + .cookie(csrfCookie) + .header(CSRF_HEADER, CSRF_VALUE) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + verify(tokenGeneratorService).generateToken(RC_CHAT_ROOM_ID); + } } diff --git a/src/test/java/de/caritas/cob/videoservice/api/facade/StartVideoCallFacadeTest.java b/src/test/java/de/caritas/cob/videoservice/api/facade/StartVideoCallFacadeTest.java index 6cee49c..26fc01e 100644 --- a/src/test/java/de/caritas/cob/videoservice/api/facade/StartVideoCallFacadeTest.java +++ b/src/test/java/de/caritas/cob/videoservice/api/facade/StartVideoCallFacadeTest.java @@ -16,7 +16,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import de.caritas.cob.videoservice.api.authorization.AuthenticatedUser; +import de.caritas.cob.videoservice.api.authorization.VideoUser; import de.caritas.cob.videoservice.api.exception.httpresponse.BadRequestException; import de.caritas.cob.videoservice.api.model.CreateVideoCallResponseDTO; import de.caritas.cob.videoservice.api.service.UuidRegistry; @@ -52,7 +52,7 @@ public class StartVideoCallFacadeTest { @Mock private VideoCallUrlGeneratorService videoCallUrlGeneratorService; @Mock - private AuthenticatedUser authenticatedUser; + private VideoUser authenticatedUser; @Mock private UuidRegistry uuidRegistry; @Mock diff --git a/src/test/java/de/caritas/cob/videoservice/api/service/securityheader/SecurityHeaderSupplierTest.java b/src/test/java/de/caritas/cob/videoservice/api/service/securityheader/SecurityHeaderSupplierTest.java index c7002fb..563c715 100644 --- a/src/test/java/de/caritas/cob/videoservice/api/service/securityheader/SecurityHeaderSupplierTest.java +++ b/src/test/java/de/caritas/cob/videoservice/api/service/securityheader/SecurityHeaderSupplierTest.java @@ -11,7 +11,7 @@ import static org.mockito.Mockito.when; import static org.springframework.test.util.ReflectionTestUtils.setField; -import de.caritas.cob.videoservice.api.authorization.AuthenticatedUser; +import de.caritas.cob.videoservice.api.authorization.VideoUser; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -27,7 +27,7 @@ public class SecurityHeaderSupplierTest { @InjectMocks private SecurityHeaderSupplier securityHeaderSupplier; @Mock - private AuthenticatedUser authenticatedUser; + private VideoUser videoUser; @Before public void setup() { @@ -65,7 +65,7 @@ public void getKeycloakAndCsrfHttpHeaders_Should_Return_CorrectHeaderAndCookieVa @Test public void getRocketChatAndCsrfHttpHeaders_Should_ReturnHeaderWithKeycloakAuthToken() { - when(authenticatedUser.getAccessToken()).thenReturn(BEARER_TOKEN); + when(videoUser.getAccessToken()).thenReturn(BEARER_TOKEN); HttpHeaders result = securityHeaderSupplier.getKeycloakAndCsrfHttpHeaders(); diff --git a/src/test/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorServiceTest.java b/src/test/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorServiceTest.java index a99a64e..a0ca77d 100644 --- a/src/test/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorServiceTest.java +++ b/src/test/java/de/caritas/cob/videoservice/api/service/video/jwt/TokenGeneratorServiceTest.java @@ -14,7 +14,7 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.impl.NullClaim; import com.auth0.jwt.interfaces.DecodedJWT; -import de.caritas.cob.videoservice.api.authorization.AuthenticatedUser; +import de.caritas.cob.videoservice.api.authorization.VideoUser; import de.caritas.cob.videoservice.api.exception.httpresponse.InternalServerErrorException; import de.caritas.cob.videoservice.api.service.video.jwt.model.VideoCallToken; import org.junit.Before; @@ -35,7 +35,7 @@ public class TokenGeneratorServiceTest { private TokenGeneratorService tokenGeneratorService; @Mock - private AuthenticatedUser authenticatedUser; + private VideoUser authenticatedUser; @Before public void setup() { @@ -122,4 +122,28 @@ public void generateModeratorToken_Should_returnExpectedToken_When_ParamsAreGive assertThat(JWT.decode(moderatorToken).getClaim("guestVideoCallUrl").asString(), is(GUEST_VIDEO_CALL_URL)); } + + @Test + public void generateToken_should_generate_moderator_token_if_user_is_consultant() { + when(authenticatedUser.getUsername()).thenReturn(USERNAME); + when(authenticatedUser.isConsultant()).thenReturn(true); + + var moderatorToken = tokenGeneratorService.generateToken("privateRoom4711"); + + verifyBasicTokenFields(moderatorToken, "privateRoom4711"); + assertThat(JWT.decode(moderatorToken).getClaim("moderator").asBoolean(), is(true)); + assertThat(JWT.decode(moderatorToken).getClaim("context").asMap().get("user").toString(), + is("{name=" + USERNAME + "}")); + } + + @Test + public void generateToken_should_generate_non_moderator_token_if_user_is_no_consultant() { + when(authenticatedUser.isConsultant()).thenReturn(false); + + var moderatorToken = tokenGeneratorService.generateToken("privateRoom4711"); + + assertThat(JWT.decode(moderatorToken).getClaim("moderator").isNull(), is(true)); + assertThat(JWT.decode(moderatorToken).getClaim("context").isNull(), is(true)); + verifyBasicTokenFields(moderatorToken, "privateRoom4711"); + } } diff --git a/src/test/java/de/caritas/cob/videoservice/api/testhelper/PathConstants.java b/src/test/java/de/caritas/cob/videoservice/api/testhelper/PathConstants.java index 7aa1aa3..3eea877 100644 --- a/src/test/java/de/caritas/cob/videoservice/api/testhelper/PathConstants.java +++ b/src/test/java/de/caritas/cob/videoservice/api/testhelper/PathConstants.java @@ -1,7 +1,11 @@ package de.caritas.cob.videoservice.api.testhelper; +import static de.caritas.cob.videoservice.api.testhelper.TestConstants.RC_CHAT_ROOM_ID; + public class PathConstants { public static final String PATH_START_VIDEO_CALL = "/videocalls/new"; public static final String PATH_REJECT_VIDEO_CALL = "/videocalls/reject"; + public static final String PATH_GET_WEB_TOKEN = "/videocalls/" + RC_CHAT_ROOM_ID + "/jwt"; + } diff --git a/src/test/java/de/caritas/cob/videoservice/api/testhelper/TestConstants.java b/src/test/java/de/caritas/cob/videoservice/api/testhelper/TestConstants.java index 4e5a3c3..3b7aefe 100644 --- a/src/test/java/de/caritas/cob/videoservice/api/testhelper/TestConstants.java +++ b/src/test/java/de/caritas/cob/videoservice/api/testhelper/TestConstants.java @@ -16,10 +16,11 @@ public class TestConstants { public static final String BEARER_TOKEN = "w948hisidfgjaoidg839huishdfkjsdkfjhsdf34"; /* - * RC Header + * RC */ public static final String RC_USER_ID_HEADER = "RCUserId"; public static final String RC_USER_ID_VALUE = "rcUser123"; + public static final String RC_CHAT_ROOM_ID = "R_ID_76543210"; /* * CSRF token @@ -52,7 +53,7 @@ public class TestConstants { public static final String CONSULTANT_ID = "fb3cbee2-c5f3-4582-a5e4-d853572e9860"; /* - * Common + * Common */ public static final String ERROR_MESSAGE = "Error message"; }