Implement token-based authentication
This commit is contained in:
parent
7237e12363
commit
5a9c845b3f
15 changed files with 280 additions and 12 deletions
|
@ -27,7 +27,7 @@ jar {
|
|||
description = "twigs-server"
|
||||
}
|
||||
|
||||
mainClassName = "com.wbrawner.budgetserver.BudgetServerApplication"
|
||||
mainClassName = "com.wbrawner.budgetserver.TwigsServerApplication"
|
||||
|
||||
sourceCompatibility = 14
|
||||
targetCompatibility = 14
|
||||
|
|
|
@ -2,10 +2,12 @@ package com.wbrawner.budgetserver;
|
|||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
public class BudgetServerApplication {
|
||||
@EnableScheduling
|
||||
public class TwigsServerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(BudgetServerApplication.class, args);
|
||||
SpringApplication.run(TwigsServerApplication.class, args);
|
||||
}
|
||||
}
|
|
@ -3,10 +3,11 @@ package com.wbrawner.budgetserver;
|
|||
import com.wbrawner.budgetserver.user.User;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.UUID;
|
||||
import java.util.Random;
|
||||
|
||||
public final class Utils {
|
||||
private static final int[] CALENDAR_FIELDS = new int[]{
|
||||
|
@ -33,6 +34,12 @@ public final class Utils {
|
|||
return calendar.getTime();
|
||||
}
|
||||
|
||||
public static Date twoWeeksFromNow() {
|
||||
GregorianCalendar calendar = new GregorianCalendar();
|
||||
calendar.add(Calendar.DATE, 14);
|
||||
return calendar.getTime();
|
||||
}
|
||||
|
||||
public static User getCurrentUser() {
|
||||
Object user = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
if (user instanceof User) {
|
||||
|
@ -42,7 +49,18 @@ public final class Utils {
|
|||
return null;
|
||||
}
|
||||
|
||||
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
private static final Random random = new SecureRandom();
|
||||
|
||||
public static String randomString(int length) {
|
||||
StringBuilder id = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
id.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
|
||||
}
|
||||
return id.toString();
|
||||
}
|
||||
|
||||
public static String randomId() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
return randomString(32);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@ package com.wbrawner.budgetserver.budget;
|
|||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
|
||||
public interface BudgetRepository extends PagingAndSortingRepository<Budget, Long> {
|
||||
public interface BudgetRepository extends PagingAndSortingRepository<Budget, String> {
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.wbrawner.budgetserver.config;
|
||||
|
||||
import com.wbrawner.budgetserver.passwordresetrequest.PasswordResetRequestRepository;
|
||||
import com.wbrawner.budgetserver.session.UserSessionRepository;
|
||||
import com.wbrawner.budgetserver.user.UserRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
@ -11,6 +12,7 @@ import org.springframework.security.config.annotation.authentication.builders.Au
|
|||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.provisioning.JdbcUserDetailsManager;
|
||||
|
@ -28,6 +30,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
|
||||
private final Environment env;
|
||||
private final DataSource datasource;
|
||||
private final UserSessionRepository userSessionRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordResetRequestRepository passwordResetRequestRepository;
|
||||
private final JdbcUserDetailsService userDetailsService;
|
||||
|
@ -35,12 +38,14 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
|
||||
public SecurityConfig(Environment env,
|
||||
DataSource datasource,
|
||||
UserSessionRepository userSessionRepository,
|
||||
UserRepository userRepository,
|
||||
PasswordResetRequestRepository passwordResetRequestRepository,
|
||||
JdbcUserDetailsService userDetailsService,
|
||||
Environment environment) {
|
||||
this.env = env;
|
||||
this.datasource = datasource;
|
||||
this.userSessionRepository = userSessionRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.passwordResetRequestRepository = passwordResetRequestRepository;
|
||||
this.userDetailsService = userDetailsService;
|
||||
|
@ -56,7 +61,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider getAuthenticationProvider() {
|
||||
var authProvider = new DaoAuthenticationProvider();
|
||||
var authProvider = new TokenAuthenticationProvider(userSessionRepository, userRepository);
|
||||
authProvider.setPasswordEncoder(getPasswordEncoder());
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
return authProvider;
|
||||
|
@ -81,6 +86,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
.authenticated()
|
||||
.and()
|
||||
.httpBasic()
|
||||
.authenticationEntryPoint(new SilentAuthenticationEntryPoint())
|
||||
.and()
|
||||
.cors()
|
||||
.configurationSource(request -> {
|
||||
|
@ -104,7 +110,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
})
|
||||
.and()
|
||||
.csrf()
|
||||
.disable();
|
||||
.disable()
|
||||
.addFilter(new TokenAuthenticationFilter(authenticationManager()))
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.wbrawner.budgetserver.config;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class SessionAuthenticationToken extends UsernamePasswordAuthenticationToken {
|
||||
/**
|
||||
* Creates a token with the supplied array of authorities.
|
||||
*
|
||||
* @param authorities the collection of <tt>GrantedAuthority</tt>s for the principal
|
||||
* represented by this authentication object.
|
||||
* @param credentials
|
||||
* @param principal
|
||||
*/
|
||||
public SessionAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(principal, credentials, authorities);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.wbrawner.budgetserver.config;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Used to avoid browser prompts for authentication
|
||||
*/
|
||||
public class SilentAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.wbrawner.budgetserver.config;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
|
||||
|
||||
public TokenAuthenticationFilter(AuthenticationManager authenticationManager) {
|
||||
super(authenticationManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
var authHeader = request.getHeader("Authorization");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
var token = authHeader.substring(7);
|
||||
var authentication = getAuthenticationManager().authenticate(new SessionAuthenticationToken(null, token, Collections.emptyList()));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package com.wbrawner.budgetserver.config;
|
||||
|
||||
import com.wbrawner.budgetserver.session.UserSessionRepository;
|
||||
import com.wbrawner.budgetserver.user.UserRepository;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class TokenAuthenticationProvider extends DaoAuthenticationProvider {
|
||||
private final UserSessionRepository userSessionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public TokenAuthenticationProvider(UserSessionRepository userSessionRepository, UserRepository userRepository) {
|
||||
this.userSessionRepository = userSessionRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
|
||||
if (!(authentication instanceof SessionAuthenticationToken)) {
|
||||
// Additional checks aren't needed since they've already been handled
|
||||
super.additionalAuthenticationChecks(userDetails, authentication);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
if (authentication instanceof SessionAuthenticationToken) {
|
||||
var session = userSessionRepository.findByToken((String) authentication.getCredentials());
|
||||
if (session.isEmpty() || session.get().getExpiration().before(new Date())) {
|
||||
throw new BadCredentialsException("Credentials expired");
|
||||
}
|
||||
var user = userRepository.findById(session.get().getUserId());
|
||||
if (user.isEmpty()) {
|
||||
throw new InternalAuthenticationServiceException("Failed to find user for token");
|
||||
}
|
||||
return new SessionAuthenticationToken(user.get(), authentication.getCredentials(), authentication.getAuthorities());
|
||||
} else {
|
||||
return super.authenticate(authentication);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.wbrawner.budgetserver.session;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.*;
|
||||
|
||||
@Entity
|
||||
public class Session {
|
||||
@Id
|
||||
private final String id = randomId();
|
||||
private final String userId;
|
||||
private final String token = randomString(255);
|
||||
private Date expiration = twoWeeksFromNow();
|
||||
|
||||
public Session() {
|
||||
this("");
|
||||
}
|
||||
|
||||
public Session(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public Date getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
public void setExpiration(Date expiration) {
|
||||
this.expiration = expiration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.wbrawner.budgetserver.session;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
public class SessionCleanupTask {
|
||||
private final UserSessionRepository sessionRepository;
|
||||
|
||||
@Autowired
|
||||
public SessionCleanupTask(UserSessionRepository sessionRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 * * * *")
|
||||
public void cleanup() {
|
||||
sessionRepository.deleteAllByExpirationBefore(new Date());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.wbrawner.budgetserver.session;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class SessionResponse {
|
||||
private final String token;
|
||||
private final String expiration;
|
||||
|
||||
public SessionResponse(Session session) {
|
||||
this(session.getToken(), session.getExpiration());
|
||||
}
|
||||
|
||||
public SessionResponse(String token, Date expiration) {
|
||||
this.token = token;
|
||||
this.expiration = expiration.toInstant().toString();
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public String getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.wbrawner.budgetserver.session;
|
||||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserSessionRepository extends PagingAndSortingRepository<Session, String> {
|
||||
List<Session> findByUserId(String userId);
|
||||
|
||||
Optional<Session> findByToken(String token);
|
||||
|
||||
Optional<Session> findByUserIdAndToken(String userId, String token);
|
||||
|
||||
void deleteAllByExpirationBefore(Date expiration);
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package com.wbrawner.budgetserver.user;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Transient;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -31,6 +34,7 @@ public class User implements UserDetails {
|
|||
this.email = email;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getId() {
|
||||
// This shouldn't ever need to be set manually, only through Hibernate
|
||||
//noinspection ConstantConditions
|
||||
|
|
|
@ -4,6 +4,9 @@ import com.wbrawner.budgetserver.ErrorResponse;
|
|||
import com.wbrawner.budgetserver.budget.BudgetRepository;
|
||||
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
||||
import com.wbrawner.budgetserver.permission.UserPermissionResponse;
|
||||
import com.wbrawner.budgetserver.session.Session;
|
||||
import com.wbrawner.budgetserver.session.SessionResponse;
|
||||
import com.wbrawner.budgetserver.session.UserSessionRepository;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.Authorization;
|
||||
|
@ -20,6 +23,7 @@ import org.springframework.web.bind.annotation.*;
|
|||
|
||||
import javax.transaction.Transactional;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.wbrawner.budgetserver.Utils.getCurrentUser;
|
||||
|
@ -33,16 +37,19 @@ public class UserController {
|
|||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserPermissionRepository userPermissionsRepository;
|
||||
private final UserSessionRepository userSessionRepository;
|
||||
private final DaoAuthenticationProvider authenticationProvider;
|
||||
|
||||
@Autowired
|
||||
public UserController(BudgetRepository budgetRepository,
|
||||
UserRepository userRepository,
|
||||
UserSessionRepository userSessionRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
UserPermissionRepository userPermissionsRepository,
|
||||
DaoAuthenticationProvider authenticationProvider) {
|
||||
this.budgetRepository = budgetRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.userSessionRepository = userSessionRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.userPermissionsRepository = userPermissionsRepository;
|
||||
this.authenticationProvider = authenticationProvider;
|
||||
|
@ -51,7 +58,7 @@ public class UserController {
|
|||
|
||||
@GetMapping(path = "", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
@ApiOperation(value = "getUsers", nickname = "getUsers", tags = {"Users"})
|
||||
ResponseEntity<List<UserPermissionResponse>> getUsers(Long budgetId) {
|
||||
ResponseEntity<List<UserPermissionResponse>> getUsers(String budgetId) {
|
||||
var budget = budgetRepository.findById(budgetId).orElse(null);
|
||||
if (budget == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
|
@ -69,7 +76,7 @@ public class UserController {
|
|||
|
||||
@PostMapping(path = "/login", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
@ApiOperation(value = "login", nickname = "login", tags = {"Users"})
|
||||
ResponseEntity<UserResponse> login(@RequestBody LoginRequest request) {
|
||||
ResponseEntity<SessionResponse> login(@RequestBody LoginRequest request) {
|
||||
var authReq = new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword());
|
||||
Authentication auth;
|
||||
try {
|
||||
|
@ -78,7 +85,9 @@ public class UserController {
|
|||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
return ResponseEntity.ok(new UserResponse(getCurrentUser()));
|
||||
var user = Objects.requireNonNull(getCurrentUser());
|
||||
var session = userSessionRepository.save(new Session(user.getId()));
|
||||
return ResponseEntity.ok(new SessionResponse(session));
|
||||
}
|
||||
|
||||
@GetMapping(path = "/me", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
|
|
Loading…
Reference in a new issue