Files
luminic-space-back-v2/src/main/kotlin/space/luminic/finance/api/AuthController.kt
2025-11-17 15:02:47 +03:00

187 lines
7.2 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package space.luminic.finance.api
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonBuilder
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.*
import space.luminic.finance.dtos.UserDTO
import space.luminic.finance.dtos.UserDTO.AuthUserDTO
import space.luminic.finance.dtos.UserDTO.RegisterUserDTO
import space.luminic.finance.mappers.UserMapper.toDto
import space.luminic.finance.mappers.UserMapper.toTelegramMap
import space.luminic.finance.services.AuthService
import java.net.URLDecoder
import java.security.MessageDigest
import java.time.Instant
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@RestController
@RequestMapping("/auth")
class AuthController(
private val authService: AuthService,
@Value("\${telegram.bot.token}") private val botToken: String
) {
private val logger = LoggerFactory.getLogger(javaClass)
fun verifyTelegramAuth(
loginData: Map<String, String>? = null, // from login widget
webAppInitData: String? = null
): Boolean {
// --- LOGIN WIDGET CHECK ---
if (loginData != null) {
val hash = loginData["hash"]
if (hash != null) {
val dataCheckString = loginData
.filterKeys { it != "hash" }
.toSortedMap()
.map { "${it.key}=${it.value}" }
.joinToString("\n")
val secretKey = MessageDigest.getInstance("SHA-256")
.digest(botToken.toByteArray())
val hmac = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec(secretKey, "HmacSHA256"))
}.doFinal(dataCheckString.toByteArray())
.joinToString("") { "%02x".format(it) }
val authDate = loginData["auth_date"]?.toLongOrNull() ?: return false
if (Instant.now().epochSecond - authDate > 3600) return false
if (hmac == hash) return true
}
}
// --- WEBAPP CHECK ---
// --- WEBAPP CHECK ---
if (webAppInitData != null) {
// Разбираем query string корректно (учитывая '=' внутри значения)
val pairs: Map<String, String> = webAppInitData.split("&")
.mapNotNull { part ->
val idx = part.indexOf('=')
if (idx <= 0) return@mapNotNull null
val k = part.substring(0, idx)
val v = part.substring(idx + 1)
k to URLDecoder.decode(v, Charsets.UTF_8.name())
}.toMap()
val receivedHash = pairs["hash"] ?: return false
// Строка для подписи: все поля КРОМЕ hash, отсортированные по ключу, в формате key=value с \n
val dataCheckString = pairs
.filterKeys { it != "hash" }
.toSortedMap()
.entries
.joinToString("\n") { (k, v) -> "$k=$v" }
// ВАЖНО: secret_key = HMAC_SHA256(message=botToken, key="WebAppData")
val secretKeyBytes = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec("WebAppData".toByteArray(Charsets.UTF_8), "HmacSHA256"))
}.doFinal(botToken.toByteArray(Charsets.UTF_8))
// hash = HMAC_SHA256(message=data_check_string, key=secret_key)
val calcHashHex = Mac.getInstance("HmacSHA256").apply {
init(SecretKeySpec(secretKeyBytes, "HmacSHA256"))
}.doFinal(dataCheckString.toByteArray(Charsets.UTF_8))
.joinToString("") { "%02x".format(it) }
// опциональная проверка свежести
val authDate = pairs["auth_date"]?.toLongOrNull() ?: return false
val now = Instant.now().epochSecond
val ttl = 6 * 3600L // например, 6 часов для WebApp
val skew = 300L // допускаем до 5 минут будущего/прошлого
val diff = now - authDate
if (diff > ttl || diff < -skew) return false
if (calcHashHex == receivedHash) return true
}
return false
return false
}
private fun sha256(input: String): ByteArray =
MessageDigest.getInstance("SHA-256").digest(input.toByteArray())
private fun hmacSha256(secret: ByteArray, message: String): String {
val key = SecretKeySpec(secret, "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(key)
val hashBytes = mac.doFinal(message.toByteArray())
return hashBytes.joinToString("") { "%02x".format(it) }
}
@GetMapping("/test")
fun test(): String {
val authentication = SecurityContextHolder.getContext().authentication
logger.info("SecurityContext in controller: $authentication")
return "Hello, ${authentication.name}"
}
@PostMapping("/login")
fun login(@RequestBody request: AuthUserDTO): Map<String, String> {
val token = authService.login(request.username.lowercase(), request.password)
return mapOf("token" to token)
}
@PostMapping("/register")
fun register(@RequestBody request: RegisterUserDTO): UserDTO {
return authService.register(request.username, request.password, request.firstName).toDto()
}
private val json = Json { ignoreUnknownKeys = true }
@PostMapping("/tg-login")
fun tgLogin(@RequestBody tgUser: UserDTO.TelegramAuthDTO): Map<String, String> {
// println(tgUser.hash)
// println(botToken)
if (tgUser.initData == null) {
if (verifyTelegramAuth(
loginData = tgUser.toTelegramMap(),
)
) {
return mapOf("token" to authService.tgAuth(tgUser))
} else throw IllegalArgumentException("Invalid Telegram login")
} else {
if (verifyTelegramAuth(webAppInitData = tgUser.initData)) {
val params = tgUser.initData.split("&").associate {
val (k, v) = it.split("=", limit = 2)
k to URLDecoder.decode(v, "UTF-8")
}
val userJson = params["user"] ?: error("No user data")
val jsonUser = json.decodeFromString<UserDTO.TelegramUserData>(userJson)
val newUser = UserDTO.TelegramAuthDTO(
jsonUser.id,
jsonUser.first_name,
jsonUser.last_name,
jsonUser.username,
jsonUser.photo_url,
null,
hash = tgUser.hash,
initData = null,
)
return mapOf("token" to authService.tgAuth(newUser))
} else throw IllegalArgumentException("Invalid Telegram login")
}
}
@GetMapping("/me")
fun getMe(): UserDTO {
logger.info("Get Me")
authService.getSecurityUser()
return authService.getSecurityUser().toDto()
}
}