Generate derived authentication tokens
All checks were successful
continuous-integration/drone/push Build is passing

Can be used to authorize download urls via query parameter since
the tokens can be used only once.
This commit is contained in:
Armin Friedl 2020-07-21 21:24:10 +02:00
parent c07a7866ce
commit a07379ebad
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
20 changed files with 483 additions and 189 deletions

View file

@ -47,6 +47,11 @@ Content-Type: application/json
:token
{"name": "Fling from querysheet with Auth and very long name", "expirationClicks": 12, "shared": true, "authCode": "abc"}
# GET derived auth token
GET http://localhost:8080/api/auth/derive
Content-Type: application/json
:token
#
:flingId = dfc208a3-5924-43b4-aa6a-c263541dca5e

View file

@ -1,17 +1,23 @@
package net.friedl.fling.controller;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.security.FlingWebSecurityConfigurer;
import net.friedl.fling.service.AuthenticationService;
@RestController
@ -50,4 +56,23 @@ public class AuthenticationController {
return authenticationService.authenticate(userAuthDto)
.orElseThrow(() -> new AccessDeniedException("Wrong username or password"));
}
//@formatter:off
/**
* Note that this endpoint is not protected. But the token will only get authority of the
* authenticated user.
* @see FlingWebSecurityConfigurer
* @see AuthenticationService
*/
@Operation(description = "Generate a derived token from the current authorization")
@ApiResponse(responseCode = "200", description = "Token impersonating the user")
@SecurityRequirement(name = "bearer")
@GetMapping("/derive")
public String deriveToken(
@Parameter(allowEmptyValue = true, description = "Token can only be used for authorizing one request. Defaults to true")
@RequestParam Optional<Boolean> singleUse)
{
return authenticationService.deriveToken(singleUse.orElse(true));
}
//@formatter:on
}

View file

@ -0,0 +1,38 @@
package net.friedl.fling.persistence.entities;
import java.time.Instant;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "Token")
@Getter
@Setter
public class TokenEntity {
@Id
private UUID id; // Note that this is not generated to ensure randomness independent from the
// persistence provider
@Column(nullable = false)
private Boolean singleUse = true;
@Column(nullable = false)
private String token; // JWT token this token is derived from
@CreationTimestamp
private Instant creationTime;
@UpdateTimestamp
private Instant updateTime;
@Version
private Long version;
}

View file

@ -0,0 +1,8 @@
package net.friedl.fling.persistence.repositories;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import net.friedl.fling.persistence.entities.TokenEntity;
public interface TokenRepository extends JpaRepository<TokenEntity, UUID> {
}

View file

@ -4,7 +4,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
public enum FlingAuthorities {
FLING_ADMIN("admin"), FLING_USER("user");
FLING_ADMIN("admin"), FLING_USER("user"), FLING_TOKEN("token");
String authority;

View file

@ -1,6 +1,7 @@
package net.friedl.fling.security;
import static net.friedl.fling.security.FlingAuthorities.FLING_ADMIN;
import static net.friedl.fling.security.FlingAuthorities.FLING_USER;
import static org.springframework.security.config.Customizer.withDefaults;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
@ -18,8 +19,8 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.FlingSecurityConfiguration;
import net.friedl.fling.security.authentication.JwtAuthenticationFilter;
import net.friedl.fling.security.authentication.filter.BearerAuthenticationFilter;
import net.friedl.fling.security.authentication.filter.TokenAuthenticationFilter;
import net.friedl.fling.service.AuthorizationService;
@Slf4j
@ -30,15 +31,18 @@ import net.friedl.fling.service.AuthorizationService;
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private List<String> allowedOrigins;
private JwtAuthenticationFilter jwtAuthenticationFilter;
private TokenAuthenticationFilter tokenAuthenticationFilter;
private BearerAuthenticationFilter bearerAuthenticationFilter;
private AuthorizationService authorizationService;
@Autowired
public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter,
AuthorizationService authorizationService,
FlingSecurityConfiguration securityConfiguraiton) {
public FlingWebSecurityConfigurer(
TokenAuthenticationFilter tokenAuthenticationFilter,
BearerAuthenticationFilter bearerAuthenticationFilter,
AuthorizationService authorizationService) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.tokenAuthenticationFilter = tokenAuthenticationFilter;
this.bearerAuthenticationFilter = bearerAuthenticationFilter;
this.authorizationService = authorizationService;
}
@ -52,7 +56,8 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**********************************************/
/** Authentication Interceptor Configuration **/
/**********************************************/
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(bearerAuthenticationFilter, TokenAuthenticationFilter.class)
// Do not keep authorization token in session. This would interfere with bearer authentication
// in that it is possible to authenticate without a bearer token if the session is kept.
// Turn off this confusing and non-obvious behavior.
@ -68,6 +73,10 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**********************************/
/** Authorization for: /api/auth **/
/**********************************/
.authorizeRequests()
.antMatchers("/api/auth/derive")
.hasAnyAuthority(FLING_ADMIN.getAuthority(), FLING_USER.getAuthority())
.and()
.authorizeRequests()
.antMatchers("/api/auth/**")
.permitAll()

