Added explicitly defined endpoint security.

This commit is contained in:
Andrew Lalis 2023-01-30 14:08:02 +01:00
parent f8dcfc8843
commit 4293ddb157
5 changed files with 64 additions and 5 deletions

View File

@ -3,6 +3,7 @@ package nl.andrewlalis.gymboard_api.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -38,7 +39,22 @@ public class SecurityConfig {
.csrf().disable() .csrf().disable()
.cors().and() .cors().and()
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests().anyRequest().permitAll(); .authorizeHttpRequests()
.requestMatchers(// Allow the following GET endpoints to be public.
HttpMethod.GET,
"/exercises",
"/leaderboards",
"/gyms/**",
"/submissions/**"
).permitAll()
.requestMatchers(// Allow the following POST endpoints to be public.
HttpMethod.POST,
"/gyms/submissions",
"/gyms/submissions/upload",
"/auth/token"
).permitAll()
// Everything else must be authenticated, just to be safe.
.anyRequest().authenticated();
return http.build(); return http.build();
} }

View File

@ -6,7 +6,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration @Configuration
public class SecurityComponents { public class WebComponents {
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10); return new BCryptPasswordEncoder(10);

View File

@ -5,7 +5,7 @@ import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse;
import nl.andrewlalis.gymboard_api.controller.dto.UserResponse; import nl.andrewlalis.gymboard_api.controller.dto.UserResponse;
import nl.andrewlalis.gymboard_api.model.auth.User; import nl.andrewlalis.gymboard_api.model.auth.User;
import nl.andrewlalis.gymboard_api.service.auth.TokenService; import nl.andrewlalis.gymboard_api.service.auth.TokenService;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -20,13 +20,38 @@ public class AuthController {
this.tokenService = tokenService; this.tokenService = tokenService;
} }
/**
* Endpoint for obtaining a new access token for a user to access certain
* parts of the application. <strong>This is a public endpoint.</strong> If
* a token is successfully obtained, it should be provided to all subsequent
* requests as an Authorization "Bearer" token.
* @param credentials The credentials.
* @return The token the client should use.
*/
@PostMapping(path = "/auth/token") @PostMapping(path = "/auth/token")
public TokenResponse getToken(@RequestBody TokenCredentials credentials) { public TokenResponse getToken(@RequestBody TokenCredentials credentials) {
return tokenService.generateAccessToken(credentials); return tokenService.generateAccessToken(credentials);
} }
/**
* Endpoint that can be used by an authenticated user to fetch a new access
* token using their current one; useful for staying logged in beyond the
* duration of the initial token's expiration.
* @param auth The current authentication.
* @return The new token the client should use.
*/
@GetMapping(path = "/auth/token")
public TokenResponse getUpdatedToken(Authentication auth) {
return tokenService.regenerateAccessToken(auth);
}
/**
* Gets information about the user, as determined by the provided access
* token.
* @param user The user that requested information.
* @return The user data.
*/
@GetMapping(path = "/auth/me") @GetMapping(path = "/auth/me")
@PreAuthorize("isAuthenticated()")
public UserResponse getMyUser(@AuthenticationPrincipal User user) { public UserResponse getMyUser(@AuthenticationPrincipal User user) {
return new UserResponse(user); return new UserResponse(user);
} }

View File

@ -8,11 +8,13 @@ import nl.andrewlalis.gymboard_api.controller.dto.TokenCredentials;
import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse; import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse;
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository; import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
import nl.andrewlalis.gymboard_api.model.auth.Role; import nl.andrewlalis.gymboard_api.model.auth.Role;
import nl.andrewlalis.gymboard_api.model.auth.TokenAuthentication;
import nl.andrewlalis.gymboard_api.model.auth.User; import nl.andrewlalis.gymboard_api.model.auth.User;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
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 org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@ -67,6 +69,22 @@ public class TokenService {
if (!passwordEncoder.matches(credentials.password(), user.getPasswordHash())) { if (!passwordEncoder.matches(credentials.password(), user.getPasswordHash())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
} }
if (!user.isActivated()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
String token = generateAccessToken(user);
return new TokenResponse(token);
}
public TokenResponse regenerateAccessToken(Authentication auth) {
if (!(auth instanceof TokenAuthentication tokenAuth)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
User user = userRepository.findByIdWithRoles(tokenAuth.user().getId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED));
if (!user.isActivated()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
String token = generateAccessToken(user); String token = generateAccessToken(user);
return new TokenResponse(token); return new TokenResponse(token);
} }

View File

@ -5,7 +5,7 @@
Use this page to test new functionality, before adding it to the main Use this page to test new functionality, before adding it to the main
app. This page should be hidden on production. app. This page should be hidden on production.
</p> </p>
<div class="row" style="border: 3px solid red"> <div style="border: 3px solid red">
<h4>Auth Test</h4> <h4>Auth Test</h4>
<q-btn <q-btn
label="Do auth" label="Do auth"