mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-02-12 17:37:27 +08:00
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:
@@ -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
|
||||||
|
|||||||
@@ -1,77 +1,111 @@
|
|||||||
package icu.samnyan.aqua.net.components
|
package icu.samnyan.aqua.net.components
|
||||||
|
|
||||||
import ext.Str
|
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 io.jsonwebtoken.JwtParser
|
import icu.samnyan.aqua.net.db.SessionToken
|
||||||
import io.jsonwebtoken.Jwts
|
import icu.samnyan.aqua.net.db.SessionTokenRepo
|
||||||
import io.jsonwebtoken.security.Keys
|
import io.jsonwebtoken.JwtParser
|
||||||
import jakarta.annotation.PostConstruct
|
import io.jsonwebtoken.Jwts
|
||||||
import org.slf4j.LoggerFactory
|
import io.jsonwebtoken.security.Keys
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import jakarta.annotation.PostConstruct
|
||||||
import org.springframework.context.annotation.Configuration
|
import jakarta.transaction.Transactional
|
||||||
import org.springframework.stereotype.Service
|
import org.slf4j.LoggerFactory
|
||||||
import java.util.*
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
import javax.crypto.SecretKey
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
@Configuration
|
import java.time.Instant
|
||||||
@ConfigurationProperties(prefix = "aqua-net.jwt")
|
import java.util.*
|
||||||
class JWTProperties {
|
import javax.crypto.SecretKey
|
||||||
var secret: Str = "Open Sesame!"
|
|
||||||
}
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "aqua-net.jwt")
|
||||||
@Service
|
class JWTProperties {
|
||||||
class JWT(
|
var secret: Str = "Open Sesame!"
|
||||||
val props: JWTProperties,
|
}
|
||||||
val userRepo: AquaNetUserRepo
|
|
||||||
) {
|
@Service
|
||||||
val log = LoggerFactory.getLogger(JWT::class.java)!!
|
class JWT(
|
||||||
lateinit var key: SecretKey
|
val props: JWTProperties,
|
||||||
lateinit var parser: JwtParser
|
val userRepo: AquaNetUserRepo,
|
||||||
|
val sessionRepo: SessionTokenRepo
|
||||||
@PostConstruct
|
) {
|
||||||
fun onLoad() {
|
val log = LoggerFactory.getLogger(JWT::class.java)!!
|
||||||
// Check secret
|
lateinit var key: SecretKey
|
||||||
if (props.secret == "Open Sesame!") {
|
lateinit var parser: JwtParser
|
||||||
log.warn("USING DEFAULT JWT SECRET, PLEASE SET aqua-net.jwt IN CONFIGURATION")
|
|
||||||
}
|
@PostConstruct
|
||||||
|
fun onLoad() {
|
||||||
// Pad byte array to 256 bits
|
// Check secret
|
||||||
var ba = props.secret.toByteArray()
|
if (props.secret == "Open Sesame!") {
|
||||||
if (ba.size < 32) {
|
log.warn("USING DEFAULT JWT SECRET, PLEASE SET aqua-net.jwt IN CONFIGURATION")
|
||||||
log.warn("JWT Secret is less than 256 bits, padding with 0. PLEASE USE A STRONGER SECRET!")
|
}
|
||||||
ba = ByteArray(32).also { ba.copyInto(it) }
|
|
||||||
}
|
// Pad byte array to 256 bits
|
||||||
|
var ba = props.secret.toByteArray()
|
||||||
// Initialize key
|
if (ba.size < 32) {
|
||||||
key = Keys.hmacShaKeyFor(ba)
|
log.warn("JWT Secret is less than 256 bits, padding with 0. PLEASE USE A STRONGER SECRET!")
|
||||||
|
ba = ByteArray(32).also { ba.copyInto(it) }
|
||||||
// Create parser
|
}
|
||||||
parser = Jwts.parser()
|
|
||||||
.verifyWith(key)
|
// Initialize key
|
||||||
.build()
|
key = Keys.hmacShaKeyFor(ba)
|
||||||
|
|
||||||
log.info("JWT Service Enabled")
|
// Create parser
|
||||||
}
|
parser = Jwts.parser()
|
||||||
|
.verifyWith(key)
|
||||||
|
.build()
|
||||||
fun gen(user: AquaNetUser): Str = Jwts.builder().header()
|
|
||||||
.keyId("aqua-net")
|
log.info("JWT Service Enabled")
|
||||||
.and()
|
}
|
||||||
.subject(user.auId.toString())
|
|
||||||
.issuedAt(Date())
|
@Transactional
|
||||||
.signWith(key)
|
fun gen(user: AquaNetUser): Str {
|
||||||
.compact()
|
val activeTokens = sessionRepo.findByAquaNetUserAuId(user.auId)
|
||||||
|
.sortedByDescending { it.expiry }.drop(4) // the cap is 5, but we append a new token after the fact
|
||||||
fun parse(token: Str): AquaNetUser? = try {
|
if (activeTokens.isNotEmpty()) {
|
||||||
userRepo.findByAuId(parser.parseSignedClaims(token).payload.subject.toLong())
|
sessionRepo.deleteAll(activeTokens)
|
||||||
} catch (e: Exception) {
|
}
|
||||||
log.debug("Failed to parse JWT", e)
|
val token = SessionToken().apply {
|
||||||
null
|
aquaNetUser = user
|
||||||
}
|
}
|
||||||
|
sessionRepo.save(token)
|
||||||
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
|
|
||||||
|
return Jwts.builder().header()
|
||||||
final inline fun <T> auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token))
|
.keyId("aqua-net")
|
||||||
|
.and()
|
||||||
|
.subject(token.token)
|
||||||
|
.issuedAt(Date())
|
||||||
|
.signWith(key)
|
||||||
|
.compact()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
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) {
|
||||||
|
log.debug("Failed to parse JWT", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
|
||||||
|
|
||||||
|
final inline fun <T> auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token))
|
||||||
}
|
}
|
||||||
31
src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt
Normal file
31
src/main/java/icu/samnyan/aqua/net/db/AquaNetSession.kt
Normal 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>
|
||||||
|
}
|
||||||
7
src/main/resources/db/80/V1000_53__net_session.sql
Normal file
7
src/main/resources/db/80/V1000_53__net_session.sql
Normal 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)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user