1
0

chore: migrate to UUIDv7

Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
2026-03-26 07:24:32 -04:00
parent 272340ee6c
commit e6cc8267d5
9 changed files with 87 additions and 12 deletions
@@ -4,10 +4,13 @@ import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import io.visus.demos.kotlinapi.converters.BinaryToUuidConverter
import io.visus.demos.kotlinapi.converters.UuidToBinaryConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.mongodb.config.EnableMongoAuditing
import org.springframework.data.mongodb.core.convert.MongoCustomConversions
import java.util.concurrent.TimeUnit
@Configuration
@@ -33,4 +36,13 @@ class MongoConfig {
return MongoClients.create(settings)
}
@Bean
fun mongoCustomConversions(): MongoCustomConversions =
MongoCustomConversions(
listOf(
UuidToBinaryConverter(),
BinaryToUuidConverter(),
),
)
}
@@ -0,0 +1,33 @@
package io.visus.demos.kotlinapi.converters
import org.bson.BsonBinarySubType
import org.bson.types.Binary
import org.springframework.core.convert.converter.Converter
import org.springframework.data.convert.ReadingConverter
import org.springframework.data.convert.WritingConverter
import java.nio.ByteBuffer
import java.util.UUID
@WritingConverter
class UuidToBinaryConverter : Converter<UUID, Binary> {
override fun convert(source: UUID): Binary {
val buffer = ByteBuffer.wrap(ByteArray(16))
buffer.putLong(source.mostSignificantBits)
buffer.putLong(source.leastSignificantBits)
return Binary(BsonBinarySubType.UUID_STANDARD, buffer.array())
}
}
@ReadingConverter
class BinaryToUuidConverter : Converter<Binary, UUID> {
override fun convert(source: Binary): UUID {
val buffer = ByteBuffer.wrap(source.data)
val high = buffer.long
val low = buffer.long
return UUID(high, low)
}
}
@@ -8,6 +8,7 @@ import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant
import java.util.Date
import java.util.UUID
@Document(collection = "refresh_tokens")
data class RefreshToken(
@@ -21,7 +22,7 @@ data class RefreshToken(
val expiresAt: Date,
var isRevoked: Boolean = false,
@Indexed
val familyId: String,
val familyId: UUID,
@CreatedDate
val createdAt: Instant? = null,
@LastModifiedDate
@@ -0,0 +1,17 @@
package io.visus.demos.kotlinapi.domain.model
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.mapping.Document
import java.util.Date
@Document(collection = "revoked_access_tokens")
data class RevokedToken(
@Id
val id: String? = null,
@Indexed(unique = true)
val jti: String,
val userId: String,
@Indexed(expireAfter = "0s")
val expiresAt: Date,
)
@@ -1,5 +1,6 @@
package io.visus.demos.kotlinapi.domain.service
import com.fasterxml.uuid.Generators
import io.visus.demos.kotlinapi.api.contracts.AuthResponse
import io.visus.demos.kotlinapi.api.contracts.RegisterResponse
import io.visus.demos.kotlinapi.config.JwtProperties
@@ -96,11 +97,11 @@ class AuthServiceImpl(
private suspend fun generateAuthResponse(
user: User,
newFamily: Boolean = false,
existingFamilyId: String? = null,
existingFamilyId: UUID? = null,
): AuthResponse {
val accessToken = jwtTokenProvider.generateAccessToken(user)
val familyId = if (newFamily) UUID.randomUUID().toString() else existingFamilyId!!
val familyId = if (newFamily) Generators.timeBasedEpochRandomGenerator().generate() else existingFamilyId!!
val refreshToken =
refreshTokenService.createRefreshToken(
userId = user.id!!,
@@ -1,11 +1,12 @@
package io.visus.demos.kotlinapi.domain.service
import io.visus.demos.kotlinapi.domain.model.RefreshToken
import java.util.UUID
interface RefreshTokenService {
suspend fun createRefreshToken(
userId: String,
familyId: String,
familyId: UUID,
): String
suspend fun getActiveSessionCount(userId: String): Long
@@ -16,7 +17,7 @@ interface RefreshTokenService {
suspend fun revokeAllUserTokens(userId: String)
suspend fun revokeTokenFamily(familyId: String)
suspend fun revokeTokenFamily(familyId: UUID)
suspend fun validateAndRotate(
userId: String,
@@ -31,7 +31,7 @@ class RefreshTokenServiceImpl(
override suspend fun createRefreshToken(
userId: String,
familyId: String,
familyId: UUID,
): String {
val activeCount = refreshTokenRepository.countActive(ObjectId(userId))
@@ -88,7 +88,7 @@ class RefreshTokenServiceImpl(
},
)
override suspend fun revokeTokenFamily(familyId: String) {
override suspend fun revokeTokenFamily(familyId: UUID) {
val tokens = refreshTokenRepository.findByFamilyId(familyId)
tokens.forEach {
@@ -1,5 +1,6 @@
package io.visus.demos.kotlinapi.infrastructure.security
import com.fasterxml.uuid.Generators
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
@@ -44,6 +45,8 @@ class JwtTokenProvider(
fun extractExpiration(token: String): Date = extractClaim(token, Claims::getExpiration)
fun extractJti(token: String): String? = extractClaim(token, Claims::getId)
fun extractUsername(token: String): String = extractClaim(token, Claims::getSubject)
fun extractTokenType(token: String): JwtTokenType? =
@@ -61,7 +64,12 @@ class JwtTokenProvider(
.subject(userDetails.username)
.claim("type", JwtTokenType.ACCESS.value)
.claim("authorities", userDetails.authorities.map { it.authority })
.issuedAt(now)
.id(
Generators
.timeBasedEpochGenerator()
.generate()
.toString(), // UUIDv7
).issuedAt(now)
.expiration(expiry)
.issuer(jwtProperties.issuer)
.signWith(secretKey, Jwts.SIG.HS512)
@@ -77,9 +85,10 @@ class JwtTokenProvider(
.subject(userDetails.username)
.claim("type", JwtTokenType.REFRESH.value)
.id(
java.util.UUID
.randomUUID()
.toString(),
Generators
.timeBasedEpochGenerator()
.generate()
.toString(), // UUIDv7
).issuedAt(now)
.expiration(expiry)
.signWith(secretKey, Jwts.SIG.HS512)
@@ -6,12 +6,13 @@ import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.Update
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import java.time.Instant
import java.util.UUID
interface RefreshTokenRepository : CoroutineCrudRepository<RefreshToken, String> {
@Query(value = "{ 'userId': ?0, 'isRevoked': false }", count = true)
suspend fun countActive(userId: ObjectId): Long
suspend fun findByFamilyId(familyId: String): List<RefreshToken>
suspend fun findByFamilyId(familyId: UUID): List<RefreshToken>
suspend fun findByTokenHash(tokenHash: String): RefreshToken?