package com.ddf.vodsystem.services; import com.ddf.vodsystem.dto.TokenPackage; import com.ddf.vodsystem.entities.RefreshToken; 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; import com.google.api.client.json.gson.GsonFactory; import org.springframework.transaction.annotation.Transactional; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import org.springframework.stereotype.Service; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.time.Duration; import java.time.Instant; import java.util.Collections; import java.util.HexFormat; import java.util.Optional; @Service public class UserService { private final GoogleIdTokenVerifier verifier; private final UserRepository userRepository; private final JwtService jwtService; @Value("token-family.expiration") private long tokenFamilyExpirationMs; @Value("refresh-token.expiration") private long refreshTokenExpirationMs; private static final SecureRandom SECURE_RANDOM = new SecureRandom(); private static final int REFRESH_GRACE_PERIOD_SECONDS = 5; private final TokenService tokenService; public UserService(UserRepository userRepository, JwtService jwtService, @Value("${google.client.id}") String googleClientId, TokenService tokenService) { this.userRepository = userRepository; NetHttpTransport transport = new NetHttpTransport(); JsonFactory jsonFactory = new GsonFactory(); this.verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory) .setAudience(Collections.singletonList(googleClientId)) .build(); this.jwtService = jwtService; this.tokenService = tokenService; } /** * Looks up a user by their database ID. * * @param userId the ID of the user to find * @return the matching {@link User}, or empty if none exists */ public Optional getUserById(Long userId) { return userRepository.findById(userId); } /** * Returns the user backing the current Spring Security authentication, if authenticated. * * @return the authenticated {@link User}, or empty if no user is authenticated */ public Optional getLoggedInUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof User user) { return Optional.of(user); } return Optional.empty(); } /** * Verifies a Google ID token, creating or updating the corresponding user, and issues a session JWT. * * @param idToken the Google ID token to verify * @return a signed JWT for the authenticated user * @throws NotAuthenticated if the token is invalid or its subject is missing */ @Transactional public TokenPackage login(String idToken) { GoogleIdToken googleIdToken = getGoogleIdToken(idToken); String googleId = googleIdToken.getPayload().getSubject(); if (googleId == null) { throw new NotAuthenticated("Invalid ID token"); } User googleUser = getGoogleUser(googleIdToken); User user = createOrUpdateUser(googleUser); byte[] rawRefresh = generateRandomBytes(32); TokenFamily tokenFamily = tokenService.createTokenFamily(user, tokenFamilyExpirationMs); tokenService.createRefreshToken(tokenService.hashToken(rawRefresh), tokenFamily, refreshTokenExpirationMs); String jwt = jwtService.generateToken(user.getId()); return new TokenPackage(tokenService.bytesToHex(rawRefresh), jwt); } @Transactional(noRollbackFor = NotAuthenticated.class) public TokenPackage refresh(String refreshToken) { if (refreshToken == null || refreshToken.isBlank()) { throw new NotAuthenticated("Missing refresh token"); } // Check Refresh Token is real byte[] rawToken = tokenService.hexToBytes(refreshToken); RefreshToken token = tokenService.getRefreshTokenByHash(rawToken) .orElseThrow(() -> new NotAuthenticated("No such refresh token")); // Find Token Family Optional tokenFamily = tokenService.getFamilyByRefreshToken(token); if (tokenFamily.isEmpty()) { throw new IllegalStateException("Token must be tied to a family"); } if (tokenFamily.get().getRevokedAt() != null) { throw new NotAuthenticated("Token family has been revoked"); } Instant now = Instant.now(); // Reuse Check if (token.getUsedAt() != null) { if (Duration.between(token.getUsedAt(), now).getSeconds() > REFRESH_GRACE_PERIOD_SECONDS) { tokenService.revokeTokenFamily(tokenFamily.get()); } throw new NotAuthenticated("Refresh token already used"); } // Expiry Check if (now.isAfter(token.getExpiresAt()) || now.isAfter(tokenFamily.get().getExpiresAt())) { throw new NotAuthenticated("Refresh token or family expired"); } // Rotate token.setUsedAt(now); byte[] newRawToken = generateRandomBytes(32); tokenService.createRefreshToken( newRawToken, tokenFamily.get(), refreshTokenExpirationMs ); String jwt = jwtService.generateToken(tokenFamily.get().getUser().getId()); return new TokenPackage(tokenService.bytesToHex(newRawToken), jwt); } /** * Looks up a user by their stream key. * * @param streamKey the stream key to match * @return the matching {@link User}, or empty if none exists */ public Optional getUserByStreamKey(String streamKey) { return userRepository.findByStreamKey(streamKey); } private User createOrUpdateUser(User user) { Optional existingUser = userRepository.findByGoogleId(user.getGoogleId()); if (existingUser.isEmpty()) { user.setRole(0); user.setCreatedAt(Instant.now()); user.setStreamKey(HexFormat.of().formatHex(generateRandomBytes(24))); return userRepository.saveAndFlush(user); } User existing = existingUser.get(); existing.setEmail(user.getEmail()); existing.setName(user.getName()); existing.setProfilePictureUrl(user.getProfilePictureUrl()); existing.setUsername(user.getUsername()); return userRepository.saveAndFlush(existing); } private User getGoogleUser(GoogleIdToken idToken) { String googleId = idToken.getPayload().getSubject(); String email = idToken.getPayload().getEmail(); String name = (String) idToken.getPayload().get("name"); String profilePictureUrl = (String) idToken.getPayload().get("picture"); User user = new User(); user.setGoogleId(googleId); user.setEmail(email); user.setName(name); user.setUsername(email); user.setProfilePictureUrl(profilePictureUrl); return user; } private GoogleIdToken getGoogleIdToken(String idToken) { try { return verifier.verify(idToken); } catch (GeneralSecurityException | IOException e) { throw new NotAuthenticated("Invalid ID token: " + e.getMessage()); } } private byte[] generateRandomBytes(int length) { byte[] bytes = new byte[length]; SECURE_RANDOM.nextBytes(bytes); return bytes; } }