feat: swap auId in JWT for individual token

note: has not been tested to ensure there are no collisions, todo
This commit is contained in:
Raymond
2025-07-27 03:06:33 -04:00
committed by Azalea
parent 82adf5c138
commit 39ed8af840
4 changed files with 155 additions and 76 deletions

View File

@@ -34,6 +34,7 @@ class UserRegistrar(
val cardService: CardService, val cardService: CardService,
val validator: AquaUserServices, val validator: AquaUserServices,
val emailProps: EmailProperties, val emailProps: EmailProperties,
val sessionRepo: SessionTokenRepo,
final val paths: PathProps final val paths: PathProps
) { ) {
val portraitPath = paths.aquaNetPortrait.path() val portraitPath = paths.aquaNetPortrait.path()
@@ -233,6 +234,12 @@ class UserRegistrar(
// Save the user // Save the user
userRepo.save(u) userRepo.save(u)
// Clear all tokens if changing password
if (key == "pwHash")
sessionRepo.deleteAll(
sessionRepo.findByAquaNetUserAuId(u.auId)
)
} }
SUCCESS SUCCESS

View File

@@ -4,14 +4,18 @@ import ext.Str
import ext.minus import ext.minus
import icu.samnyan.aqua.net.db.AquaNetUser import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.db.AquaNetUserRepo import icu.samnyan.aqua.net.db.AquaNetUserRepo
import icu.samnyan.aqua.net.db.SessionToken
import icu.samnyan.aqua.net.db.SessionTokenRepo
import io.jsonwebtoken.JwtParser import io.jsonwebtoken.JwtParser
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.Keys
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import jakarta.transaction.Transactional
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant
import java.util.* import java.util.*
import javax.crypto.SecretKey import javax.crypto.SecretKey
@@ -24,7 +28,8 @@ class JWTProperties {
@Service @Service
class JWT( class JWT(
val props: JWTProperties, val props: JWTProperties,
val userRepo: AquaNetUserRepo val userRepo: AquaNetUserRepo,
val sessionRepo: SessionTokenRepo
) { ) {
val log = LoggerFactory.getLogger(JWT::class.java)!! val log = LoggerFactory.getLogger(JWT::class.java)!!
lateinit var key: SecretKey lateinit var key: SecretKey
@@ -55,20 +60,49 @@ class JWT(
log.info("JWT Service Enabled") log.info("JWT Service Enabled")
} }
@Transactional
fun gen(user: AquaNetUser): Str {
val activeTokens = sessionRepo.findByAquaNetUserAuId(user.auId)
.sortedByDescending { it.expiry }.drop(4) // the cap is 5, but we append a new token after the fact
if (activeTokens.isNotEmpty()) {
sessionRepo.deleteAll(activeTokens)
}
val token = SessionToken().apply {
aquaNetUser = user
}
sessionRepo.save(token)
fun gen(user: AquaNetUser): Str = Jwts.builder().header() return Jwts.builder().header()
.keyId("aqua-net") .keyId("aqua-net")
.and() .and()
.subject(user.auId.toString()) .subject(token.token)
.issuedAt(Date()) .issuedAt(Date())
.signWith(key) .signWith(key)
.compact() .compact()
}
fun parse(token: Str): AquaNetUser? = try { @Transactional
userRepo.findByAuId(parser.parseSignedClaims(token).payload.subject.toLong()) fun parse(token: Str): AquaNetUser? {
try {
val uuid = parser.parseSignedClaims(token).payload.subject.toString()
val token = sessionRepo.findByToken(uuid)
if (token != null) {
val toBeRemoved = sessionRepo.findByAquaNetUserAuId(token.aquaNetUser.auId)
.filter { it.expiry < Instant.now() }
if (toBeRemoved.isNotEmpty())
sessionRepo.deleteAll(toBeRemoved)
if (token.expiry < Instant.now()) {
sessionRepo.delete(token)
return null
}
}
return token?.aquaNetUser
} catch (e: Exception) { } catch (e: Exception) {
log.debug("Failed to parse JWT", e) log.debug("Failed to parse JWT", e)
null return null
}
} }
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token") fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")

View File

@@ -0,0 +1,31 @@
package icu.samnyan.aqua.net.db
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.io.Serializable
import java.time.Instant
import java.util.UUID
@Entity
@Table(name = "aqua_net_session")
class SessionToken(
@Id
@Column(nullable = false)
var token: String = UUID.randomUUID().toString(),
// Token creation time
@Column(nullable = false)
var expiry: Instant = Instant.now().plusSeconds(3 * 86400),
// Linking to the AquaNetUser
@ManyToOne
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser = AquaNetUser()
) : Serializable
@Repository
interface SessionTokenRepo : JpaRepository<SessionToken, String> {
fun findByToken(token: String): SessionToken?
fun findByAquaNetUserAuId(auId: Long): List<SessionToken>
}

View File

@@ -0,0 +1,7 @@
CREATE TABLE aqua_net_session
(
token VARCHAR(36) NOT NULL,
expiry datetime NOT NULL,
au_id BIGINT NULL,
CONSTRAINT pk_session PRIMARY KEY (token)
);