Skip to content

Commit

Permalink
Merge pull request #20 from rajumb0232/auth/updates
Browse files Browse the repository at this point in the history
Refactoring Authentication Module
  • Loading branch information
rajumb0232 authored Nov 9, 2024
2 parents b087315 + 4aad34c commit 81b36b0
Show file tree
Hide file tree
Showing 26 changed files with 610 additions and 209 deletions.
2 changes: 2 additions & 0 deletions E-Stores-API/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// use this template to create the .env file.
DOMAIN=
IS_HTTPS=
MAIL_USERNAME=
MAIL_PASSWORD=
POSTGRES_URL=
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.devb.estores.cache;

import com.devb.estores.config.AppEnv;
import com.devb.estores.model.User;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
@AllArgsConstructor
public class CacheBeanConfig {

private final AppEnv appEnv;

private <K, V> CacheConfigurer<K, V> configure(String cacheName, Class<K> keyType, Class<V> valueType, int heap, int offHeap, Duration ttl) {
return CacheConfigurer.<K, V>builder()
.cacheName(cacheName)
Expand All @@ -29,4 +34,14 @@ CacheConfigurer<String, Integer> OtpCache() {
CacheConfigurer<String, User> userCache() {
return this.configure(CacheName.USER_CACHE, String.class, User.class, 20000, 10, Duration.ofDays(1));
}

@Bean
CacheConfigurer<String, String> accessTokenCache() {
return this.configure(CacheName.ACCESS_TOKEN_CACHE, String.class, String.class, 50000, 10, Duration.ofSeconds(appEnv.getJwt().getAccessExpirationSeconds()));
}

@Bean
CacheConfigurer<String, String> refreshTokenCache() {
return this.configure(CacheName.REFRESH_TOKEN_CACHE, String.class, String.class, 50000, 10, Duration.ofSeconds(appEnv.getJwt().getRefreshExpirationSeconds()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
public class CacheName {
public static final String OTP_CACHE = "otp-cache";
public static final String USER_CACHE = "user-cache";
public static final String ACCESS_TOKEN_CACHE = "access-token-cache";
public static final String REFRESH_TOKEN_CACHE = "refresh-token-cache";
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
public class AppEnv {

private String baseURL;
private String domain;
private boolean isHttps;
private Jwt jwt;

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,8 @@ public ResponseEntity<ResponseStructure<AuthResponse>> login(@RequestBody AuthRe

@PostMapping("/logout")
@PreAuthorize("hasAuthority('ADMIN') OR hasAuthority('SUPER_ADMIN') OR hasAuthority('SELLER') OR hasAuthority('CUSTOMER')")
public ResponseEntity<SimpleResponseStructure> logout(@CookieValue(name = "rt", required = false) String refreshToken,
@CookieValue(name = "at", required = false) String accessToken) {
authService.logout(refreshToken, accessToken);
HttpHeaders headers = authService.invalidateTokens();
public ResponseEntity<SimpleResponseStructure> logout(@CookieValue(name = "at", required = false) String accessToken) {
HttpHeaders headers = authService.logout(accessToken);
return responseBuilder.success(HttpStatus.OK, headers, "Logout Successful");
}

Expand All @@ -88,9 +86,9 @@ public ResponseEntity<ResponseStructure<AuthResponse>> refreshLogin(@CookieValue

@PostMapping("/revoke-other")
@PreAuthorize("hasAuthority('ADMIN') OR hasAuthority('SUPER_ADMIN') OR hasAuthority('SELLER') OR hasAuthority('CUSTOMER')")
public ResponseEntity<SimpleResponseStructure> revokeAllOtherTokens(@CookieValue(name = "rt", required = false) String refreshToken,
@CookieValue(name = "at", required = false) String accessToken) {
authService.revokeAllOtherTokens(refreshToken, accessToken);
public ResponseEntity<SimpleResponseStructure> revokeAllOtherTokens(@CookieValue(name = "at", required = false) String accessToken,
@CookieValue(name = "did", required = false) String deviceId) {
authService.revokeAllOtherTokens(accessToken, deviceId);
return responseBuilder.success(HttpStatus.OK, "Successfully revoked all other device access");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.devb.estores.enums;

public enum TokenType {
ACCESS, REFRESH;
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,10 @@ public ResponseEntity<ErrorStructure<String>> handleInvalidUserRole(InvalidUserR
log.error(ex.getMessage()+" | "+"Invalid User role specified");
return errorResponse.structure(HttpStatus.BAD_REQUEST, ex.getMessage(), "Invalid User role specified");
}

@ExceptionHandler(InvalidJwtException.class)
public ResponseEntity<ErrorStructure<String>> handleInvalidJwtException(InvalidJwtException ex) {
log.error("{} | Invalid JWT used", ex.getMessage());
return errorResponse.structure(HttpStatus.FORBIDDEN, ex.getMessage(), "Invalid JWT used");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.devb.estores.exceptions;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class InvalidJwtException extends RuntimeException {
private final String message;
}
35 changes: 35 additions & 0 deletions E-Stores-API/src/main/java/com/devb/estores/mapper/UserMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.devb.estores.mapper;

import com.devb.estores.enums.UserRole;
import com.devb.estores.model.User;
import com.devb.estores.requestdto.UserRequest;
import com.devb.estores.responsedto.UserResponse;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class UserMapper {

public UserResponse mapToUserResponse(User user) {
return UserResponse.builder()
.userId(user.getUserId())
.username(user.getUsername())
.roles(user.getRoles().stream().map(UserRole::name).toList())
.email(user.getEmail())
.isEmailVerified(user.isEmailVerified())
.build();
}

public User mapToUserEntity(UserRequest userRequest, UserRole role) {
return User.builder()
.username(userRequest.getEmail().split("@gmail.com")[0])
.email(userRequest.getEmail())
.password(userRequest.getPassword())
.roles(role.equals(UserRole.SELLER)
? Arrays.asList(UserRole.SELLER, UserRole.CUSTOMER)
: List.of(UserRole.CUSTOMER))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.devb.estores.model;

import com.devb.estores.enums.TokenType;
import com.devb.estores.util.IdGenerator;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.GenericGenerator;

import java.time.LocalDateTime;

@Entity
@Table(name = "token_identifications")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TokenIdentification {
@Id
@GeneratedValue(generator = "custom")
@GenericGenerator(name = "custom", type = IdGenerator.class)
private String tokenId;
private String username;
private String deviceId;
private TokenType tokenType;
private String jti;
private LocalDateTime expiration;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.devb.estores.repository;

import com.devb.estores.enums.TokenType;
import com.devb.estores.model.TokenIdentification;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

public interface TokenIdentificationRepo extends JpaRepository<TokenIdentification, String> {

Optional<TokenIdentification> findByUsernameAndDeviceIdAndTokenType(String username, String deviceId, TokenType tokenType);
Page<TokenIdentification> findByExpirationBefore(LocalDateTime now, Pageable pageable);

void deleteByUsernameAndDeviceIdAndTokenType(String username, String deviceId, TokenType tokenType);

List<TokenIdentification> findAllByUsernameAndDeviceIdNot(String username, String currentDeviceId);

List<TokenIdentification> findAllByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package com.devb.estores.security;

import com.devb.estores.config.AppEnv;
import com.devb.estores.exceptions.InvalidJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.security.Key;
import java.util.Date;
import java.util.List;
import java.util.function.Supplier;


@Slf4j
Expand Down Expand Up @@ -49,11 +53,23 @@ private Key getSignatureKey() {
// parsing JWT

public Claims extractClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignatureKey())
.build()
.parseClaimsJws(token)
.getBody();
try {
return Jwts.parserBuilder()
.setSigningKey(getSignatureKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException | IllegalArgumentException e){
// Returning null to handle as per requirement
return null;
}
}

public Claims extractClaimsOrThrow(String token) throws InvalidJwtException {
Claims claims = extractClaims(token);
if(claims == null)
throw new InvalidJwtException("Failed to parse the token");
return claims;
}

public String getUsername(Claims claims) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.devb.estores.security;

import com.devb.estores.util.SimpleResponseStructure;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;

import java.io.IOException;

public class RequestUtils {

public static final String SEC_CH_UA = "sec-ch-ua";
public static final String SEC_CH_UA_PLATFORM = "sec-ch-ua-platform";
public static final String SEC_CH_UA_MOBILE = "sec-ch-ua-mobile";
public static final String USER_AGENT = "user-agent";

public static String extractCookie(String cookieName, Cookie[] cookies) {
String cookieValue = null;
if (cookies != null)
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
cookieValue = cookie.getValue();
break;
}
}
return cookieValue;
}

public static void handleException(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("Application/json");
response.setHeader("error", message);
SimpleResponseStructure structure = SimpleResponseStructure.builder()
.status(HttpStatus.UNAUTHORIZED.value())
.message(message)
.build();
new ObjectMapper().writeValue(response.getOutputStream(), structure);
}

public static String extractDeviceId(Cookie[] cookies) {
String did = "";
if (cookies != null)
for (Cookie cookie : cookies) {
if (cookie.getName().equals("did")) {
did = cookie.getValue();
break;
}
}
return did;
}

/**
* Helps in extracting the browser name from the given secChUa
*/
public static String extractBrowserName(String secChUa) {
if (secChUa != null) {
int start = secChUa.indexOf('"') + 1;
int end = secChUa.indexOf("\"", start);
return secChUa.substring(start, end);
} else
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.devb.estores.security;

import com.devb.estores.securityfilters.AuthFilter;
import com.devb.estores.securityfilters.JwtAuthenticationHelper;
import com.devb.estores.securityfilters.LoginFilter;
import com.devb.estores.securityfilters.RefreshFilter;
import lombok.AllArgsConstructor;
Expand All @@ -27,8 +28,8 @@
@AllArgsConstructor
public class SecurityConfig {

private final JwtService jwtService;
private final CorsConfigurationSource corsSource;
private final JwtAuthenticationHelper authenticationHelper;

@Bean
PasswordEncoder passwordEncoder() {
Expand Down Expand Up @@ -97,7 +98,7 @@ SecurityFilterChain refreshTokenFilterChain(HttpSecurity httpSecurity) throws Ex
.securityMatchers(matcher -> matcher.requestMatchers("/api/fkv1/refresh/**"))
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new RefreshFilter(jwtService), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new RefreshFilter( authenticationHelper), UsernamePasswordAuthenticationFilter.class)
.build();
}

Expand All @@ -110,7 +111,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Except
.securityMatchers(matcher -> matcher.requestMatchers("/api/fkv1/**"))
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new AuthFilter(jwtService), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new AuthFilter( authenticationHelper), UsernamePasswordAuthenticationFilter.class)
.build();
}

Expand Down
Loading

0 comments on commit 81b36b0

Please sign in to comment.