View file

@ -4,25 +4,31 @@ import java.util.List;
import java.util.UUID;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
public class FlingToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = -1112423505610346583L;
private String jwtToken;
private String token;
public FlingToken(GrantedAuthority authority, String jwtToken) {
super(List.of(authority));
this.jwtToken = jwtToken;
public FlingToken(List<GrantedAuthority> authorities, String token) {
super(authorities);
this.token = token;
}
public boolean authorizedForFling(UUID id) {
for (GrantedAuthority grantedAuthority : getAuthorities()) {
if (grantedAuthority instanceof FlingAdminAuthority) return true;
if (grantedAuthority instanceof FlingAdminAuthority) {
return true;
}
if (!(grantedAuthority instanceof FlingUserAuthority)) continue;
UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId();
if (grantedFlingId.equals(id)) return true;
if (grantedAuthority instanceof FlingUserAuthority) {
UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId();
if (grantedFlingId.equals(id)) {
return true;
}
}
}
return false;
@ -30,7 +36,7 @@ public class FlingToken extends AbstractAuthenticationToken {
@Override
public String getCredentials() {
return this.jwtToken;
return this.token;
}
@Override

View file

@ -1,4 +1,4 @@
package net.friedl.fling.security.authentication;
package net.friedl.fling.security.authentication.authorities;
import org.springframework.security.core.GrantedAuthority;
import net.friedl.fling.security.FlingAuthorities;

View file

@ -1,4 +1,4 @@
package net.friedl.fling.security.authentication;
package net.friedl.fling.security.authentication.authorities;
import java.util.UUID;
import org.springframework.security.core.GrantedAuthority;

View file

@ -1,4 +1,4 @@
package net.friedl.fling.security.authentication;
package net.friedl.fling.security.authentication.filter;
import java.io.IOException;
import java.util.stream.Collectors;
@ -13,18 +13,19 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.service.AuthenticationService;
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
public class BearerAuthenticationFilter extends OncePerRequestFilter {
private static final String TOKEN_PREFIX = "Bearer ";
private static final String HEADER_STRING = "Authorization";
private AuthenticationService authenticationService;
@Autowired
public JwtAuthenticationFilter(AuthenticationService authenticationService) {
public BearerAuthenticationFilter(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@ -37,7 +38,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
log.info("Anonymous request for {} {}{}", request.getMethod(), request.getRequestURL(),
request.getQueryString() != null ? "?"+request.getQueryString(): "");
request.getQueryString() != null ? "?" + request.getQueryString() : "");
filterChain.doFilter(request, response);
return;
}
@ -48,8 +49,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (securityContext.getAuthentication() == null) {
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
request.getQueryString() != null ? "?"+request.getQueryString(): "");
FlingToken token = authenticationService.parseAuthentication(authToken);
request.getQueryString() != null ? "?" + request.getQueryString() : "");
FlingToken token = authenticationService.parseJwtAuthentication(authToken);
log.info("Authenticated as {}", token.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
securityContext.setAuthentication(token);

View file

@ -0,0 +1,59 @@
package net.friedl.fling.security.authentication.filter;
import java.io.IOException;
import java.util.stream.Collectors;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.service.AuthenticationService;
@Slf4j
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private AuthenticationService authenticationService;
@Autowired
public TokenAuthenticationFilter(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String derivedToken = request.getParameter("derivedtoken");
if (derivedToken == null) {
log.info("No derived token in request for {} {}{}", request.getMethod(),
request.getRequestURL(),
request.getQueryString() != null ? "?" + request.getQueryString() : "");
filterChain.doFilter(request, response);
return;
}
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext.getAuthentication() == null) {
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
request.getQueryString() != null ? "?" + request.getQueryString() : "");
FlingToken token = authenticationService.parseDerivedToken(derivedToken);
log.info("Authenticated as {}", token.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
securityContext.setAuthentication(token);
}
filterChain.doFilter(request, response);
}
}

View file

@ -56,7 +56,7 @@ public class ArtifactService {
*/
public ArtifactDto create(UUID flingId, ArtifactDto artifactDto) {
FlingEntity flingEntity = flingRepository.getOne(flingId);
log.debug("Creating new ArtifactEntity for ArtifactDto[.path={}]", artifactDto.getPath());
ArtifactEntity artifactEntity = artifactMapper.map(artifactDto);
artifactEntity.setFling(flingEntity);

View file

@ -3,12 +3,16 @@ package net.friedl.fling.service;
import java.security.Key;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
@ -19,10 +23,12 @@ import lombok.extern.slf4j.Slf4j;
import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.entities.TokenEntity;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.security.authentication.FlingAdminAuthority;
import net.friedl.fling.persistence.repositories.TokenRepository;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.FlingUserAuthority;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@Slf4j
@Service
@ -30,6 +36,7 @@ public class AuthenticationService {
private JwtParser jwtParser;
private Key jwtSigningKey;
private FlingRepository flingRepository;
private TokenRepository tokenRepository;
private PasswordEncoder passwordEncoder;
@Value("${fling.security.admin-name}")
@ -41,12 +48,14 @@ public class AuthenticationService {
@Autowired
public AuthenticationService(JwtParser jwtParser, Key jwtSigningKey,
PasswordEncoder passwordEncoder, FlingRepository flingRepository) {
PasswordEncoder passwordEncoder, FlingRepository flingRepository,
TokenRepository tokenRepository) {
this.jwtParser = jwtParser;
this.jwtSigningKey = jwtSigningKey;
this.passwordEncoder = passwordEncoder;
this.flingRepository = flingRepository;
this.tokenRepository = tokenRepository;
}
public Optional<String> authenticate(AdminAuthDto adminAuth) {
@ -71,7 +80,9 @@ public class AuthenticationService {
public Optional<String> authenticate(UserAuthDto userAuth) {
log.info("Authenticating for fling [.shareId={}]", userAuth.getShareId());
FlingEntity flingEntity = flingRepository.findByShareId(userAuth.getShareId());
if(flingEntity == null) { throw new EntityNotFoundException("No entity for shareId="+userAuth.getShareId()); }
if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + userAuth.getShareId());
}
String providedAuthCodeHash = passwordEncoder.encode(userAuth.getAuthCode());
String actualAuthCodeHash = flingEntity.getAuthCode();
@ -90,15 +101,15 @@ public class AuthenticationService {
}
public FlingToken parseAuthentication(String token) {
public FlingToken parseJwtAuthentication(String token) {
Claims claims = jwtParser.parseClaimsJws(token).getBody();
switch (claims.getSubject()) {
case "admin":
return new FlingToken(new FlingAdminAuthority(), token);
return new FlingToken(List.of(new FlingAdminAuthority()), token);
case "user":
UUID grantedFlingId = UUID.fromString(claims.get("id", String.class));
return new FlingToken(new FlingUserAuthority(grantedFlingId), token);
return new FlingToken(List.of(new FlingUserAuthority(grantedFlingId)), token);
default:
throw new BadCredentialsException("Invalid token");
}
@ -116,4 +127,47 @@ public class AuthenticationService {
.setExpiration(Date.from(Instant.now().plusSeconds(jwtExpiration)))
.signWith(jwtSigningKey);
}
/**
* Creates a derived token with the given settings. Note that the returned string is opaque and
* should not not be interpreted in any way but only used as is.
*
* @param singleUse Whether this token should be deleted after a single use
* @return An opaque string representing the token
*/
@Transactional
public String deriveToken(Boolean singleUse) {
UUID id = UUID.randomUUID();
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(id);
if (singleUse != null) {
tokenEntity.setSingleUse(singleUse);
}
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext.getAuthentication() instanceof FlingToken) {
FlingToken flingToken = (FlingToken) securityContext.getAuthentication();
tokenEntity.setToken(flingToken.getCredentials());
} else {
// This should be prevented in FlingWebSecurityConfigurer
throw new IllegalStateException("Cannot derive token from current authentication");
}
tokenRepository.save(tokenEntity);
return id.toString();
}
@Transactional
public FlingToken parseDerivedToken(String derivedToken) {
TokenEntity tokenEntity = tokenRepository.getOne(UUID.fromString(derivedToken));
FlingToken flingToken = parseJwtAuthentication(tokenEntity.getToken());
if (tokenEntity.getSingleUse()) {
tokenRepository.delete(tokenEntity);
}
return flingToken;
}
}

View file

@ -75,7 +75,9 @@ public class AuthorizationService {
}
FlingEntity flingEntity = flingRepository.findByShareId(shareId);
if(flingEntity == null) { throw new EntityNotFoundException("No entity for shareId="+shareId); }
if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + shareId);
}
return allowFlingAccess(flingEntity.getId(), token);
}

View file

@ -90,7 +90,9 @@ public class FlingService {
public FlingDto getByShareId(String shareId) {
FlingEntity flingEntity = flingRepository.findByShareId(shareId);
if(flingEntity == null) { throw new EntityNotFoundException("No entity for shareId="+shareId); }
if (flingEntity == null) {
throw new EntityNotFoundException("No entity for shareId=" + shareId);
}
return flingMapper.map(flingEntity);
}

View file

@ -84,4 +84,5 @@ public class AuthenticationControllerTest {
.andExpect(status().is(200))
.andExpect(content().string("token"));
}
}

View file

@ -1,24 +1,33 @@
package net.friedl.fling.service;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.security.Key;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.persistence.EntityNotFoundException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
@ -33,10 +42,13 @@ import io.jsonwebtoken.security.Keys;
import net.friedl.fling.model.dto.AdminAuthDto;
import net.friedl.fling.model.dto.UserAuthDto;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.entities.TokenEntity;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.security.authentication.FlingAdminAuthority;
import net.friedl.fling.persistence.repositories.TokenRepository;
import net.friedl.fling.security.FlingAuthorities;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.FlingUserAuthority;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@ExtendWith(SpringExtension.class)
@TestPropertySource("classpath:/application-test.properties")
@ -48,6 +60,9 @@ public class AuthenticationServiceTest {
@MockBean
private FlingRepository flingRepository;
@MockBean
private TokenRepository tokenRepository;
@MockBean
private JwtParser jwtParser;
@ -60,8 +75,11 @@ public class AuthenticationServiceTest {
@Bean
public AuthenticationService authenticationService(JwtParser jwtParser,
PasswordEncoder passwordEncoder, FlingRepository flingRepository) {
return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository);
PasswordEncoder passwordEncoder, FlingRepository flingRepository,
TokenRepository tokenRepository) {
return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository,
tokenRepository);
}
}
@ -118,23 +136,24 @@ public class AuthenticationServiceTest {
@Test
public void authenticate_noFlingForShareId_throws() {
UserAuthDto userAuthDto = UserAuthDto.builder()
UserAuthDto userAuthDto = UserAuthDto.builder()
.authCode("authCode")
.shareId("doesNotExist").build();
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash");
assertThrows(EntityNotFoundException.class, () -> authenticationService.authenticate(userAuthDto));
assertThrows(EntityNotFoundException.class,
() -> authenticationService.authenticate(userAuthDto));
}
@Test
public void parseAuthentication_owner_AdminAuthority() {
public void parseJwtAuthentication_owner_AdminAuthority() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
FlingToken flingToken = authenticationService.parseAuthentication("any");
FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
assertThat(flingToken.isAuthenticated(), equalTo(true));
// authorized for any fling
assertThat(flingToken.authorizedForFling(UUID.randomUUID()), equalTo(true));
@ -144,12 +163,12 @@ public class AuthenticationServiceTest {
}
@Test
public void parseAuthentication_user_UserAuthorityForId() {
public void parseJwtAuthentication_user_UserAuthorityForId() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "user", "id", new UUID(0, 0).toString())), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
FlingToken flingToken = authenticationService.parseAuthentication("any");
FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
assertThat(flingToken.isAuthenticated(), equalTo(true));
// authorized for fling in token
assertThat(flingToken.authorizedForFling(new UUID(0, 0)), equalTo(true));
@ -161,12 +180,110 @@ public class AuthenticationServiceTest {
}
@Test
public void parseAuthentication_unknownSubject_throws() {
public void parseJwtAuthentication_unknownSubject_throws() {
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "unknownSubject")), "signature");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
assertThrows(BadCredentialsException.class,
() -> authenticationService.parseAuthentication("any"));
() -> authenticationService.parseJwtAuthentication("any"));
}
@Test
public void deriveToken_noAuthenticationInSecurityContext_throws() {
assertThrows(IllegalStateException.class,
() -> authenticationService.deriveToken(false));
}
@Test
public void deriveToken_authenticationInSecurityContext_ok() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
String derivedToken = authenticationService.deriveToken(null);
assertThat(derivedToken, is(not(emptyOrNullString())));
SecurityContextHolder.clearContext();
}
@Test
public void deriveToken_singleUseNotSet_singleUseIsTrue() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
ArgumentCaptor<TokenEntity> tokenEntityCaptor = ArgumentCaptor.forClass(TokenEntity.class);
authenticationService.deriveToken(null);
verify(tokenRepository).save(tokenEntityCaptor.capture());
assertThat(tokenEntityCaptor.getValue().getSingleUse(), is(true));
}
@Test
public void deriveToken_singleUseFalse_singleUseIsFalse() {
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "token");
SecurityContextHolder.setContext(new SecurityContextImpl(flingToken));
ArgumentCaptor<TokenEntity> tokenEntityCaptor = ArgumentCaptor.forClass(TokenEntity.class);
authenticationService.deriveToken(false);
verify(tokenRepository).save(tokenEntityCaptor.capture());
assertThat(tokenEntityCaptor.getValue().getSingleUse(), is(false));
}
@Test
public void parseDerivedToken_singleUse_deletesToken() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(true);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
authenticationService.parseDerivedToken(token);
verify(tokenRepository).delete(tokenEntity);
}
@Test
public void parseDerivedToken_singleUseFalse_doesNotDeleteToken() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(false);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
authenticationService.parseDerivedToken(token);
verify(tokenRepository, never()).delete(tokenEntity);
}
@Test
public void parseDerivedToken_returnsParentAuthentication() {
String token = UUID.randomUUID().toString();
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
new DefaultClaims(Map.of("sub", "admin")), "signature");
TokenEntity tokenEntity = new TokenEntity();
tokenEntity.setId(UUID.fromString(token));
tokenEntity.setSingleUse(true);
tokenEntity.setToken("jwtToken");
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
when(tokenRepository.getOne(any(UUID.class))).thenReturn(tokenEntity);
FlingToken flingToken = authenticationService.parseDerivedToken(token);
assertEquals(flingToken.getAuthorities().stream().findFirst().get().getAuthority(),
FlingAuthorities.FLING_ADMIN.getAuthority());
}
}

