mirror of
https://github.com/MewoLab/AquaDX.git
synced 2025-10-25 12:02:40 +00: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:
parent
82adf5c138
commit
39ed8af840
@ -34,6 +34,7 @@ class UserRegistrar(
|
||||
val cardService: CardService,
|
||||
val validator: AquaUserServices,
|
||||
val emailProps: EmailProperties,
|
||||
val sessionRepo: SessionTokenRepo,
|
||||
final val paths: PathProps
|
||||
) {
|
||||
val portraitPath = paths.aquaNetPortrait.path()
|
||||
@ -233,6 +234,12 @@ class UserRegistrar(
|
||||
|
||||
// Save the user
|
||||
userRepo.save(u)
|
||||
|
||||
// Clear all tokens if changing password
|
||||
if (key == "pwHash")
|
||||
sessionRepo.deleteAll(
|
||||
sessionRepo.findByAquaNetUserAuId(u.auId)
|
||||
)
|
||||
}
|
||||
|
||||
SUCCESS
|
||||
|
||||
@ -1,77 +1,111 @@
|
||||
package icu.samnyan.aqua.net.components
|
||||
|
||||
import ext.Str
|
||||
import ext.minus
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
import icu.samnyan.aqua.net.db.AquaNetUserRepo
|
||||
import io.jsonwebtoken.JwtParser
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.*
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "aqua-net.jwt")
|
||||
class JWTProperties {
|
||||
var secret: Str = "Open Sesame!"
|
||||
}
|
||||
|
||||
@Service
|
||||
class JWT(
|
||||
val props: JWTProperties,
|
||||
val userRepo: AquaNetUserRepo
|
||||
) {
|
||||
val log = LoggerFactory.getLogger(JWT::class.java)!!
|
||||
lateinit var key: SecretKey
|
||||
lateinit var parser: JwtParser
|
||||
|
||||
@PostConstruct
|
||||
fun onLoad() {
|
||||
// Check secret
|
||||
if (props.secret == "Open Sesame!") {
|
||||
log.warn("USING DEFAULT JWT SECRET, PLEASE SET aqua-net.jwt IN CONFIGURATION")
|
||||
}
|
||||
|
||||
// Pad byte array to 256 bits
|
||||
var ba = props.secret.toByteArray()
|
||||
if (ba.size < 32) {
|
||||
log.warn("JWT Secret is less than 256 bits, padding with 0. PLEASE USE A STRONGER SECRET!")
|
||||
ba = ByteArray(32).also { ba.copyInto(it) }
|
||||
}
|
||||
|
||||
// Initialize key
|
||||
key = Keys.hmacShaKeyFor(ba)
|
||||
|
||||
// Create parser
|
||||
parser = Jwts.parser()
|
||||
.verifyWith(key)
|
||||
.build()
|
||||
|
||||
log.info("JWT Service Enabled")
|
||||
}
|
||||
|
||||
|
||||
fun gen(user: AquaNetUser): Str = Jwts.builder().header()
|
||||
.keyId("aqua-net")
|
||||
.and()
|
||||
.subject(user.auId.toString())
|
||||
.issuedAt(Date())
|
||||
.signWith(key)
|
||||
.compact()
|
||||
|
||||
fun parse(token: Str): AquaNetUser? = try {
|
||||
userRepo.findByAuId(parser.parseSignedClaims(token).payload.subject.toLong())
|
||||
} catch (e: Exception) {
|
||||
log.debug("Failed to parse JWT", e)
|
||||
null
|
||||
}
|
||||
|
||||
fun auth(token: Str) = parse(token) ?: (400 - "Invalid token")
|
||||
|
||||
final inline fun <T> auth(token: Str, block: (AquaNetUser) -> T) = block(auth(token))
|
||||
package icu.samnyan.aqua.net.components
|
||||
|
||||
import ext.Str
|
||||
import ext.minus
|
||||
import icu.samnyan.aqua.net.db.AquaNetUser
|
||||
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.Jwts
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.transaction.Transactional
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "aqua-net.jwt")
|
||||
class JWTProperties {
|
||||
var secret: Str = "Open Sesame!"
|
||||
}
|
||||
|
||||
@Service
|
||||
class JWT(
|
||||
val props: JWTProperties,
|
||||
val userRepo: AquaNetUserRepo,
|
||||
val sessionRepo: SessionTokenRepo
|
||||
) {
|
||||
val log = LoggerFactory.getLogger(JWT::class.java)!!
|
||||
lateinit var key: SecretKey
|
||||
lateinit var parser: JwtParser
|
||||
|
||||
@PostConstruct
|
||||
fun onLoad() {
|
||||
// Check secret
|
||||
if (props.secret == "Open Sesame!") {
|
||||
log.warn("USING DEFAULT JWT SECRET, PLEASE SET aqua-net.jwt IN CONFIGURATION")
|
||||
}
|
||||
|
||||
// Pad byte array to 256 bits
|
||||
var ba = props.secret.toByteArray()
|
||||
if (ba.size < 32) {
|
||||
log.warn("JWT Secret is less than 256 bits, padding with 0. PLEASE USE A STRONGER SECRET!")
|
||||
ba = ByteArray(32).also { ba.copyInto(it) }
|
||||
}
|
||||
|
||||
// Initialize key
|
||||
key = Keys.hmacShaKeyFor(ba)
|
||||
|
||||
// Create parser
|
||||
parser = Jwts.parser()
|
||||
.verifyWith(key)
|
||||
.build()
|
||||
|
||||
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)
|
||||
|
||||
return Jwts.builder().header()
|
||||
.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)
|
||||
);
|
||||
Loading…
x
Reference in New Issue
Block a user