Skip to content
Snippets Groups Projects
Commit 746ce868 authored by Martin Weise's avatar Martin Weise
Browse files

Merge branch '233-frontend-errors-and-authorized-dev-keys-3' into dev

parents 678327d3 cd83e4ff
Branches
Tags
4 merge requests!129New module for citation as they occur multiple,!121Modified logging, modified logging level, modified flasgger endpoint,!113Resolve "Bugs related with Query Service",!111Resolve "Frontend errors and authorized dev keys"
Showing
with 261 additions and 52 deletions
......@@ -84,16 +84,17 @@ public class TokenEndpoint {
.body(dto);
}
@DeleteMapping("/{hash}")
@DeleteMapping("/{id}")
@Transactional
@Timed(value = "token.delete", description = "Time needed to delete the developer tokens")
@Operation(summary = "Delete developer token", security = @SecurityRequirement(name = "bearerAuth"))
public void delete(@NotNull @PathVariable("hash") String hash,
public void delete(@NotNull @PathVariable("id") Long id,
@NotNull Principal principal) throws TokenNotFoundException, UserNotFoundException {
log.debug("endpoint delete developer token, hash={}, principal={}", hash, principal);
final Token token = tokenService.findOne(hash);
log.debug("endpoint delete developer token, id={}, principal={}", id, principal);
final Token token = tokenService.findOne(id);
log.trace("found token {}", token);
tokenService.delete(token.getTokenHash(), principal);
log.info("Deleted token with id {}", id);
}
}
\ No newline at end of file
package at.tuwien;
import at.tuwien.api.auth.SignupRequestDto;
import at.tuwien.api.user.UserDetailsDto;
import at.tuwien.entities.user.RoleType;
import at.tuwien.entities.user.TimeSecret;
import at.tuwien.entities.user.Token;
import at.tuwien.entities.user.User;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.test.context.TestPropertySource;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
@TestPropertySource(locations = "classpath:application.properties")
public abstract class BaseUnitTest {
......@@ -38,9 +44,21 @@ public abstract class BaseUnitTest {
.emailVerified(USER_1_VERIFIED)
.themeDark(USER_1_THEME_DARK)
.created(USER_1_CREATED)
.roles(List.of(RoleType.ROLE_RESEARCHER))
.lastModified(USER_1_LAST_MODIFIED)
.build();
public final static UserDetails USER_1_DETAILS = UserDetailsDto.builder()
.username(USER_1_USERNAME)
.email(USER_1_EMAIL)
.password(USER_1_PASSWORD)
.authorities(List.of(new SimpleGrantedAuthority("ROLE_RESEARCHER")))
.build();
public final static Principal USER_1_PRINCIPAL = new UsernamePasswordAuthenticationToken(USER_1_DETAILS,
USER_1_PASSWORD, USER_1_DETAILS.getAuthorities());
public final static Long USER_2_ID = 2L;
public final static String USER_2_EMAIL = "jane.doe@example.com";
public final static String USER_2_USERNAME = "jdoe2";
......@@ -59,37 +77,56 @@ public abstract class BaseUnitTest {
.emailVerified(USER_2_VERIFIED)
.themeDark(USER_2_THEME_DARK)
.created(USER_2_CREATED)
.roles(List.of(RoleType.ROLE_RESEARCHER))
.lastModified(USER_2_LAST_MODIFIED)
.build();
public final static Long TOKEN_1_ID = 1L;
public final static Boolean TOKEN_1_PROCESSED = false;
public final static String TOKEN_1_TOKEN = "mysecrettokenrandomlygenerated";
public final static Instant TOKEN_1_VALID_TO = Instant.now()
public final static UserDetails USER_2_DETAILS = UserDetailsDto.builder()
.username(USER_2_USERNAME)
.email(USER_2_EMAIL)
.password(USER_2_PASSWORD)
.authorities(List.of(new SimpleGrantedAuthority("ROLE_RESEARCHER")))
.build();
public final static Principal USER_2_PRINCIPAL = new UsernamePasswordAuthenticationToken(USER_2_DETAILS,
USER_2_PASSWORD, USER_2_DETAILS.getAuthorities());
public final static Long TIME_SECRET_1_ID = 1L;
public final static Boolean TIME_SECRET_1_PROCESSED = false;
public final static String TIME_SECRET_1_TOKEN = "mysecrettokenrandomlygenerated";
public final static Instant TIME_SECRET_1_VALID_TO = Instant.now()
.plus(1, ChronoUnit.DAYS);
public final static Long TOKEN_2_ID = 2L;
public final static Boolean TOKEN_2_PROCESSED = true;
public final static String TOKEN_2_TOKEN = "blahblahblah";
public final static Instant TOKEN_2_VALID_TO = Instant.now()
public final static Long TIME_SECRET_2_ID = 2L;
public final static Boolean TIME_SECRET_2_PROCESSED = true;
public final static String TIME_SECRET_2_TOKEN = "blahblahblah";
public final static Instant TIME_SECRET_2_VALID_TO = Instant.now()
.plus(1, ChronoUnit.DAYS);
public final static TimeSecret TOKEN_1 = TimeSecret.builder()
.id(TOKEN_1_ID)
public final static TimeSecret TIME_SECRET_1 = TimeSecret.builder()
.id(TIME_SECRET_1_ID)
.uid(USER_1_ID)
.user(USER_1)
.token(TOKEN_1_TOKEN)
.processed(TOKEN_1_PROCESSED)
.validTo(TOKEN_1_VALID_TO)
.token(TIME_SECRET_1_TOKEN)
.processed(TIME_SECRET_1_PROCESSED)
.validTo(TIME_SECRET_1_VALID_TO)
.build();
public final static TimeSecret TOKEN_2 = TimeSecret.builder()
.id(TOKEN_2_ID)
public final static TimeSecret TIME_SECRET_2 = TimeSecret.builder()
.id(TIME_SECRET_2_ID)
.uid(USER_2_ID)
.user(USER_2)
.token(TOKEN_2_TOKEN)
.processed(TOKEN_2_PROCESSED)
.validTo(TOKEN_2_VALID_TO)
.token(TIME_SECRET_2_TOKEN)
.processed(TIME_SECRET_2_PROCESSED)
.validTo(TIME_SECRET_2_VALID_TO)
.build();
public final static Long TOKEN_1_ID = 1L;
public final static Instant TOKEN_1_EXPIRES = Instant.now().plus(100000000, ChronoUnit.MILLIS);
public final static Token TOKEN_1 = Token.builder()
.id(TOKEN_1_ID)
.expires(TOKEN_1_EXPIRES)
.build();
}
......@@ -41,10 +41,10 @@ public class AuthenticationServiceIntegrationTest extends BaseUnitTest {
public void beforeEach() {
final User u1 = userRepository.save(USER_1);
final User u2 = userRepository.save(USER_2);
TOKEN_1.setUser(u1);
tokenRepository.save(TOKEN_1);
TOKEN_2.setUser(u2);
tokenRepository.save(TOKEN_2);
TIME_SECRET_1.setUser(u1);
tokenRepository.save(TIME_SECRET_1);
TIME_SECRET_2.setUser(u2);
tokenRepository.save(TIME_SECRET_2);
}
@Test
......
......@@ -20,31 +20,29 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class TokenServiceIntegrationTest extends BaseUnitTest {
public class TimeSecretUnitTest extends BaseUnitTest {
@MockBean
private ReadyConfig readyConfig;
@Autowired
private TimeSecretService tokenService;
private TimeSecretService timeSecretService;
@Autowired
private TimeSecretRepository tokenRepository;
private TimeSecretRepository timeSecretRepository;
@BeforeEach
public void beforeEach() {
tokenRepository.save(TOKEN_1);
timeSecretRepository.save(TIME_SECRET_1);
}
@Test
public void updateVerification_succeeds() throws SecretInvalidException {
/* mock */
/* test */
tokenService.invalidate(TOKEN_1_TOKEN);
timeSecretService.invalidate(TIME_SECRET_1_TOKEN);
assertThrows(SecretInvalidException.class, () -> {
tokenService.invalidate(TOKEN_1_TOKEN);
timeSecretService.invalidate(TIME_SECRET_1_TOKEN);
});
}
......
package at.tuwien.service;
import at.tuwien.BaseUnitTest;
import at.tuwien.auth.JwtUtils;
import at.tuwien.config.ReadyConfig;
import at.tuwien.entities.user.Token;
import at.tuwien.exception.TokenNotEligableException;
import at.tuwien.exception.TokenNotFoundException;
import at.tuwien.exception.UserNotFoundException;
import at.tuwien.repositories.TokenRepository;
import at.tuwien.repositories.UserRepository;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.servlet.ServletException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import static org.junit.jupiter.api.Assertions.assertThrows;
@Log4j2
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class TokenIntegrationTest extends BaseUnitTest {
@MockBean
private ReadyConfig readyConfig;
@Autowired
private TokenService tokenService;
@Autowired
private UserRepository userRepository;
@Autowired
private TokenRepository tokenRepository;
@Autowired
private JwtUtils jwtUtils;
@BeforeEach
public void beforeEach() {
userRepository.save(USER_1);
}
@Test
public void check_succeeds() throws ServletException {
/* mock */
final String jwt = jwtUtils.generateJwtToken(USER_1_USERNAME, TOKEN_1_EXPIRES);
final Token entity = Token.builder()
.token(jwt)
.tokenHash(JwtUtils.toHash(jwt))
.creator(USER_1_ID)
.expires(TOKEN_1_EXPIRES)
.build();
final Token token = tokenRepository.save(entity);
/* test */
tokenService.check(jwt);
}
@Test
public void check_revoked_fails() {
/* mock */
final String jwt = jwtUtils.generateJwtToken(USER_1_USERNAME, TOKEN_1_EXPIRES);
final Token entity = Token.builder()
.token(jwt)
.tokenHash(JwtUtils.toHash(jwt))
.creator(USER_1_ID)
.expires(TOKEN_1_EXPIRES)
.deleted(Instant.now().minus(1, ChronoUnit.SECONDS))
.build();
final Token token = tokenRepository.save(entity);
/* test */
assertThrows(ServletException.class, () -> {
tokenService.check(jwt);
});
}
@Test
public void create_userNotFound_fails() {
/* test */
assertThrows(UserNotFoundException.class, () -> {
tokenService.create(USER_2_PRINCIPAL);
});
}
}
package at.tuwien.auth;
import at.tuwien.service.TokenService;
import at.tuwien.service.impl.UserDetailsServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
......@@ -20,10 +21,12 @@ import java.io.IOException;
public class AuthTokenFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final TokenService tokenService;
private final UserDetailsServiceImpl userDetailsService;
public AuthTokenFilter(JwtUtils jwtUtils, UserDetailsServiceImpl userDetailsService) {
public AuthTokenFilter(JwtUtils jwtUtils, TokenService tokenService, UserDetailsServiceImpl userDetailsService) {
this.jwtUtils = jwtUtils;
this.tokenService = tokenService;
this.userDetailsService = userDetailsService;
}
......@@ -32,6 +35,7 @@ public class AuthTokenFilter extends OncePerRequestFilter {
throws ServletException, IOException {
final String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
tokenService.check(jwt);
final String username = jwtUtils.getUserNameFromJwtToken(jwt);
final UserDetails userDetails;
try {
......
......@@ -5,6 +5,7 @@ import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
......@@ -40,6 +41,10 @@ public class JwtUtils {
.getSubject();
}
public static String toHash(String token) {
return DigestUtils.sha256Hex(token);
}
public boolean validateJwtToken(String authToken) {
try {
final DecodedJWT jwt = JWT.decode(authToken);
......
......@@ -2,6 +2,7 @@ package at.tuwien.config;
import at.tuwien.auth.AuthTokenFilter;
import at.tuwien.auth.JwtUtils;
import at.tuwien.service.TokenService;
import at.tuwien.service.impl.UserDetailsServiceImpl;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
......@@ -35,20 +36,22 @@ import javax.servlet.http.HttpServletResponse;
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtUtils jwtUtils;
private final TokenService tokenService;
private final SecurityConfig securityConfig;
private final UserDetailsServiceImpl userDetailsService;
@Autowired
public WebSecurityConfig(JwtUtils jwtUtils, SecurityConfig securityConfig,
public WebSecurityConfig(JwtUtils jwtUtils, TokenService tokenService, SecurityConfig securityConfig,
UserDetailsServiceImpl userDetailsService) {
this.jwtUtils = jwtUtils;
this.tokenService = tokenService;
this.securityConfig = securityConfig;
this.userDetailsService = userDetailsService;
}
@Bean
public AuthTokenFilter authTokenFilter() {
return new AuthTokenFilter(jwtUtils, userDetailsService);
return new AuthTokenFilter(jwtUtils, tokenService, userDetailsService);
}
@Override
......
......@@ -5,7 +5,9 @@ import at.tuwien.exception.TokenNotEligableException;
import at.tuwien.exception.TokenNotFoundException;
import at.tuwien.exception.UserNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.ServletException;
import java.security.Principal;
import java.util.List;
......@@ -40,6 +42,15 @@ public interface TokenService {
*/
Token findOne(String tokenHash) throws TokenNotFoundException;
/**
* Finds a token by id.
*
* @param id The token id.
* @return The token, if successful.
* @throws TokenNotFoundException The token was not found in the metadata database.
*/
Token findOne(Long id) throws TokenNotFoundException;
/**
* Deletes a developer token in the metadata database by hash and user principal.
*
......@@ -49,4 +60,11 @@ public interface TokenService {
* @throws UserNotFoundException The user does not exist in the metadata database.
*/
void delete(String tokenHash, Principal principal) throws TokenNotFoundException, UserNotFoundException;
/**
* Checks if the developer token has not been marked as deleted
*
* @param jwt The token
*/
void check(String jwt) throws ServletException;
}
......@@ -15,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.ServletException;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
......@@ -80,6 +81,17 @@ public class TokenServiceImpl implements TokenService {
return optional.get();
}
@Override
@Transactional(readOnly = true)
public Token findOne(Long id) throws TokenNotFoundException {
final Optional<Token> optional = tokenRepository.findById(id);
if (optional.isEmpty()) {
log.error("Failed to find token with id {}", id);
throw new TokenNotFoundException("Failed to find token");
}
return optional.get();
}
@Override
@Transactional
public void delete(String tokenHash, Principal principal) throws TokenNotFoundException, UserNotFoundException {
......@@ -94,4 +106,20 @@ public class TokenServiceImpl implements TokenService {
log.debug("deleted token {}", token);
}
@Override
@Transactional
public void check(String jwt) throws ServletException {
final Optional<Token> optional = tokenRepository.findByTokenHash(JwtUtils.toHash(jwt));
if (optional.isEmpty()) {
return;
}
final Token token = optional.get();
if (token.getDeleted() != null) {
log.error("Token was marked as deleted on {}", token.getDeleted());
throw new ServletException("Token was marked as deleted");
}
token.setLastUsed(Instant.now());
tokenRepository.save(token);
}
}
......@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
......@@ -51,10 +52,10 @@ public class Token {
@Column(nullable = false, updatable = false)
private Instant expires;
@Column(nullable = false, updatable = false)
@Column
private Instant lastUsed;
@Column(nullable = false, updatable = false)
@Column(updatable = false)
private Instant deleted;
}
......@@ -263,7 +263,7 @@ export default {
}
},
login () {
let redirect = ![undefined ,'/', '/login'].includes(this.$router.currentRoute.path)
const redirect = ![undefined, '/', '/login'].includes(this.$router.currentRoute.path)
this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {} })
},
navigate (item) {
......
......@@ -77,7 +77,8 @@ export default {
sharedFilesystem: process.env.SHARED_FILESYSTEM || '/tmp',
version: process.env.VERSION || 'latest',
logo: process.env.LOGO || '/logo.png',
mailVerify: process.env.MAIL_VERIFY || false
mailVerify: process.env.MAIL_VERIFY || false,
tokenMax: process.env.TOKEN_MAX || 5
},
proxy: {
......
......@@ -9,9 +9,10 @@
<v-card-text>
<v-list-item v-for="(item, i) in tokens" :key="i" three-line>
<v-list-item-content>
<v-list-item-title>sha256:{{ item.token_hash }}</v-list-item-title>
<v-list-item-subtitle v-if="!item.token">
Created on {{ format(item.created) }}, Valid until: {{ format(item.expires) }}</v-list-item-subtitle>
<v-list-item-title :class="tokenClass(item)">sha256:{{ item.token_hash }}</v-list-item-title>
<v-list-item-subtitle v-if="!item.token" :class="tokenClass(item)">
Last used: <span v-if="item.last_used">{{ format(item.last_used) }}</span><span v-if="!item.last_used">Never</span> &mdash; valid until: {{ format(item.expires) }}
</v-list-item-subtitle>
<v-list-item-subtitle v-if="item.token">
<v-text-field
v-model="item.token"
......@@ -23,12 +24,12 @@
@click:append-outer="copy(item)" />
</v-list-item-subtitle>
<v-list-item-subtitle v-if="!item.token">
<a @click="revokeToken(item.token_hash)">Revoke Token</a>
<a @click="revokeToken(item.id)">Revoke Token</a>
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-btn class="mt-4" x-small @click="mintToken">
Mint Token
<v-btn :disabled="tokens.length >= tokenMax" class="mt-4" color="secondary" small @click="mintToken">
Create Token
</v-btn>
</v-card-text>
</v-card>
......@@ -59,6 +60,9 @@ export default {
return {
headers: { Authorization: `Bearer ${this.token}` }
}
},
tokenMax () {
return this.$config.tokenMax
}
},
mounted () {
......@@ -74,6 +78,9 @@ export default {
format (timestamp) {
return formatTimestamp(timestamp)
},
tokenClass (token) {
return token.last_used ? '' : 'token-not_used'
},
async loadTokens () {
this.loading = true
try {
......@@ -102,10 +109,10 @@ export default {
}
this.loading = false
},
async revokeToken (hash) {
async revokeToken (id) {
this.loading = true
try {
await this.$axios.delete(`/api/user/token/${hash}`, this.config)
await this.$axios.delete(`/api/user/token/${id}`, this.config)
await this.loadTokens()
} catch (err) {
this.$toast.error('Could not delete token')
......@@ -115,3 +122,8 @@ export default {
}
}
</script>
<style>
.token-not_used {
opacity: 0.4;
}
</style>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment