Initial commit.
This commit is contained in:
37
codiki-application/pom.xml
Normal file
37
codiki-application/pom.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.codiki</groupId>
|
||||
<artifactId>codiki-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<artifactId>codiki-application</artifactId>
|
||||
|
||||
<name>codiki-application</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.codiki</groupId>
|
||||
<artifactId>codiki-domain</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.codiki.application.configuration;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
@Configuration
|
||||
public class ServiceConfiguration {
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.codiki.application.security;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
// This class allow to retrieve connected user information through "Authentication" object.
|
||||
@Component
|
||||
public class AuthenticationFacade {
|
||||
public Authentication getAuthentication() {
|
||||
return SecurityContextHolder.getContext().getAuthentication();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.codiki.application.security;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.codiki.application.user.UserUseCases;
|
||||
import org.codiki.application.security.model.CustomUserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
private final UserUseCases userUseCases;
|
||||
|
||||
public CustomUserDetailsService(UserUseCases userUseCases) {
|
||||
this.userUseCases = userUseCases;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String userIdAsString) throws UsernameNotFoundException {
|
||||
UUID userId = parseUserId(userIdAsString);
|
||||
return userUseCases.findById(userId)
|
||||
.map(CustomUserDetails::new)
|
||||
.orElseThrow(() -> new UsernameNotFoundException(userIdAsString));
|
||||
}
|
||||
|
||||
private UUID parseUserId(String userIdAsString) {
|
||||
try {
|
||||
return UUID.fromString(userIdAsString);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new UsernameNotFoundException(userIdAsString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.codiki.application.security;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import org.codiki.domain.user.model.User;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
private final Algorithm algorithm;
|
||||
private final JWTVerifier jwtVerifier;
|
||||
private final int tokenExpirationDelayInMinutes;
|
||||
|
||||
public JwtService(
|
||||
@Value("${application.security.jwt.secretKey}") String secretKey,
|
||||
@Value("${application.security.jwt.expirationDelayInMinutes}") int tokenExpirationDelayInMinutes
|
||||
) {
|
||||
algorithm = Algorithm.HMAC512(secretKey);
|
||||
this.tokenExpirationDelayInMinutes = tokenExpirationDelayInMinutes;
|
||||
jwtVerifier = JWT.require(algorithm).build();
|
||||
}
|
||||
|
||||
public String createJwt(User user) {
|
||||
ZonedDateTime expirationDate = ZonedDateTime.now().plusMinutes(tokenExpirationDelayInMinutes);
|
||||
|
||||
return JWT.create()
|
||||
.withSubject(user.id().toString())
|
||||
.withExpiresAt(expirationDate.toInstant())
|
||||
.sign(algorithm);
|
||||
}
|
||||
|
||||
public boolean isValid(String token) {
|
||||
boolean result;
|
||||
try {
|
||||
jwtVerifier.verify(token);
|
||||
result = true;
|
||||
} catch (JWTVerificationException exception) {
|
||||
result = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String extractUsername(String token) {
|
||||
return JWT.decode(token).getSubject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.codiki.application.security.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("hasAuthority('ROLE_' + T(org.codiki.domain.user.model.UserRole).ADMIN.name())")
|
||||
public @interface AllowedToAdmins {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.codiki.application.security.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("permitAll()")
|
||||
public @interface AllowedToAnonymous {
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.codiki.application.security.model;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.codiki.domain.user.model.User;
|
||||
import org.codiki.domain.user.model.UserRole;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
public class CustomUserDetails implements UserDetails {
|
||||
private final User user;
|
||||
|
||||
public CustomUserDetails(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return user.roles()
|
||||
.stream()
|
||||
.map(UserRole::name)
|
||||
.map(role -> "ROLE_" + role)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return user.id().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return user.password();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.codiki.application.user;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.codiki.application.security.AuthenticationFacade;
|
||||
import org.codiki.application.security.JwtService;
|
||||
import org.codiki.application.security.annotation.AllowedToAdmins;
|
||||
import org.codiki.domain.exception.LoginFailureException;
|
||||
import org.codiki.domain.exception.RefreshTokenDoesNotExistException;
|
||||
import org.codiki.domain.exception.UserDoesNotExistException;
|
||||
import org.codiki.domain.user.model.RefreshToken;
|
||||
import org.codiki.domain.user.model.User;
|
||||
import org.codiki.domain.user.model.UserAuthenticationData;
|
||||
import org.codiki.domain.user.port.UserPort;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserUseCases {
|
||||
private static final String TOKEN_TYPE = "Bearer";
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
private final UserPort userPort;
|
||||
private final AuthenticationFacade authenticationFacade;
|
||||
private final int refreshTokenExpirationDelayInDays;
|
||||
|
||||
public UserUseCases(
|
||||
AuthenticationFacade authenticationFacade,
|
||||
JwtService jwtService,
|
||||
PasswordEncoder passwordEncoder,
|
||||
UserPort userPort,
|
||||
@Value("${application.security.refreshToken.expirationDelayInDays}")
|
||||
int refreshTokenExpirationDelayInDays
|
||||
) {
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtService = jwtService;
|
||||
this.userPort = userPort;
|
||||
this.authenticationFacade = authenticationFacade;
|
||||
this.refreshTokenExpirationDelayInDays = refreshTokenExpirationDelayInDays;
|
||||
}
|
||||
|
||||
public Optional<User> findById(UUID userId) {
|
||||
return userPort.findById(userId);
|
||||
}
|
||||
|
||||
@AllowedToAdmins
|
||||
public List<User> findAll() {
|
||||
return userPort.findAll();
|
||||
}
|
||||
|
||||
public UserAuthenticationData authenticate(UUID userId, String password) {
|
||||
User user = userPort.findById(userId)
|
||||
.orElseThrow(LoginFailureException::new);
|
||||
|
||||
if (!passwordEncoder.matches(password, user.password())) {
|
||||
throw new LoginFailureException();
|
||||
}
|
||||
|
||||
return generateAuthenticationData(user);
|
||||
}
|
||||
|
||||
public UserAuthenticationData authenticate(UUID refreshTokenValue) {
|
||||
RefreshToken refreshToken = userPort.findRefreshTokenById(refreshTokenValue)
|
||||
.filter(RefreshToken::isNotExpired)
|
||||
.orElseThrow(() -> new RefreshTokenDoesNotExistException(refreshTokenValue));
|
||||
|
||||
User user = userPort.findById(refreshToken.userId())
|
||||
.orElseThrow(() -> new UserDoesNotExistException(refreshToken.userId()));
|
||||
|
||||
return generateAuthenticationData(user);
|
||||
}
|
||||
|
||||
public Optional<User> getAuthenticatedUser() {
|
||||
return Optional.of(authenticationFacade.getAuthentication())
|
||||
.map(Authentication::getPrincipal)
|
||||
.filter(String.class::isInstance)
|
||||
.map(String.class::cast)
|
||||
.map(UUID::fromString)
|
||||
.flatMap(userPort::findById);
|
||||
}
|
||||
|
||||
private UserAuthenticationData generateAuthenticationData(User user) {
|
||||
String accessToken = jwtService.createJwt(user);
|
||||
|
||||
RefreshToken newRefreshToken = createNewRefreshToken(user);
|
||||
|
||||
return new UserAuthenticationData(
|
||||
TOKEN_TYPE,
|
||||
accessToken,
|
||||
newRefreshToken
|
||||
);
|
||||
}
|
||||
|
||||
private RefreshToken createNewRefreshToken(User user) {
|
||||
RefreshToken refreshToken = new RefreshToken(
|
||||
user.id(),
|
||||
ZonedDateTime.now().plusDays(refreshTokenExpirationDelayInDays)
|
||||
);
|
||||
userPort.save(refreshToken);
|
||||
return refreshToken;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user