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.MongoClientSettings
|
||||||
import com.mongodb.client.MongoClient
|
import com.mongodb.client.MongoClient
|
||||||
import com.mongodb.client.MongoClients
|
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.beans.factory.annotation.Value
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.data.mongodb.config.EnableMongoAuditing
|
import org.springframework.data.mongodb.config.EnableMongoAuditing
|
||||||
|
import org.springframework.data.mongodb.core.convert.MongoCustomConversions
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -33,4 +36,13 @@ class MongoConfig {
|
|||||||
|
|
||||||
return MongoClients.create(settings)
|
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 org.springframework.data.mongodb.core.mapping.Document
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Document(collection = "refresh_tokens")
|
@Document(collection = "refresh_tokens")
|
||||||
data class RefreshToken(
|
data class RefreshToken(
|
||||||
@@ -21,7 +22,7 @@ data class RefreshToken(
|
|||||||
val expiresAt: Date,
|
val expiresAt: Date,
|
||||||
var isRevoked: Boolean = false,
|
var isRevoked: Boolean = false,
|
||||||
@Indexed
|
@Indexed
|
||||||
val familyId: String,
|
val familyId: UUID,
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
val createdAt: Instant? = null,
|
val createdAt: Instant? = null,
|
||||||
@LastModifiedDate
|
@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
|
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.AuthResponse
|
||||||
import io.visus.demos.kotlinapi.api.contracts.RegisterResponse
|
import io.visus.demos.kotlinapi.api.contracts.RegisterResponse
|
||||||
import io.visus.demos.kotlinapi.config.JwtProperties
|
import io.visus.demos.kotlinapi.config.JwtProperties
|
||||||
@@ -96,11 +97,11 @@ class AuthServiceImpl(
|
|||||||
private suspend fun generateAuthResponse(
|
private suspend fun generateAuthResponse(
|
||||||
user: User,
|
user: User,
|
||||||
newFamily: Boolean = false,
|
newFamily: Boolean = false,
|
||||||
existingFamilyId: String? = null,
|
existingFamilyId: UUID? = null,
|
||||||
): AuthResponse {
|
): AuthResponse {
|
||||||
val accessToken = jwtTokenProvider.generateAccessToken(user)
|
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 =
|
val refreshToken =
|
||||||
refreshTokenService.createRefreshToken(
|
refreshTokenService.createRefreshToken(
|
||||||
userId = user.id!!,
|
userId = user.id!!,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package io.visus.demos.kotlinapi.domain.service
|
package io.visus.demos.kotlinapi.domain.service
|
||||||
|
|
||||||
import io.visus.demos.kotlinapi.domain.model.RefreshToken
|
import io.visus.demos.kotlinapi.domain.model.RefreshToken
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
interface RefreshTokenService {
|
interface RefreshTokenService {
|
||||||
suspend fun createRefreshToken(
|
suspend fun createRefreshToken(
|
||||||
userId: String,
|
userId: String,
|
||||||
familyId: String,
|
familyId: UUID,
|
||||||
): String
|
): String
|
||||||
|
|
||||||
suspend fun getActiveSessionCount(userId: String): Long
|
suspend fun getActiveSessionCount(userId: String): Long
|
||||||
@@ -16,7 +17,7 @@ interface RefreshTokenService {
|
|||||||
|
|
||||||
suspend fun revokeAllUserTokens(userId: String)
|
suspend fun revokeAllUserTokens(userId: String)
|
||||||
|
|
||||||
suspend fun revokeTokenFamily(familyId: String)
|
suspend fun revokeTokenFamily(familyId: UUID)
|
||||||
|
|
||||||
suspend fun validateAndRotate(
|
suspend fun validateAndRotate(
|
||||||
userId: String,
|
userId: String,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class RefreshTokenServiceImpl(
|
|||||||
|
|
||||||
override suspend fun createRefreshToken(
|
override suspend fun createRefreshToken(
|
||||||
userId: String,
|
userId: String,
|
||||||
familyId: String,
|
familyId: UUID,
|
||||||
): String {
|
): String {
|
||||||
val activeCount = refreshTokenRepository.countActive(ObjectId(userId))
|
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)
|
val tokens = refreshTokenRepository.findByFamilyId(familyId)
|
||||||
|
|
||||||
tokens.forEach {
|
tokens.forEach {
|
||||||
|
|||||||
+13
-4
@@ -1,5 +1,6 @@
|
|||||||
package io.visus.demos.kotlinapi.infrastructure.security
|
package io.visus.demos.kotlinapi.infrastructure.security
|
||||||
|
|
||||||
|
import com.fasterxml.uuid.Generators
|
||||||
import io.jsonwebtoken.Claims
|
import io.jsonwebtoken.Claims
|
||||||
import io.jsonwebtoken.ExpiredJwtException
|
import io.jsonwebtoken.ExpiredJwtException
|
||||||
import io.jsonwebtoken.Jwts
|
import io.jsonwebtoken.Jwts
|
||||||
@@ -44,6 +45,8 @@ class JwtTokenProvider(
|
|||||||
|
|
||||||
fun extractExpiration(token: String): Date = extractClaim(token, Claims::getExpiration)
|
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 extractUsername(token: String): String = extractClaim(token, Claims::getSubject)
|
||||||
|
|
||||||
fun extractTokenType(token: String): JwtTokenType? =
|
fun extractTokenType(token: String): JwtTokenType? =
|
||||||
@@ -61,7 +64,12 @@ class JwtTokenProvider(
|
|||||||
.subject(userDetails.username)
|
.subject(userDetails.username)
|
||||||
.claim("type", JwtTokenType.ACCESS.value)
|
.claim("type", JwtTokenType.ACCESS.value)
|
||||||
.claim("authorities", userDetails.authorities.map { it.authority })
|
.claim("authorities", userDetails.authorities.map { it.authority })
|
||||||
.issuedAt(now)
|
.id(
|
||||||
|
Generators
|
||||||
|
.timeBasedEpochGenerator()
|
||||||
|
.generate()
|
||||||
|
.toString(), // UUIDv7
|
||||||
|
).issuedAt(now)
|
||||||
.expiration(expiry)
|
.expiration(expiry)
|
||||||
.issuer(jwtProperties.issuer)
|
.issuer(jwtProperties.issuer)
|
||||||
.signWith(secretKey, Jwts.SIG.HS512)
|
.signWith(secretKey, Jwts.SIG.HS512)
|
||||||
@@ -77,9 +85,10 @@ class JwtTokenProvider(
|
|||||||
.subject(userDetails.username)
|
.subject(userDetails.username)
|
||||||
.claim("type", JwtTokenType.REFRESH.value)
|
.claim("type", JwtTokenType.REFRESH.value)
|
||||||
.id(
|
.id(
|
||||||
java.util.UUID
|
Generators
|
||||||
.randomUUID()
|
.timeBasedEpochGenerator()
|
||||||
.toString(),
|
.generate()
|
||||||
|
.toString(), // UUIDv7
|
||||||
).issuedAt(now)
|
).issuedAt(now)
|
||||||
.expiration(expiry)
|
.expiration(expiry)
|
||||||
.signWith(secretKey, Jwts.SIG.HS512)
|
.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.mongodb.repository.Update
|
||||||
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
|
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
interface RefreshTokenRepository : CoroutineCrudRepository<RefreshToken, String> {
|
interface RefreshTokenRepository : CoroutineCrudRepository<RefreshToken, String> {
|
||||||
@Query(value = "{ 'userId': ?0, 'isRevoked': false }", count = true)
|
@Query(value = "{ 'userId': ?0, 'isRevoked': false }", count = true)
|
||||||
suspend fun countActive(userId: ObjectId): Long
|
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?
|
suspend fun findByTokenHash(tokenHash: String): RefreshToken?
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user