View file

@ -19,9 +19,9 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import net.friedl.fling.persistence.entities.FlingEntity;
import net.friedl.fling.persistence.repositories.FlingRepository;
import net.friedl.fling.security.authentication.FlingAdminAuthority;
import net.friedl.fling.security.authentication.FlingToken;
import net.friedl.fling.security.authentication.FlingUserAuthority;
import net.friedl.fling.security.authentication.authorities.FlingAdminAuthority;
import net.friedl.fling.security.authentication.authorities.FlingUserAuthority;
@ExtendWith(SpringExtension.class)
public class AuthorizationServiceTest {
@ -50,7 +50,7 @@ public class AuthorizationServiceTest {
@Test
public void allowUpload_flingAdmin_true() {
FlingToken flingToken = new FlingToken(new FlingAdminAuthority(), "jwtToken");
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "jwtToken");
assertTrue(authorizationService.allowUpload(UUID.randomUUID(), flingToken));
}
@ -59,7 +59,8 @@ public class AuthorizationServiceTest {
FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(false);
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken");
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
@ -71,7 +72,8 @@ public class AuthorizationServiceTest {
FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(true);
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken");
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(1, 1))).thenReturn(flingEntity);
@ -84,7 +86,8 @@ public class AuthorizationServiceTest {
FlingEntity flingEntity = new FlingEntity();
flingEntity.setAllowUpload(true);
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken");
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
@ -101,25 +104,28 @@ public class AuthorizationServiceTest {
@Test
public void allowFlingAcess_flingAdmin_true() {
FlingToken flingToken = new FlingToken(new FlingAdminAuthority(), "jwtToken");
FlingToken flingToken = new FlingToken(List.of(new FlingAdminAuthority()), "jwtToken");
assertTrue(authorizationService.allowFlingAccess(UUID.randomUUID(), flingToken));
}
@Test
public void allowFlingAcess_flingUser_notAuthorizedForId_false() {
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken");
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
assertFalse(authorizationService.allowFlingAccess(new UUID(1, 1), flingToken));
}
@Test
public void allowFlingAcess_flingUser_authorizedForId_true() {
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken");
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
assertTrue(authorizationService.allowFlingAccess(new UUID(0, 0), flingToken));
}
@Test
public void allowFlingAccessByShareId_noFlingForShareId_throw() {
FlingToken flingToken = new FlingToken(new FlingUserAuthority(new UUID(0, 0)), "jwtToken");
FlingToken flingToken =
new FlingToken(List.of(new FlingUserAuthority(new UUID(0, 0))), "jwtToken");
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
assertThrows(EntityNotFoundException.class,

View file

@ -1,114 +1,114 @@
import log from 'loglevel';
import React, {useState, useEffect, useRef} from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import {artifactClient} from '../../util/flingclient';
import { ArtifactClient, FlingClient } from '../../util/fc';
import { prettifyTimestamp } from '../../util/fn';
function FlingArtifactControl(props) {
let iframeContainer = useRef(null);
let iframeContainer = useRef(null);
const artifactClient = new ArtifactClient();
function handleDelete(ev) {
artifactClient.deleteArtifact(props.artifact.id)
.then(() => props.reloadArtifactsFn());
}
function handleDelete(ev) {
artifactClient.deleteArtifact(props.artifact.id)
.then(() => props.reloadArtifactsFn());
}
function handleDownload(ev) {
artifactClient.downloadArtifact(props.artifact.id)
.then(url => {
// We need this iframe hack because with a regular href, while
// the browser downloads the file fine, it also reloads the page, hence
// loosing all logs and state
let frame = document.createElement("iframe");
frame.src = url;
iframeContainer.current.appendChild(frame);
});
}
function handleDownload(ev) {
artifactClient.downloadArtifactWithHttpInfo(props.artifact.id)
.then(response => {
log.info(response.headers);
var blob = new Blob([response.data], {type: response.type});
if(window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, response.name);
}
else{
var elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = response.name;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
});
}
return(
<div className={`btn-group ${props.hidden ? "d-invisible": "d-visible"}`}>
<button className="btn btn-sm" onClick={handleDelete}><i className="icon icon-delete"/></button>
<button className="btn btn-sm"><i className="icon icon-edit"/></button>
<button className="btn btn-sm" onClick={handleDownload}><i className="icon icon-download"/></button>
<div className="d-hide" ref={iframeContainer}/>
</div>
);
return (
<div className={`btn-group ${props.hidden ? "d-invisible" : "d-visible"}`}>
<button className="btn btn-sm" onClick={handleDelete}><i className="icon icon-delete" /></button>
<button className="btn btn-sm"><i className="icon icon-edit" /></button>
<button className="btn btn-sm" onClick={handleDownload}><i className="icon icon-download" /></button>
<div className="d-hide" ref={iframeContainer} />
</div>
);
}
function FlingArtifactRow(props) {
let [hovered, setHovered] = useState(false);
function readableBytes(bytes) {
if(bytes <= 0) return "0 KB";
let [hovered, setHovered] = useState(false);
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
}
function localizedUploadDate() {
let d = new Date(props.artifact.uploadTime);
return d.toLocaleDateString();
}
return(
<tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
<td>{props.artifact.name}</td>
<td>{localizedUploadDate()}</td>
<td>{readableBytes(props.artifact.size)}</td>
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
</tr>
);
return (
<tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
<td>{props.artifact.path}</td>
<td>{prettifyTimestamp(props.artifact.creationTime, true)}</td>
<td></td>
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
</tr>
);
}
function FlingInfo(props) {
return(
<div className="m-2">
{ /* Add some infos about the fling */ }
</div>
);
return (
<div className="m-2">
{ /* Add some infos about the fling */}
</div>
);
}
export default function FlingArtifacts(props) {
const [artifacts, setArtifacts] = useState([]);
useEffect(getArtifacts, [props.activeFling]);
export default function FlingArtifacts() {
const flingClient = new FlingClient();
const activeFling = useSelector(state => state.flings.activeFling);
const [artifacts, setArtifacts] = useState([]);
return (
<div>
<FlingInfo />
useEffect(getArtifacts, [activeFling]);
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Uploaded</th>
<th>Size</th>
<th/>
</tr>
</thead>
<tbody>
{artifacts}
</tbody>
</table>
</div>
);
return (
<div>
<FlingInfo />
function getArtifacts() {
if (!props.activeFling) {
log.debug("No fling active. Not getting artifacts.");
return;
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Uploaded</th>
<th>Size</th>
<th />
</tr>
</thead>
<tbody>
{artifacts}
</tbody>
</table>
</div>
);
function getArtifacts() {
if (!activeFling) {
log.debug("No fling active. Not getting artifacts.");
return;
}
log.debug(`Fling ${activeFling} active. Getting artifacts.`);
let artifacts = [];
flingClient.getArtifacts(activeFling.id)
.then(result => {
log.debug(`Got ${result.length} artifacts`);
for (let artifact of result) {
artifacts.push(<FlingArtifactRow key={artifact.id} artifact={artifact} reloadArtifactsFn={getArtifacts} />);
}
log.debug(`Fling ${props.activeFling} active. Getting artifacts.`);
let artifacts = [];
artifactClient.getArtifacts(props.activeFling)
.then(result => {
log.debug(`Got ${result.length} artifacts`);
for(let artifact of result) {
artifacts.push(<FlingArtifactRow key={artifact.id} artifact={artifact} reloadArtifactsFn={getArtifacts} />);
}
setArtifacts(artifacts);
});
}
setArtifacts(artifacts);
});
}
}

View file

@ -1,39 +0,0 @@
import log from "loglevel";
import produce from "immer";
import { SET_FLINGS, SET_ACTIVE_FLING, ADD_FLING } from "../actionTypes";
const initialState = {
// type [fc.Artifact]
aritfacts: []
}
export default produce((draft, action) => {
switch (action.type) {
case SET_FLINGS:
draft.flings = action.payload;
break;
case ADD_FLING:
// Check storage again here, otherwise there could be a race
// condition due to async calls of SET_FLINGS and ADD_FLING
let foundFlingIdx = draft.flings.findIndex(fling =>
fling.id === action.payload.id);
if (foundFlingIdx === -1) {
log.debug(`Adding new fling with id ${action.payload.id}`)
draft.flings.push(action.payload);
} else {
log.debug(`Fling already exists. ` +
`Updating fling with id ${action.payload.id}`)
draft.flings[foundFlingIdx] = action.payload
}
break;
case SET_ACTIVE_FLING:
draft.activeFling = action.payload;
break;
default:
break;
}
return draft;
}, initialState);