Skip to content
Closed
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
@@ -0,0 +1,31 @@
package com.smartjam.smartjamapi.entity;

import java.time.Instant;

import jakarta.persistence.*;

import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name = "access_tokens")
@Setter
@Getter
public class AccessTokenEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String jti;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private UserEntity user;

@Column(nullable = false, name = "expires_at")
private Instant expiresAt;

@Column(nullable = false)
private boolean revoked = false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.smartjam.smartjamapi.repository;

import java.util.Optional;

import com.smartjam.smartjamapi.entity.AccessTokenEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AccessTokenRepository extends JpaRepository<AccessTokenEntity, Long> {
Optional<AccessTokenEntity> findByJti(String jti);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.smartjam.smartjamapi.security;

import java.security.Key;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import javax.crypto.SecretKey;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;

@Service
public class JwtService {

@Value("${security.jwt.secret-key}")
private String secretKey;

@Getter
@Value("${security.jwt.expiration-time}")
private long jwtExpiration;

private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}

public String generateRefreshToken() {
byte[] randomBytes = new byte[64];
new SecureRandom().nextBytes(randomBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}

public String generateAccessToken(UserDetailsImpl userDetails, String jti) {
Map<String, Object> claims = new HashMap<>();
claims.put(
"authorities",
userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList());

return Jwts.builder()
.claims(claims)
.subject(userDetails.getEmail())
.id(jti)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSigningKey())
.compact();
}

public String generateJti() {
return UUID.randomUUID().toString();
}

public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

public String extractJti(String token) {
return extractClaim(token, Claims::getId);
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith((SecretKey) getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}

public boolean isTokenValid(String token, UserDetailsImpl userDetails) {
final String emailFromToken = extractUsername(token);
return emailFromToken.equals(userDetails.getEmail()) && !isTokenExpired(token);
}

private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.smartjam.smartjamapi.service;

import java.time.Instant;
import java.util.NoSuchElementException;

import jakarta.transaction.Transactional;

import com.smartjam.smartjamapi.dto.AuthResponse;
import com.smartjam.smartjamapi.dto.LoginRequest;
import com.smartjam.smartjamapi.dto.RegisterRequest;
import com.smartjam.smartjamapi.dto.TokenDto;
import com.smartjam.smartjamapi.entity.AccessTokenEntity;
import com.smartjam.smartjamapi.entity.RefreshTokenEntity;
import com.smartjam.smartjamapi.entity.UserEntity;
import com.smartjam.smartjamapi.enums.AvailabilityStatus;
import com.smartjam.smartjamapi.enums.Role;
import com.smartjam.smartjamapi.repository.AccessTokenRepository;
import com.smartjam.smartjamapi.repository.RefreshTokenRepository;
import com.smartjam.smartjamapi.repository.UserRepository;
import com.smartjam.smartjamapi.security.JwtService;
import com.smartjam.smartjamapi.security.UserDetailsImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

private final UserRepository repository;
private final RefreshTokenRepository refreshTokenRepository;
private final AccessTokenRepository accessTokenRepository;

private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;

private static final Logger log = LoggerFactory.getLogger(AuthService.class);

public AuthService(
UserRepository repository,
RefreshTokenRepository refreshTokenRepository,
AccessTokenRepository accessTokenRepository,
PasswordEncoder passwordEncoder,
AuthenticationManager authenticationManager,
JwtService jwtService,
UserDetailsService userDetailsService) {
this.repository = repository;
this.refreshTokenRepository = refreshTokenRepository;
this.accessTokenRepository = accessTokenRepository;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}

public AuthResponse login(LoginRequest request) {
UserEntity userEntity = repository
.findByEmail(request.email())
.orElseThrow(() -> new NoSuchElementException("Login not found, try register, please"));
if (!passwordEncoder.matches(request.password(), userEntity.getPasswordHash())) {
throw new IllegalStateException("Invalid password");
}

UserDetailsImpl userDetails = UserDetailsImpl.build(userEntity);

String jti = jwtService.generateJti();
String accessToken = jwtService.generateAccessToken(userDetails, jti);
String refreshToken = jwtService.generateRefreshToken();

AccessTokenEntity accessTokenEntity = new AccessTokenEntity();
accessTokenEntity.setJti(jti);
accessTokenEntity.setUser(userEntity);
accessTokenEntity.setExpiresAt(Instant.now().plusMillis(jwtService.getJwtExpiration()));
accessTokenEntity.setRevoked(false);

accessTokenRepository.save(accessTokenEntity);

RefreshTokenEntity refreshTokenEntity = new RefreshTokenEntity();
refreshTokenEntity.setToken(refreshToken);
refreshTokenEntity.setUser(userEntity);
refreshTokenEntity.setExpiresAt(Instant.now().plusMillis(jwtService.getJwtExpiration()));

refreshTokenRepository.save(refreshTokenEntity);

return new AuthResponse("Logged in successfully", AvailabilityStatus.AVAILABLE, refreshToken, accessToken);
}

public AuthResponse register(RegisterRequest request) {
boolean exists = repository.findByEmail(request.email()).isPresent();

if (exists) {
throw new IllegalStateException("The account exists, try login, please");
}

UserEntity userEntity = new UserEntity();
userEntity.setUsername(request.username());
userEntity.setEmail(request.email());
userEntity.setPasswordHash(passwordEncoder.encode(request.password()));
userEntity.setRole(Role.STUDENT);

repository.save(userEntity);

UserDetailsImpl userDetails = UserDetailsImpl.build(userEntity);

String jti = jwtService.generateJti();
String accessToken = jwtService.generateAccessToken(userDetails, jti);
String refreshToken = jwtService.generateRefreshToken();

AccessTokenEntity accessTokenEntity = new AccessTokenEntity();
accessTokenEntity.setJti(jti);
accessTokenEntity.setUser(userEntity);
accessTokenEntity.setExpiresAt(Instant.now().plusMillis(jwtService.getJwtExpiration()));
accessTokenEntity.setRevoked(false);

accessTokenRepository.save(accessTokenEntity);

RefreshTokenEntity refreshTokenEntity = new RefreshTokenEntity();
refreshTokenEntity.setToken(refreshToken);
refreshTokenEntity.setUser(userEntity);
refreshTokenEntity.setExpiresAt(Instant.now().plusMillis(jwtService.getJwtExpiration()));

refreshTokenRepository.save(refreshTokenEntity);

return new AuthResponse("Register successfully", AvailabilityStatus.AVAILABLE, refreshToken, accessToken);
}

@Transactional
public AuthResponse getNewToken(TokenDto tokenDto) {
RefreshTokenEntity refreshToken = refreshTokenRepository
.findByToken(tokenDto.refresh_token())
.orElseThrow(() -> new NoSuchElementException("Token not found, try login, please"));
if (refreshToken.getExpiresAt().isBefore(Instant.now())) {
throw new IllegalStateException("Refresh token expired");
}

UserEntity userEntity = repository
.findById(refreshToken.getUser().getId())
.orElseThrow(() -> new NoSuchElementException("User not found"));
UserDetailsImpl userDetails = UserDetailsImpl.build(userEntity);

String jti = jwtService.generateJti();
String accessToken = jwtService.generateAccessToken(userDetails, jti);
String newRefreshToken = jwtService.generateRefreshToken();

AccessTokenEntity accessTokenEntity = new AccessTokenEntity();
accessTokenEntity.setJti(jti);
accessTokenEntity.setUser(userEntity);
accessTokenEntity.setExpiresAt(Instant.now().plusMillis(jwtService.getJwtExpiration()));
accessTokenEntity.setRevoked(false);

accessTokenRepository.save(accessTokenEntity);

RefreshTokenEntity refreshTokenEntity = new RefreshTokenEntity();
refreshTokenEntity.setToken(newRefreshToken);
refreshTokenEntity.setUser(userEntity);
refreshTokenEntity.setExpiresAt(Instant.now().plusMillis(jwtService.getJwtExpiration()));

refreshTokenRepository.save(refreshTokenEntity);

return new AuthResponse(
"Token generate successfully", AvailabilityStatus.AVAILABLE, newRefreshToken, accessToken);
}
}
Loading