chore: migrate to UUIDv7
Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
@@ -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 {
|
||||
|
||||
+13
-4
@@ -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)
|
||||
|
||||
+2
-1
@@ -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?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user