Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@EnableCaching
@Configuration
public class SystemConfiguration {

public final static String WALLET_SERVICE_URL = "http://localhost:8110/v1/internal/wallets";
public final static String WALLET_SERVICE_URL = "http://wallet-service:8110/v1/internal/wallets";

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
Expand All @@ -27,13 +29,18 @@ public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory)
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(60))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(serializer)
);
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put("blacklistedTokens",
defaultConfig.entryTtl(Duration.ofDays(7))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@RestController
@RequestMapping("/v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final UserAuthenticationService userAuthenticationService ;
private final UserAuthenticationService userAuthenticationService;

@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest registerRequest) {
Expand All @@ -25,9 +23,10 @@ public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest
}

@PostMapping("/refresh-access-token")
public ResponseEntity<AuthResponse> refreshAccessToken(@RequestHeader ("X-User-Id") UUID userId) {
AuthResponse response = userAuthenticationService
.refreshAccessToken(userId);
public ResponseEntity<AuthResponse> refreshAccessToken(
@RequestHeader ("X-Refresh-Token") String refreshToken
) {
AuthResponse response = userAuthenticationService.refreshAccessToken(refreshToken);
return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.micropay.security.exception;

public class InvalidTokenException extends RuntimeException {

public InvalidTokenException() {
super("Invalid token.");
}

public InvalidTokenException(String message) {
super(message);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.micropay.security.exception.handler;

import com.micropay.security.exception.DuplicateObjectException;
import com.micropay.security.exception.InvalidTokenException;
import com.micropay.security.exception.NotActiveUserException;
import com.micropay.security.exception.UserNotFoundException;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -53,7 +54,7 @@ public ResponseEntity<ErrorResponse> handleNotActiveUserException(NotActiveUserE
"User is blocked or suspended.",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(body);
return ResponseEntity.status(HttpStatus.CONFLICT).body(body);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
Expand All @@ -67,4 +68,14 @@ public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Metho
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
}

@ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<ErrorResponse> handleInvalidTokenException(InvalidTokenException exception) {
ErrorResponse body = new ErrorResponse(
HttpStatus.UNAUTHORIZED.value(),
exception.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public interface CacheService {

<T> T getOrPut(String cacheName, String key, TypeReference<T> type, Supplier<T> supplier);

void checkAndBlacklist(String refreshToken);

void evictAll(String cacheName);

void evict(String cacheName, String key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.micropay.security.exception.InvalidTokenException;
import com.micropay.security.service.cache.CacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -44,6 +45,30 @@ public <T> T getOrPut(String cacheName, String key, TypeReference<T> type, Suppl
return value;
}

private Cache getCache() {
Cache cache = cacheManager.getCache("blacklistedTokens");
if (cache == null) {
log.warn("Cache '{}' not found. ", "blacklistedTokens");
}
return cache;
}

@Override
public void checkAndBlacklist(String refreshToken) {
Cache cache = getCache();
String key = generateKey(refreshToken);

if (cache.get(key) != null) {
throw new InvalidTokenException("Token blacklisted.");
}
cache.put(key, true);
log.info("Token blacklisted: {} ", refreshToken);
}

private String generateKey(String token) {
return "blacklist:" + token;
}

@Override
public void evict(String cacheName, String key) {
Cache cache = cacheManager.getCache(cacheName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ public interface JwtService {

AuthResponse generateTokens(User user);

void validateToken(String token);

String extractUserId(String token);

String extractRole(String token);

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
import com.micropay.security.dto.response.AuthResponse;
import org.springframework.security.core.userdetails.UserDetailsService;

import java.util.UUID;

public interface UserAuthenticationService extends UserDetailsService {

AuthResponse registerUser(RegisterRequest registerRequest);

AuthResponse refreshAccessToken(UUID userId);
AuthResponse refreshAccessToken(String refreshToken);

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.micropay.security.service.security.impl;

import com.micropay.security.dto.response.AuthResponse;
import com.micropay.security.exception.InvalidTokenException;
import com.micropay.security.model.RoleType;
import com.micropay.security.model.entity.User;
import com.micropay.security.service.security.JwtService;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
Expand Down Expand Up @@ -39,6 +41,40 @@ public AuthResponse generateTokens(User user) {
return new AuthResponse(accessToken, refreshToken);
}

@Override
public void validateToken(String token) {
try {
Jwts.parser().verifyWith(generateSecretKey(secretKey)).build().parseSignedClaims(token);
} catch (JwtException exception) {
throw new InvalidTokenException();
}
}

@Override
public String extractUserId(String token) {
return Jwts.parser()
.verifyWith(generateSecretKey(secretKey))
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}

@Override
public String extractRole(String token) {
return Jwts.parser()
.verifyWith(generateSecretKey(secretKey))
.build()
.parseSignedClaims(token)
.getPayload()
.get("role", String.class);
}

private SecretKey generateSecretKey(String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}

private String generateAccessToken(UUID userId, RoleType role) {
return Jwts.builder()
.subject(userId.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,30 @@ public UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoun
}

@Override
public AuthResponse refreshAccessToken(UUID userId) {
log.info("Refreshing access token for user: {}", userId);
public AuthResponse refreshAccessToken(String refreshToken) {
cacheService.checkAndBlacklist(refreshToken);
log.info("Refreshing access token.");

jwtService.validateToken(refreshToken);

UUID userId = UUID.fromString(jwtService.extractUserId(refreshToken));
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));

String role = jwtService.extractRole(refreshToken);
validateRole(role, user);

isActive(user);
return jwtService.generateTokens(user);
}

private void validateRole(String role, User user) {
String userRole = user.getRole().getRole().toString();
if (!userRole.equals(role)) {
throw new InvalidTokenException("Invalid refresh token.");
}
}

private void isActive(User user) {
if (user.getStatus() != UserStatus.ACTIVE) {
throw new NotActiveUserException(user.getId());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.micropay.security.mapper;

import com.micropay.security.model.entity.Credential;
import com.micropay.security.model.entity.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class CredentialMapperTest {

private CredentialMapper mapper;
private final static String PIN_HASH = "PIN_HASH";

@BeforeEach
void setUp() {
mapper = new CredentialMapperImpl();
}

@Test
void shouldBuildCredentialEntity_WhenUserAndPinHashProvided() {
User user = new User();

Credential credential = mapper.buildEntity(user, PIN_HASH);

assertNotNull(credential);
assertEquals(user, credential.getUser());
assertEquals(PIN_HASH, credential.getPinHash());
}

@Test
void shouldBuildCredentialEntity_WhenUserIsNullButPinHashProvided() {
Credential credential = mapper.buildEntity(null, PIN_HASH);

assertNotNull(credential);
assertNull(credential.getUser());
assertEquals(PIN_HASH, credential.getPinHash());
}

@Test
void shouldBuildCredentialEntity_WhenUserProvidedButPinHashIsNull() {
User user = new User();

Credential credential = mapper.buildEntity(user, null);

assertNotNull(credential);
assertEquals(user, credential.getUser());
assertNull(credential.getPinHash());
}

@Test
void shouldReturnNull_WhenUserAndPinHashAreNull() {
Credential credential = mapper.buildEntity(null, null);

assertNull(credential);
}
}
Loading