diff --git a/api/src/main/java/com/code4ro/nextdoor/authentication/controller/AuthenticationController.java b/api/src/main/java/com/code4ro/nextdoor/authentication/controller/AuthenticationController.java index e73d958..64ca916 100644 --- a/api/src/main/java/com/code4ro/nextdoor/authentication/controller/AuthenticationController.java +++ b/api/src/main/java/com/code4ro/nextdoor/authentication/controller/AuthenticationController.java @@ -5,6 +5,8 @@ import com.code4ro.nextdoor.authentication.dto.RegistrationRequest; import com.code4ro.nextdoor.authentication.entity.User; import com.code4ro.nextdoor.authentication.service.AuthenticationService; +import com.code4ro.nextdoor.authentication.service.RefreshTokenService; +import com.code4ro.nextdoor.security.entity.UserPrincipal; import com.code4ro.nextdoor.security.jwt.JwtTokenProvider; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -14,10 +16,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.validation.Valid; @@ -30,17 +29,20 @@ public class AuthenticationController { private final AuthenticationService authenticationService; private final AuthenticationManager authenticationManager; private final JwtTokenProvider tokenProvider; + private final RefreshTokenService refreshTokenService; @Autowired public AuthenticationController(final AuthenticationService authenticationService, final AuthenticationManager authenticationManager, - final JwtTokenProvider tokenProvider) { + final JwtTokenProvider tokenProvider, + final RefreshTokenService refreshTokenService) { this.authenticationService = authenticationService; this.authenticationManager = authenticationManager; this.tokenProvider = tokenProvider; + this.refreshTokenService = refreshTokenService; } - @ApiOperation(value = "Register an user") + @ApiOperation("Register an user") @PostMapping("/register") public ResponseEntity register(@RequestBody RegistrationRequest signUpRequest) { final User savedUser = authenticationService.register(signUpRequest); @@ -51,7 +53,7 @@ public ResponseEntity register(@RequestBody RegistrationRequest signUpRequ return ResponseEntity.created(location).build(); } - @ApiOperation(value = "Login user in application") + @ApiOperation("Login user in application") @PostMapping("/login") public ResponseEntity login(@Valid @RequestBody final LoginRequest loginRequest) { final Authentication authentication = authenticationManager.authenticate( @@ -59,7 +61,17 @@ public ResponseEntity login(@Valid @RequestBody final SecurityContextHolder.getContext().setAuthentication(authentication); - final String jwt = tokenProvider.generateToken(authentication); - return ResponseEntity.ok(new JwtAuthenticationResponse(jwt)); + final UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + final String accessTokenJwt = tokenProvider.generateAccessToken(userPrincipal.getId()); + final String refreshTokenJwt = refreshTokenService.generate(userPrincipal.getId()); + + return ResponseEntity.ok(new JwtAuthenticationResponse(accessTokenJwt, refreshTokenJwt)); + } + + @ApiOperation("Request a new authentication token based on a refresh token") + @GetMapping("/token") + public ResponseEntity getAccessToken(@RequestParam String refreshToken) { + final String accessToken = authenticationService.getAccessToken(refreshToken); + return ResponseEntity.ok(new JwtAuthenticationResponse(accessToken, refreshToken)); } } diff --git a/api/src/main/java/com/code4ro/nextdoor/authentication/dto/JwtAuthenticationResponse.java b/api/src/main/java/com/code4ro/nextdoor/authentication/dto/JwtAuthenticationResponse.java index 00c581b..810a1df 100644 --- a/api/src/main/java/com/code4ro/nextdoor/authentication/dto/JwtAuthenticationResponse.java +++ b/api/src/main/java/com/code4ro/nextdoor/authentication/dto/JwtAuthenticationResponse.java @@ -7,9 +7,11 @@ @Setter public class JwtAuthenticationResponse { private String accessToken; + private String refreshToken; private String tokenType = "Bearer"; - public JwtAuthenticationResponse(String accessToken) { + public JwtAuthenticationResponse(final String accessToken, final String refreshToken) { this.accessToken = accessToken; + this.refreshToken = refreshToken; } } diff --git a/api/src/main/java/com/code4ro/nextdoor/authentication/entity/RefreshToken.java b/api/src/main/java/com/code4ro/nextdoor/authentication/entity/RefreshToken.java new file mode 100644 index 0000000..808a4e1 --- /dev/null +++ b/api/src/main/java/com/code4ro/nextdoor/authentication/entity/RefreshToken.java @@ -0,0 +1,21 @@ +package com.code4ro.nextdoor.authentication.entity; + +import com.code4ro.nextdoor.core.entity.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.Entity; +import java.util.Date; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Entity +public class RefreshToken extends BaseEntity { + private String userId; + private String token; + private Date expiryDate; +} diff --git a/api/src/main/java/com/code4ro/nextdoor/authentication/repository/RefreshTokenRepository.java b/api/src/main/java/com/code4ro/nextdoor/authentication/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..693b827 --- /dev/null +++ b/api/src/main/java/com/code4ro/nextdoor/authentication/repository/RefreshTokenRepository.java @@ -0,0 +1,12 @@ +package com.code4ro.nextdoor.authentication.repository; + + +import com.code4ro.nextdoor.authentication.entity.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface RefreshTokenRepository extends JpaRepository { + List findAllByUserId(String userId); +} diff --git a/api/src/main/java/com/code4ro/nextdoor/authentication/service/AuthenticationService.java b/api/src/main/java/com/code4ro/nextdoor/authentication/service/AuthenticationService.java index ecb3792..caa1d52 100644 --- a/api/src/main/java/com/code4ro/nextdoor/authentication/service/AuthenticationService.java +++ b/api/src/main/java/com/code4ro/nextdoor/authentication/service/AuthenticationService.java @@ -5,4 +5,6 @@ public interface AuthenticationService { User register(RegistrationRequest registrationRequest); + + String getAccessToken(String refreshTokenJwt); } diff --git a/api/src/main/java/com/code4ro/nextdoor/authentication/service/RefreshTokenService.java b/api/src/main/java/com/code4ro/nextdoor/authentication/service/RefreshTokenService.java new file mode 100644 index 0000000..7865428 --- /dev/null +++ b/api/src/main/java/com/code4ro/nextdoor/authentication/service/RefreshTokenService.java @@ -0,0 +1,9 @@ +package com.code4ro.nextdoor.authentication.service; + +import java.util.UUID; + +public interface RefreshTokenService { + boolean isValid(UUID userId, String jti); + + String generate(UUID userId); +} diff --git a/api/src/main/java/com/code4ro/nextdoor/authentication/service/impl/AuthenticationServiceImpl.java b/api/src/main/java/com/code4ro/nextdoor/authentication/service/impl/AuthenticationServiceImpl.java index 73ff4fd..0e9def1 100644 --- a/api/src/main/java/com/code4ro/nextdoor/authentication/service/impl/AuthenticationServiceImpl.java +++ b/api/src/main/java/com/code4ro/nextdoor/authentication/service/impl/AuthenticationServiceImpl.java @@ -1,26 +1,37 @@ package com.code4ro.nextdoor.authentication.service.impl; -import com.code4ro.nextdoor.core.exception.NextDoorValidationException; import com.code4ro.nextdoor.authentication.dto.RegistrationRequest; import com.code4ro.nextdoor.authentication.entity.User; import com.code4ro.nextdoor.authentication.repository.UserRepository; import com.code4ro.nextdoor.authentication.service.AuthenticationService; +import com.code4ro.nextdoor.authentication.service.RefreshTokenService; +import com.code4ro.nextdoor.core.exception.NextDoorValidationException; +import com.code4ro.nextdoor.security.jwt.JwtTokenProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.UUID; @Service public class AuthenticationServiceImpl implements AuthenticationService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider tokenProvider; + private final RefreshTokenService refreshTokenService; @Autowired public AuthenticationServiceImpl(final UserRepository userRepository, - final PasswordEncoder passwordEncoder) { + final PasswordEncoder passwordEncoder, + final JwtTokenProvider tokenProvider, + final RefreshTokenService refreshTokenService) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; + this.tokenProvider = tokenProvider; + this.refreshTokenService = refreshTokenService; } @Transactional @@ -34,4 +45,20 @@ public User register(final RegistrationRequest registrationRequest) { final User user = new User(registrationRequest.getEmail(), encodedPassword); return userRepository.save(user); } + + @Override + public String getAccessToken(final String refreshTokenJwt) { + if (!StringUtils.hasText(refreshTokenJwt) || !tokenProvider.isValid(refreshTokenJwt)) { + throw new NextDoorValidationException("authentication.refreshToken.invalid", HttpStatus.BAD_REQUEST); + } + + final String refreshToken = tokenProvider.getRefreshTokenFromJWT(refreshTokenJwt); + final UUID userId = tokenProvider.getUserIdFromJWT(refreshTokenJwt); + + if (!refreshTokenService.isValid(userId, refreshToken)) { + throw new NextDoorValidationException("authentication.refreshToken.invalid", HttpStatus.BAD_REQUEST); + } + + return tokenProvider.generateAccessToken(userId); + } } diff --git a/api/src/main/java/com/code4ro/nextdoor/authentication/service/impl/RefreshTokenServiceImpl.java b/api/src/main/java/com/code4ro/nextdoor/authentication/service/impl/RefreshTokenServiceImpl.java new file mode 100644 index 0000000..78c4ebd --- /dev/null +++ b/api/src/main/java/com/code4ro/nextdoor/authentication/service/impl/RefreshTokenServiceImpl.java @@ -0,0 +1,61 @@ +package com.code4ro.nextdoor.authentication.service.impl; + +import com.code4ro.nextdoor.authentication.entity.RefreshToken; +import com.code4ro.nextdoor.authentication.repository.RefreshTokenRepository; +import com.code4ro.nextdoor.authentication.service.RefreshTokenService; +import com.code4ro.nextdoor.security.entity.RefreshTokenHolder; +import com.code4ro.nextdoor.security.jwt.JwtTokenProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class RefreshTokenServiceImpl implements RefreshTokenService { + private final RefreshTokenRepository refreshTokenRepository; + private final JwtTokenProvider tokenProvider; + private final PasswordEncoder passwordEncoder; + + @Autowired + public RefreshTokenServiceImpl(final RefreshTokenRepository refreshTokenRepository, + final JwtTokenProvider tokenProvider, + final PasswordEncoder passwordEncoder) { + this.refreshTokenRepository = refreshTokenRepository; + this.tokenProvider = tokenProvider; + this.passwordEncoder = passwordEncoder; + } + + @Override + public boolean isValid(final UUID userId, final String jti) { + final List refreshTokens = refreshTokenRepository.findAllByUserId(userId.toString()); + final Optional refreshTokenOptional = refreshTokens.stream() + .filter(rt -> passwordEncoder.matches(jti, rt.getToken())) + .findFirst(); + if (refreshTokenOptional.isEmpty()) { + return false; + } + + final RefreshToken refreshToken = refreshTokenOptional.get(); + final boolean isValid = refreshToken.getExpiryDate().after(new Date()); + if (!isValid) { + refreshTokenRepository.delete(refreshToken); + } + + return isValid; + } + + @Override + public String generate(final UUID userId) { + final String refreshToken = UUID.randomUUID().toString(); + final RefreshTokenHolder refreshTokenHolder = tokenProvider.generateRefreshToken(userId, refreshToken); + final RefreshToken refreshTokenEntity = refreshTokenHolder.getRefreshToken(); + refreshTokenEntity.setToken(passwordEncoder.encode(refreshToken)); + refreshTokenRepository.save(refreshTokenEntity); + + return refreshTokenHolder.getRefreshTokenJwt(); + } +} diff --git a/api/src/main/java/com/code4ro/nextdoor/group/entity/Group.java b/api/src/main/java/com/code4ro/nextdoor/group/entity/Group.java index f102c0c..b957320 100644 --- a/api/src/main/java/com/code4ro/nextdoor/group/entity/Group.java +++ b/api/src/main/java/com/code4ro/nextdoor/group/entity/Group.java @@ -12,7 +12,7 @@ import javax.persistence.OneToOne; import javax.persistence.Table; -@Table(name = "groups") +@Table(name = "nd_groups") @Entity @Getter @Setter diff --git a/api/src/main/java/com/code4ro/nextdoor/group/service/impl/GroupServiceImpl.java b/api/src/main/java/com/code4ro/nextdoor/group/service/impl/GroupServiceImpl.java index dd91095..91e170b 100644 --- a/api/src/main/java/com/code4ro/nextdoor/group/service/impl/GroupServiceImpl.java +++ b/api/src/main/java/com/code4ro/nextdoor/group/service/impl/GroupServiceImpl.java @@ -5,9 +5,11 @@ import com.code4ro.nextdoor.group.dto.GroupDto; import com.code4ro.nextdoor.group.dto.GroupUpdateDto; import com.code4ro.nextdoor.group.entity.Group; +import com.code4ro.nextdoor.group.entity.GroupSecurityPolicy; import com.code4ro.nextdoor.group.repository.GroupRepository; import com.code4ro.nextdoor.group.service.GroupService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,12 +20,15 @@ public class GroupServiceImpl implements GroupService { private final GroupRepository groupRepository; private final MapperService mapperService; + private final PasswordEncoder passwordEncoder; @Autowired public GroupServiceImpl(final GroupRepository groupRepository, - final MapperService mapperService) { + final MapperService mapperService, + final PasswordEncoder passwordEncoder) { this.groupRepository = groupRepository; this.mapperService = mapperService; + this.passwordEncoder = passwordEncoder; } @Override @@ -32,6 +37,10 @@ public GroupDto create(final GroupCreateDto createDto) { final Group group = mapperService.map(createDto, Group.class); if (group.getOpen()) { group.setSecurityPolicy(null); + } else { + final GroupSecurityPolicy securityPolicy = group.getSecurityPolicy(); + final String rawAnswer = securityPolicy.getAnswer(); + securityPolicy.setAnswer(passwordEncoder.encode(rawAnswer)); } final Group savedGroup = groupRepository.save(group); diff --git a/api/src/main/java/com/code4ro/nextdoor/security/entity/RefreshTokenHolder.java b/api/src/main/java/com/code4ro/nextdoor/security/entity/RefreshTokenHolder.java new file mode 100644 index 0000000..0940e02 --- /dev/null +++ b/api/src/main/java/com/code4ro/nextdoor/security/entity/RefreshTokenHolder.java @@ -0,0 +1,14 @@ +package com.code4ro.nextdoor.security.entity; + +import com.code4ro.nextdoor.authentication.entity.RefreshToken; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class RefreshTokenHolder { + private final String refreshTokenJwt; + private final RefreshToken refreshToken; +} diff --git a/api/src/main/java/com/code4ro/nextdoor/security/jwt/JwtAuthenticationFilter.java b/api/src/main/java/com/code4ro/nextdoor/security/jwt/JwtAuthenticationFilter.java index ce6df10..7b8e8b9 100644 --- a/api/src/main/java/com/code4ro/nextdoor/security/jwt/JwtAuthenticationFilter.java +++ b/api/src/main/java/com/code4ro/nextdoor/security/jwt/JwtAuthenticationFilter.java @@ -47,7 +47,7 @@ protected void doFilterInternal(final HttpServletRequest request, final FilterChain filterChain) throws ServletException, IOException { try { final String jwt = getJwtFromRequest(request); - if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + if (StringUtils.hasText(jwt) && tokenProvider.isValid(jwt)) { final UUID userId = tokenProvider.getUserIdFromJWT(jwt); final UserDetails userDetails = customUserDetailsService.loadUserById(userId); final UsernamePasswordAuthenticationToken authentication = diff --git a/api/src/main/java/com/code4ro/nextdoor/security/jwt/JwtTokenProvider.java b/api/src/main/java/com/code4ro/nextdoor/security/jwt/JwtTokenProvider.java index 936cd19..019ad1d 100644 --- a/api/src/main/java/com/code4ro/nextdoor/security/jwt/JwtTokenProvider.java +++ b/api/src/main/java/com/code4ro/nextdoor/security/jwt/JwtTokenProvider.java @@ -1,11 +1,11 @@ package com.code4ro.nextdoor.security.jwt; -import com.code4ro.nextdoor.security.entity.UserPrincipal; +import com.code4ro.nextdoor.authentication.entity.RefreshToken; +import com.code4ro.nextdoor.security.entity.RefreshTokenHolder; import io.jsonwebtoken.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import java.util.Date; @@ -15,25 +15,40 @@ public class JwtTokenProvider { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenProvider.class); - @Value("${app.jwtSecret}") + @Value("${app.jwt.secret}") private String jwtSecret; - @Value("${app.jwtExpirationInMs}") - private int jwtExpirationInMs; + @Value("${app.jwt.accessTokenExpirationInMs}") + private long accessTokenExpirationInMs; - public String generateToken(final Authentication authentication) { - - final UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + @Value("${app.jwt.refreshTokenExpirationInMs}") + private long refreshTokenExpirationInMs; + public String generateAccessToken(final UUID userId) { final Date now = new Date(); - final Date expiryDate = new Date(now.getTime() + jwtExpirationInMs); + final Date expiryDate = new Date(now.getTime() + accessTokenExpirationInMs); return Jwts.builder() - .setSubject(userPrincipal.getId().toString()) + .setSubject(userId.toString()) + .setIssuedAt(new Date()) + .setExpiration(expiryDate) + .signWith(SignatureAlgorithm.HS512, jwtSecret) + .compact(); + } + + public RefreshTokenHolder generateRefreshToken(final UUID userId, final String refreshToken) { + final Date now = new Date(); + final Date expiryDate = new Date(now.getTime() + refreshTokenExpirationInMs); + + final String refreshTokenJwt = Jwts.builder() + .setId(refreshToken) + .setSubject(userId.toString()) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); + final RefreshToken refreshTokenEntity = new RefreshToken(userId.toString(), refreshToken, expiryDate); + return new RefreshTokenHolder(refreshTokenJwt, refreshTokenEntity); } public UUID getUserIdFromJWT(final String token) { @@ -45,7 +60,16 @@ public UUID getUserIdFromJWT(final String token) { return UUID.fromString(claims.getSubject()); } - public boolean validateToken(final String authToken) { + public String getRefreshTokenFromJWT(final String token) { + Claims claims = Jwts.parser() + .setSigningKey(jwtSecret) + .parseClaimsJws(token) + .getBody(); + + return claims.getId(); + } + + public boolean isValid(final String authToken) { try { Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); return true; @@ -60,6 +84,7 @@ public boolean validateToken(final String authToken) { } catch (IllegalArgumentException ex) { LOGGER.error("JWT claims string is empty."); } + return false; } } diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 4715580..a8f2394 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -10,8 +10,10 @@ spring: ddl-auto: update flyway: enabled: true - locations: classpath:db/migrate,db/data + locations: classpath:db/migrate,classpath:db/data baselineOnMigrate: true app: - jwtSecret: test - jwtExpirationInMs: 1000 + jwt: + secret: test + accessTokenExpirationInMs: 86400000 + refreshTokenExpirationInMs: 30758400000 diff --git a/api/src/test/java/com/code4ro/nextdoor/core/AbstractControllerIntegrationTest.java b/api/src/test/java/com/code4ro/nextdoor/core/AbstractControllerIntegrationTest.java index 3704939..7234def 100644 --- a/api/src/test/java/com/code4ro/nextdoor/core/AbstractControllerIntegrationTest.java +++ b/api/src/test/java/com/code4ro/nextdoor/core/AbstractControllerIntegrationTest.java @@ -19,7 +19,7 @@ @SpringBootTest @AutoConfigureMockMvc @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -public class AbstractControllerIntegrationTest { +public abstract class AbstractControllerIntegrationTest { @Autowired protected MockMvc mvc; diff --git a/api/src/test/java/com/code4ro/nextdoor/group/GroupServiceTest.java b/api/src/test/java/com/code4ro/nextdoor/group/GroupServiceTest.java index 438c7aa..2ef7639 100644 --- a/api/src/test/java/com/code4ro/nextdoor/group/GroupServiceTest.java +++ b/api/src/test/java/com/code4ro/nextdoor/group/GroupServiceTest.java @@ -13,6 +13,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.crypto.password.PasswordEncoder; import javax.persistence.EntityNotFoundException; import java.util.Optional; @@ -29,6 +30,8 @@ public class GroupServiceTest { private GroupRepository groupRepository; @Mock private MapperService mapperService; + @Mock + private PasswordEncoder passwordEncoder; @InjectMocks private GroupServiceImpl groupService; @@ -54,6 +57,7 @@ public void createClosedGroup() { final Group group = GroupFactory.createClosedGroup(); when(mapperService.map(createDto, Group.class)).thenReturn(group); + when(passwordEncoder.encode(any())).thenReturn(UUID.randomUUID().toString()); groupService.create(createDto); diff --git a/api/src/test/resources/application.yml b/api/src/test/resources/application.yml index 4b590f7..d05417b 100644 --- a/api/src/test/resources/application.yml +++ b/api/src/test/resources/application.yml @@ -7,6 +7,13 @@ spring: jpa: hibernate.ddl-auto: create-drop generate-ddl: true + flyway: + enabled: false + locations: classpath:db/migrate + baselineOnMigrate: true app: - jwtSecret: test - jwtExpirationInMs: 172800000 + jwt: + secret: test + accessTokenExpirationInMs: 86400000 + refreshTokenExpirationInMs: 30758400000 +