WIP: feat(security): implement refresh tokens #49

Closed
dylan wants to merge 25 commits from feat/add-refresh-tokens into master
6 changed files with 117 additions and 5 deletions
Showing only changes of commit cc3765c284 - Show all commits

View File

@@ -0,0 +1,7 @@
package com.ddf.vodsystem.dto;
public record TokenPackage (
String refreshToken,
String accessToken
) {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View 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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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}