WIP: feat(security): implement refresh tokens #49
7
src/main/java/com/ddf/vodsystem/dto/TokenPackage.java
Normal file
7
src/main/java/com/ddf/vodsystem/dto/TokenPackage.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.ddf.vodsystem.dto;
|
||||
|
||||
public record TokenPackage (
|
||||
String refreshToken,
|
||||
String accessToken
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ddf.vodsystem.repositories;
|
||||
|
||||
import com.ddf.vodsystem.entities.RefreshToken;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ddf.vodsystem.repositories;
|
||||
|
||||
import com.ddf.vodsystem.entities.TokenFamily;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface TokenFamilyRepository extends JpaRepository<TokenFamily, Long> {
|
||||
}
|
||||
63
src/main/java/com/ddf/vodsystem/security/TokenService.java
Normal file
63
src/main/java/com/ddf/vodsystem/security/TokenService.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package com.ddf.vodsystem.security;
|
||||
|
||||
import com.ddf.vodsystem.entities.RefreshToken;
|
||||
import com.ddf.vodsystem.entities.TokenFamily;
|
||||
import com.ddf.vodsystem.entities.User;
|
||||
import com.ddf.vodsystem.repositories.RefreshTokenRepository;
|
||||
import com.ddf.vodsystem.repositories.TokenFamilyRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
|
||||
@Service
|
||||
public class TokenService {
|
||||
private final TokenFamilyRepository tokenFamilyRepository;
|
||||
private final RefreshTokenRepository refreshTokenRepository;
|
||||
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
|
||||
public TokenService (
|
||||
TokenFamilyRepository tokenFamilyRepository,
|
||||
RefreshTokenRepository refreshTokenRepository
|
||||
) {
|
||||
this.tokenFamilyRepository = tokenFamilyRepository;
|
||||
this.refreshTokenRepository = refreshTokenRepository;
|
||||
}
|
||||
|
||||
public TokenFamily createTokenFamily(User user, long expiryTimeMs) {
|
||||
Instant now = Instant.now();
|
||||
|
||||
TokenFamily tokenFamily = new TokenFamily();
|
||||
tokenFamily.setUser(user);
|
||||
tokenFamily.setCreatedAt(now);
|
||||
tokenFamily.setExpiresAt(now.plusMillis(expiryTimeMs));
|
||||
|
||||
return tokenFamilyRepository.saveAndFlush(tokenFamily);
|
||||
}
|
||||
|
||||
public RefreshToken getRefreshToken(
|
||||
byte[] token,
|
||||
TokenFamily tokenFamily,
|
||||
long expiryTimeMs
|
||||
) {
|
||||
Instant now = Instant.now();
|
||||
|
||||
RefreshToken refreshToken = new RefreshToken();
|
||||
refreshToken.setTokenFamily(tokenFamily);
|
||||
refreshToken.setTokenHash(hashToken(token));
|
||||
refreshToken.setCreatedAt(now);
|
||||
refreshToken.setExpiresAt(now.plusMillis(expiryTimeMs));
|
||||
return refreshTokenRepository.saveAndFlush(refreshToken);
|
||||
}
|
||||
|
||||
private static byte[] hashToken(byte[] token) {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256").digest(token); // 32 bytes
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.ddf.vodsystem.services;
|
||||
|
||||
import com.ddf.vodsystem.dto.TokenPackage;
|
||||
import com.ddf.vodsystem.entities.TokenFamily;
|
||||
import com.ddf.vodsystem.entities.User;
|
||||
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
||||
import com.ddf.vodsystem.repositories.UserRepository;
|
||||
import com.ddf.vodsystem.security.JwtService;
|
||||
import com.ddf.vodsystem.security.TokenService;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
||||
import com.google.api.client.http.javanet.NetHttpTransport;
|
||||
import com.google.api.client.json.JsonFactory;
|
||||
@@ -20,6 +23,7 @@ import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HexFormat;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@@ -29,10 +33,12 @@ public class UserService {
|
||||
private final JwtService jwtService;
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
private final TokenService tokenService;
|
||||
|
||||
public UserService(UserRepository userRepository,
|
||||
JwtService jwtService,
|
||||
@Value("${google.client.id}") String googleClientId) {
|
||||
@Value("${google.client.id}") String googleClientId,
|
||||
TokenService tokenService) {
|
||||
this.userRepository = userRepository;
|
||||
|
||||
NetHttpTransport transport = new NetHttpTransport();
|
||||
@@ -42,6 +48,7 @@ public class UserService {
|
||||
.setAudience(Collections.singletonList(googleClientId))
|
||||
.build();
|
||||
this.jwtService = jwtService;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,9 +92,20 @@ public class UserService {
|
||||
User googleUser = getGoogleUser(googleIdToken);
|
||||
User user = createOrUpdateUser(googleUser);
|
||||
|
||||
TokenFamily tokenFamily = tokenService.createTokenFamily(user, )
|
||||
String jwt = jwtService.generateToken(user.getId());
|
||||
|
||||
|
||||
return jwtService.generateToken(user.getId());
|
||||
}
|
||||
|
||||
public TokenPackage refresh(String refreshToken) {
|
||||
if (refreshToken == null) {
|
||||
throw new NotAuthenticated("Missing refresh token");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a user by their stream key.
|
||||
*
|
||||
@@ -104,7 +122,7 @@ public class UserService {
|
||||
if (existingUser.isEmpty()) {
|
||||
user.setRole(0);
|
||||
user.setCreatedAt(Instant.now());
|
||||
user.setStreamKey(generateStreamKey());
|
||||
user.setStreamKey(HexFormat.of().formatHex(generateRandomBytes(24)));
|
||||
return userRepository.saveAndFlush(user);
|
||||
}
|
||||
|
||||
@@ -140,9 +158,9 @@ public class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
private String generateStreamKey() {
|
||||
byte[] bytes = new byte[24];
|
||||
private byte[] generateRandomBytes(int length) {
|
||||
byte[] bytes = new byte[length];
|
||||
SECURE_RANDOM.nextBytes(bytes);
|
||||
return HexFormat.of().formatHex(bytes);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
|
||||
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
|
||||
spring.security.oauth2.client.registration.google.scope=profile,email
|
||||
jwt.secret.key=${JWT_SECRET_KEY}
|
||||
# 1 Hour
|
||||
jwt.expiration=3600000
|
||||
# 7 days
|
||||
refresh-token.expiration=604800000
|
||||
# 90 days
|
||||
token-family.expiration=7776000000
|
||||
|
||||
# Frontend
|
||||
frontend.url=${FRONTEND_URL}
|
||||
|
||||
Reference in New Issue
Block a user