0.1 #1
20 changed files with 483 additions and 189 deletions
|
@ -47,6 +47,11 @@ Content-Type: application/json
|
||||||
:token
|
:token
|
||||||
{"name": "Fling from querysheet with Auth and very long name", "expirationClicks": 12, "shared": true, "authCode": "abc"}
|
{"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
|
:flingId = dfc208a3-5924-43b4-aa6a-c263541dca5e
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,23 @@
|
||||||
package net.friedl.fling.controller;
|
package net.friedl.fling.controller;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.validation.annotation.Validated;
|
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.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
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.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import net.friedl.fling.model.dto.AdminAuthDto;
|
import net.friedl.fling.model.dto.AdminAuthDto;
|
||||||
import net.friedl.fling.model.dto.UserAuthDto;
|
import net.friedl.fling.model.dto.UserAuthDto;
|
||||||
|
import net.friedl.fling.security.FlingWebSecurityConfigurer;
|
||||||
import net.friedl.fling.service.AuthenticationService;
|
import net.friedl.fling.service.AuthenticationService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@ -50,4 +56,23 @@ public class AuthenticationController {
|
||||||
return authenticationService.authenticate(userAuthDto)
|
return authenticationService.authenticate(userAuthDto)
|
||||||
.orElseThrow(() -> new AccessDeniedException("Wrong username or password"));
|
.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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
public enum FlingAuthorities {
|
public enum FlingAuthorities {
|
||||||
FLING_ADMIN("admin"), FLING_USER("user");
|
FLING_ADMIN("admin"), FLING_USER("user"), FLING_TOKEN("token");
|
||||||
|
|
||||||
String authority;
|
String authority;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package net.friedl.fling.security;
|
package net.friedl.fling.security;
|
||||||
|
|
||||||
import static net.friedl.fling.security.FlingAuthorities.FLING_ADMIN;
|
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 static org.springframework.security.config.Customizer.withDefaults;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -18,8 +19,8 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.friedl.fling.FlingSecurityConfiguration;
|
import net.friedl.fling.security.authentication.filter.BearerAuthenticationFilter;
|
||||||
import net.friedl.fling.security.authentication.JwtAuthenticationFilter;
|
import net.friedl.fling.security.authentication.filter.TokenAuthenticationFilter;
|
||||||
import net.friedl.fling.service.AuthorizationService;
|
import net.friedl.fling.service.AuthorizationService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -30,15 +31,18 @@ import net.friedl.fling.service.AuthorizationService;
|
||||||
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
private List<String> allowedOrigins;
|
private List<String> allowedOrigins;
|
||||||
|
|
||||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
private TokenAuthenticationFilter tokenAuthenticationFilter;
|
||||||
|
private BearerAuthenticationFilter bearerAuthenticationFilter;
|
||||||
private AuthorizationService authorizationService;
|
private AuthorizationService authorizationService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FlingWebSecurityConfigurer(JwtAuthenticationFilter jwtAuthenticationFilter,
|
public FlingWebSecurityConfigurer(
|
||||||
AuthorizationService authorizationService,
|
TokenAuthenticationFilter tokenAuthenticationFilter,
|
||||||
FlingSecurityConfiguration securityConfiguraiton) {
|
BearerAuthenticationFilter bearerAuthenticationFilter,
|
||||||
|
AuthorizationService authorizationService) {
|
||||||
|
|
||||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
this.tokenAuthenticationFilter = tokenAuthenticationFilter;
|
||||||
|
this.bearerAuthenticationFilter = bearerAuthenticationFilter;
|
||||||
this.authorizationService = authorizationService;
|
this.authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +56,8 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
/**********************************************/
|
/**********************************************/
|
||||||
/** Authentication Interceptor Configuration **/
|
/** 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
|
// 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.
|
// in that it is possible to authenticate without a bearer token if the session is kept.
|
||||||
// Turn off this confusing and non-obvious behavior.
|
// Turn off this confusing and non-obvious behavior.
|
||||||
|
@ -68,6 +73,10 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||||||
/**********************************/
|
/**********************************/
|
||||||
/** Authorization for: /api/auth **/
|
/** Authorization for: /api/auth **/
|
||||||
/**********************************/
|
/**********************************/
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers("/api/auth/derive")
|
||||||
|
.hasAnyAuthority(FLING_ADMIN.getAuthority(), FLING_USER.getAuthority())
|
||||||
|
.and()
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers("/api/auth/**")
|
.antMatchers("/api/auth/**")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
|
|
|
@ -4,25 +4,31 @@ import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
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 {
|
public class FlingToken extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
private static final long serialVersionUID = -1112423505610346583L;
|
private static final long serialVersionUID = -1112423505610346583L;
|
||||||
private String jwtToken;
|
private String token;
|
||||||
|
|
||||||
public FlingToken(GrantedAuthority authority, String jwtToken) {
|
public FlingToken(List<GrantedAuthority> authorities, String token) {
|
||||||
super(List.of(authority));
|
super(authorities);
|
||||||
this.jwtToken = jwtToken;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean authorizedForFling(UUID id) {
|
public boolean authorizedForFling(UUID id) {
|
||||||
for (GrantedAuthority grantedAuthority : getAuthorities()) {
|
for (GrantedAuthority grantedAuthority : getAuthorities()) {
|
||||||
if (grantedAuthority instanceof FlingAdminAuthority) return true;
|
if (grantedAuthority instanceof FlingAdminAuthority) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(grantedAuthority instanceof FlingUserAuthority)) continue;
|
if (grantedAuthority instanceof FlingUserAuthority) {
|
||||||
|
UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId();
|
||||||
UUID grantedFlingId = ((FlingUserAuthority) grantedAuthority).getFlingId();
|
if (grantedFlingId.equals(id)) {
|
||||||
if (grantedFlingId.equals(id)) return true;
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -30,7 +36,7 @@ public class FlingToken extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCredentials() {
|
public String getCredentials() {
|
||||||
return this.jwtToken;
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package net.friedl.fling.security.authentication;
|
package net.friedl.fling.security.authentication.authorities;
|
||||||
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import net.friedl.fling.security.FlingAuthorities;
|
import net.friedl.fling.security.FlingAuthorities;
|
|
@ -1,4 +1,4 @@
|
||||||
package net.friedl.fling.security.authentication;
|
package net.friedl.fling.security.authentication.authorities;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
|
@ -1,4 +1,4 @@
|
||||||
package net.friedl.fling.security.authentication;
|
package net.friedl.fling.security.authentication.filter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -13,18 +13,19 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.friedl.fling.security.authentication.FlingToken;
|
||||||
import net.friedl.fling.service.AuthenticationService;
|
import net.friedl.fling.service.AuthenticationService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class BearerAuthenticationFilter extends OncePerRequestFilter {
|
||||||
private static final String TOKEN_PREFIX = "Bearer ";
|
private static final String TOKEN_PREFIX = "Bearer ";
|
||||||
private static final String HEADER_STRING = "Authorization";
|
private static final String HEADER_STRING = "Authorization";
|
||||||
|
|
||||||
private AuthenticationService authenticationService;
|
private AuthenticationService authenticationService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public JwtAuthenticationFilter(AuthenticationService authenticationService) {
|
public BearerAuthenticationFilter(AuthenticationService authenticationService) {
|
||||||
this.authenticationService = authenticationService;
|
this.authenticationService = authenticationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
|
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
|
||||||
log.info("Anonymous request for {} {}{}", request.getMethod(), request.getRequestURL(),
|
log.info("Anonymous request for {} {}{}", request.getMethod(), request.getRequestURL(),
|
||||||
request.getQueryString() != null ? "?"+request.getQueryString(): "");
|
request.getQueryString() != null ? "?" + request.getQueryString() : "");
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -48,8 +49,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
if (securityContext.getAuthentication() == null) {
|
if (securityContext.getAuthentication() == null) {
|
||||||
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
|
log.info("Authenticating request for {} {}{}", request.getMethod(), request.getRequestURL(),
|
||||||
request.getQueryString() != null ? "?"+request.getQueryString(): "");
|
request.getQueryString() != null ? "?" + request.getQueryString() : "");
|
||||||
FlingToken token = authenticationService.parseAuthentication(authToken);
|
FlingToken token = authenticationService.parseJwtAuthentication(authToken);
|
||||||
log.info("Authenticated as {}", token.getAuthorities().stream()
|
log.info("Authenticated as {}", token.getAuthorities().stream()
|
||||||
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
|
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")));
|
||||||
securityContext.setAuthentication(token);
|
securityContext.setAuthentication(token);
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,12 +3,16 @@ package net.friedl.fling.service;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.persistence.EntityNotFoundException;
|
import javax.persistence.EntityNotFoundException;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
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.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import io.jsonwebtoken.Claims;
|
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.AdminAuthDto;
|
||||||
import net.friedl.fling.model.dto.UserAuthDto;
|
import net.friedl.fling.model.dto.UserAuthDto;
|
||||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
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.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.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
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@ -30,6 +36,7 @@ public class AuthenticationService {
|
||||||
private JwtParser jwtParser;
|
private JwtParser jwtParser;
|
||||||
private Key jwtSigningKey;
|
private Key jwtSigningKey;
|
||||||
private FlingRepository flingRepository;
|
private FlingRepository flingRepository;
|
||||||
|
private TokenRepository tokenRepository;
|
||||||
private PasswordEncoder passwordEncoder;
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Value("${fling.security.admin-name}")
|
@Value("${fling.security.admin-name}")
|
||||||
|
@ -41,12 +48,14 @@ public class AuthenticationService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public AuthenticationService(JwtParser jwtParser, Key jwtSigningKey,
|
public AuthenticationService(JwtParser jwtParser, Key jwtSigningKey,
|
||||||
PasswordEncoder passwordEncoder, FlingRepository flingRepository) {
|
PasswordEncoder passwordEncoder, FlingRepository flingRepository,
|
||||||
|
TokenRepository tokenRepository) {
|
||||||
|
|
||||||
this.jwtParser = jwtParser;
|
this.jwtParser = jwtParser;
|
||||||
this.jwtSigningKey = jwtSigningKey;
|
this.jwtSigningKey = jwtSigningKey;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.flingRepository = flingRepository;
|
this.flingRepository = flingRepository;
|
||||||
|
this.tokenRepository = tokenRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<String> authenticate(AdminAuthDto adminAuth) {
|
public Optional<String> authenticate(AdminAuthDto adminAuth) {
|
||||||
|
@ -71,7 +80,9 @@ public class AuthenticationService {
|
||||||
public Optional<String> authenticate(UserAuthDto userAuth) {
|
public Optional<String> authenticate(UserAuthDto userAuth) {
|
||||||
log.info("Authenticating for fling [.shareId={}]", userAuth.getShareId());
|
log.info("Authenticating for fling [.shareId={}]", userAuth.getShareId());
|
||||||
FlingEntity flingEntity = flingRepository.findByShareId(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 providedAuthCodeHash = passwordEncoder.encode(userAuth.getAuthCode());
|
||||||
String actualAuthCodeHash = flingEntity.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();
|
Claims claims = jwtParser.parseClaimsJws(token).getBody();
|
||||||
|
|
||||||
switch (claims.getSubject()) {
|
switch (claims.getSubject()) {
|
||||||
case "admin":
|
case "admin":
|
||||||
return new FlingToken(new FlingAdminAuthority(), token);
|
return new FlingToken(List.of(new FlingAdminAuthority()), token);
|
||||||
case "user":
|
case "user":
|
||||||
UUID grantedFlingId = UUID.fromString(claims.get("id", String.class));
|
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:
|
default:
|
||||||
throw new BadCredentialsException("Invalid token");
|
throw new BadCredentialsException("Invalid token");
|
||||||
}
|
}
|
||||||
|
@ -116,4 +127,47 @@ public class AuthenticationService {
|
||||||
.setExpiration(Date.from(Instant.now().plusSeconds(jwtExpiration)))
|
.setExpiration(Date.from(Instant.now().plusSeconds(jwtExpiration)))
|
||||||
.signWith(jwtSigningKey);
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,9 @@ public class AuthorizationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
FlingEntity flingEntity = flingRepository.findByShareId(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 allowFlingAccess(flingEntity.getId(), token);
|
return allowFlingAccess(flingEntity.getId(), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,9 @@ public class FlingService {
|
||||||
|
|
||||||
public FlingDto getByShareId(String shareId) {
|
public FlingDto getByShareId(String shareId) {
|
||||||
FlingEntity flingEntity = flingRepository.findByShareId(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);
|
return flingMapper.map(flingEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,4 +84,5 @@ public class AuthenticationControllerTest {
|
||||||
.andExpect(status().is(200))
|
.andExpect(status().is(200))
|
||||||
.andExpect(content().string("token"));
|
.andExpect(content().string("token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,33 @@
|
||||||
package net.friedl.fling.service;
|
package net.friedl.fling.service;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.emptyOrNullString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.hasItem;
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.not;
|
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.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
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 static org.mockito.Mockito.when;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.persistence.EntityNotFoundException;
|
import javax.persistence.EntityNotFoundException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.TestConfiguration;
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
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.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
import org.springframework.test.context.TestPropertySource;
|
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.AdminAuthDto;
|
||||||
import net.friedl.fling.model.dto.UserAuthDto;
|
import net.friedl.fling.model.dto.UserAuthDto;
|
||||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
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.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.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)
|
@ExtendWith(SpringExtension.class)
|
||||||
@TestPropertySource("classpath:/application-test.properties")
|
@TestPropertySource("classpath:/application-test.properties")
|
||||||
|
@ -48,6 +60,9 @@ public class AuthenticationServiceTest {
|
||||||
@MockBean
|
@MockBean
|
||||||
private FlingRepository flingRepository;
|
private FlingRepository flingRepository;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private TokenRepository tokenRepository;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
private JwtParser jwtParser;
|
private JwtParser jwtParser;
|
||||||
|
|
||||||
|
@ -60,8 +75,11 @@ public class AuthenticationServiceTest {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuthenticationService authenticationService(JwtParser jwtParser,
|
public AuthenticationService authenticationService(JwtParser jwtParser,
|
||||||
PasswordEncoder passwordEncoder, FlingRepository flingRepository) {
|
PasswordEncoder passwordEncoder, FlingRepository flingRepository,
|
||||||
return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository);
|
TokenRepository tokenRepository) {
|
||||||
|
|
||||||
|
return new AuthenticationService(jwtParser, jwtSigningKey, passwordEncoder, flingRepository,
|
||||||
|
tokenRepository);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,23 +136,24 @@ public class AuthenticationServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticate_noFlingForShareId_throws() {
|
public void authenticate_noFlingForShareId_throws() {
|
||||||
UserAuthDto userAuthDto = UserAuthDto.builder()
|
UserAuthDto userAuthDto = UserAuthDto.builder()
|
||||||
.authCode("authCode")
|
.authCode("authCode")
|
||||||
.shareId("doesNotExist").build();
|
.shareId("doesNotExist").build();
|
||||||
|
|
||||||
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
|
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
|
||||||
when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash");
|
when(passwordEncoder.encode(any(String.class))).thenReturn("authCodeHash");
|
||||||
|
|
||||||
assertThrows(EntityNotFoundException.class, () -> authenticationService.authenticate(userAuthDto));
|
assertThrows(EntityNotFoundException.class,
|
||||||
|
() -> authenticationService.authenticate(userAuthDto));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void parseAuthentication_owner_AdminAuthority() {
|
public void parseJwtAuthentication_owner_AdminAuthority() {
|
||||||
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
new DefaultClaims(Map.of("sub", "admin")), "signature");
|
new DefaultClaims(Map.of("sub", "admin")), "signature");
|
||||||
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
|
||||||
FlingToken flingToken = authenticationService.parseAuthentication("any");
|
FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
|
||||||
assertThat(flingToken.isAuthenticated(), equalTo(true));
|
assertThat(flingToken.isAuthenticated(), equalTo(true));
|
||||||
// authorized for any fling
|
// authorized for any fling
|
||||||
assertThat(flingToken.authorizedForFling(UUID.randomUUID()), equalTo(true));
|
assertThat(flingToken.authorizedForFling(UUID.randomUUID()), equalTo(true));
|
||||||
|
@ -144,12 +163,12 @@ public class AuthenticationServiceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void parseAuthentication_user_UserAuthorityForId() {
|
public void parseJwtAuthentication_user_UserAuthorityForId() {
|
||||||
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
new DefaultClaims(Map.of("sub", "user", "id", new UUID(0, 0).toString())), "signature");
|
new DefaultClaims(Map.of("sub", "user", "id", new UUID(0, 0).toString())), "signature");
|
||||||
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
|
||||||
FlingToken flingToken = authenticationService.parseAuthentication("any");
|
FlingToken flingToken = authenticationService.parseJwtAuthentication("any");
|
||||||
assertThat(flingToken.isAuthenticated(), equalTo(true));
|
assertThat(flingToken.isAuthenticated(), equalTo(true));
|
||||||
// authorized for fling in token
|
// authorized for fling in token
|
||||||
assertThat(flingToken.authorizedForFling(new UUID(0, 0)), equalTo(true));
|
assertThat(flingToken.authorizedForFling(new UUID(0, 0)), equalTo(true));
|
||||||
|
@ -161,12 +180,110 @@ public class AuthenticationServiceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void parseAuthentication_unknownSubject_throws() {
|
public void parseJwtAuthentication_unknownSubject_throws() {
|
||||||
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
Jws<Claims> jwsClaims = new DefaultJws<>(new DefaultJwsHeader(),
|
||||||
new DefaultClaims(Map.of("sub", "unknownSubject")), "signature");
|
new DefaultClaims(Map.of("sub", "unknownSubject")), "signature");
|
||||||
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
when(jwtParser.parseClaimsJws(any(String.class))).thenReturn(jwsClaims);
|
||||||
|
|
||||||
assertThrows(BadCredentialsException.class,
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,9 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
import net.friedl.fling.persistence.entities.FlingEntity;
|
import net.friedl.fling.persistence.entities.FlingEntity;
|
||||||
import net.friedl.fling.persistence.repositories.FlingRepository;
|
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.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)
|
@ExtendWith(SpringExtension.class)
|
||||||
public class AuthorizationServiceTest {
|
public class AuthorizationServiceTest {
|
||||||
|
@ -50,7 +50,7 @@ public class AuthorizationServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void allowUpload_flingAdmin_true() {
|
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));
|
assertTrue(authorizationService.allowUpload(UUID.randomUUID(), flingToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,8 @@ public class AuthorizationServiceTest {
|
||||||
FlingEntity flingEntity = new FlingEntity();
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
flingEntity.setAllowUpload(false);
|
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);
|
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
@ -71,7 +72,8 @@ public class AuthorizationServiceTest {
|
||||||
FlingEntity flingEntity = new FlingEntity();
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
flingEntity.setAllowUpload(true);
|
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);
|
when(flingRepository.getOne(new UUID(1, 1))).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
@ -84,7 +86,8 @@ public class AuthorizationServiceTest {
|
||||||
FlingEntity flingEntity = new FlingEntity();
|
FlingEntity flingEntity = new FlingEntity();
|
||||||
flingEntity.setAllowUpload(true);
|
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);
|
when(flingRepository.getOne(new UUID(0, 0))).thenReturn(flingEntity);
|
||||||
|
|
||||||
|
@ -101,25 +104,28 @@ public class AuthorizationServiceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void allowFlingAcess_flingAdmin_true() {
|
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));
|
assertTrue(authorizationService.allowFlingAccess(UUID.randomUUID(), flingToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void allowFlingAcess_flingUser_notAuthorizedForId_false() {
|
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));
|
assertFalse(authorizationService.allowFlingAccess(new UUID(1, 1), flingToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void allowFlingAcess_flingUser_authorizedForId_true() {
|
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));
|
assertTrue(authorizationService.allowFlingAccess(new UUID(0, 0), flingToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void allowFlingAccessByShareId_noFlingForShareId_throw() {
|
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);
|
when(flingRepository.findByShareId(any(String.class))).thenReturn(null);
|
||||||
|
|
||||||
assertThrows(EntityNotFoundException.class,
|
assertThrows(EntityNotFoundException.class,
|
||||||
|
|
|
@ -1,114 +1,114 @@
|
||||||
import log from 'loglevel';
|
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) {
|
function FlingArtifactControl(props) {
|
||||||
let iframeContainer = useRef(null);
|
let iframeContainer = useRef(null);
|
||||||
|
const artifactClient = new ArtifactClient();
|
||||||
|
|
||||||
function handleDelete(ev) {
|
function handleDelete(ev) {
|
||||||
artifactClient.deleteArtifact(props.artifact.id)
|
artifactClient.deleteArtifact(props.artifact.id)
|
||||||
.then(() => props.reloadArtifactsFn());
|
.then(() => props.reloadArtifactsFn());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleDownload(ev) {
|
function handleDownload(ev) {
|
||||||
artifactClient.downloadArtifact(props.artifact.id)
|
artifactClient.downloadArtifactWithHttpInfo(props.artifact.id)
|
||||||
.then(url => {
|
.then(response => {
|
||||||
// We need this iframe hack because with a regular href, while
|
log.info(response.headers);
|
||||||
// the browser downloads the file fine, it also reloads the page, hence
|
var blob = new Blob([response.data], {type: response.type});
|
||||||
// loosing all logs and state
|
if(window.navigator.msSaveOrOpenBlob) {
|
||||||
let frame = document.createElement("iframe");
|
window.navigator.msSaveBlob(blob, response.name);
|
||||||
frame.src = url;
|
}
|
||||||
iframeContainer.current.appendChild(frame);
|
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(
|
return (
|
||||||
<div className={`btn-group ${props.hidden ? "d-invisible": "d-visible"}`}>
|
<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" onClick={handleDelete}><i className="icon icon-delete" /></button>
|
||||||
<button className="btn btn-sm"><i className="icon icon-edit"/></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>
|
<button className="btn btn-sm" onClick={handleDownload}><i className="icon icon-download" /></button>
|
||||||
<div className="d-hide" ref={iframeContainer}/>
|
<div className="d-hide" ref={iframeContainer} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FlingArtifactRow(props) {
|
function FlingArtifactRow(props) {
|
||||||
let [hovered, setHovered] = useState(false);
|
let [hovered, setHovered] = useState(false);
|
||||||
function readableBytes(bytes) {
|
|
||||||
if(bytes <= 0) return "0 KB";
|
|
||||||
|
|
||||||
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
return (
|
||||||
sizes = ['Byte', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
<tr key={props.artifact.id} className="artifact-row" onMouseOver={() => setHovered(true)} onMouseOut={() => setHovered(false)}>
|
||||||
|
<td>{props.artifact.path}</td>
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
|
<td>{prettifyTimestamp(props.artifact.creationTime, true)}</td>
|
||||||
}
|
<td></td>
|
||||||
|
<td><FlingArtifactControl artifact={props.artifact} reloadArtifactsFn={props.reloadArtifactsFn} hidden={!hovered} /></td>
|
||||||
function localizedUploadDate() {
|
</tr>
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FlingInfo(props) {
|
function FlingInfo(props) {
|
||||||
return(
|
return (
|
||||||
<div className="m-2">
|
<div className="m-2">
|
||||||
{ /* Add some infos about the fling */ }
|
{ /* Add some infos about the fling */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FlingArtifacts(props) {
|
export default function FlingArtifacts() {
|
||||||
const [artifacts, setArtifacts] = useState([]);
|
const flingClient = new FlingClient();
|
||||||
useEffect(getArtifacts, [props.activeFling]);
|
const activeFling = useSelector(state => state.flings.activeFling);
|
||||||
|
const [artifacts, setArtifacts] = useState([]);
|
||||||
|
|
||||||
return (
|
useEffect(getArtifacts, [activeFling]);
|
||||||
<div>
|
|
||||||
<FlingInfo />
|
|
||||||
|
|
||||||
<table className="table">
|
return (
|
||||||
<thead>
|
<div>
|
||||||
<tr>
|
<FlingInfo />
|
||||||
<th>Name</th>
|
|
||||||
<th>Uploaded</th>
|
|
||||||
<th>Size</th>
|
|
||||||
<th/>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{artifacts}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
function getArtifacts() {
|
<table className="table">
|
||||||
if (!props.activeFling) {
|
<thead>
|
||||||
log.debug("No fling active. Not getting artifacts.");
|
<tr>
|
||||||
return;
|
<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.`);
|
setArtifacts(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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
Loading…
Reference in a new issue