recurrents
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
package space.luminic.finance.api
|
||||
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils.sha256
|
||||
import org.apache.commons.codec.digest.HmacUtils.hmacSha256
|
||||
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
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
@@ -27,28 +28,83 @@ class AuthController(
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
fun verifyTelegramAuth(data: Map<String, String>, botToken: String): Boolean {
|
||||
val hash = data["hash"] ?: return false
|
||||
fun verifyTelegramAuth(
|
||||
loginData: Map<String, String>? = null, // from login widget
|
||||
webAppInitData: String? = null
|
||||
): Boolean {
|
||||
|
||||
val dataCheckString = data
|
||||
.filterKeys { it != "hash" }
|
||||
.toSortedMap()
|
||||
.map { "${it.key}=${it.value}" }
|
||||
.joinToString("\n")
|
||||
// --- 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 = sha256(botToken)
|
||||
val hmacHex = hmacSha256(secretKey, dataCheckString)
|
||||
val secretKey = MessageDigest.getInstance("SHA-256")
|
||||
.digest(botToken.toByteArray())
|
||||
|
||||
if (hmacHex != hash) return false
|
||||
val hmac = Mac.getInstance("HmacSHA256").apply {
|
||||
init(SecretKeySpec(secretKey, "HmacSHA256"))
|
||||
}.doFinal(dataCheckString.toByteArray())
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
|
||||
val authDate = data["auth_date"]?.toLongOrNull() ?: return false
|
||||
val now = Instant.now().epochSecond
|
||||
val authDate = loginData["auth_date"]?.toLongOrNull() ?: return false
|
||||
if (Instant.now().epochSecond - authDate > 3600) return false
|
||||
|
||||
// Опционально — запрет старых ответов (например, старше 1 часа)
|
||||
val maxAgeSeconds = 3600
|
||||
if (now - authDate > maxAgeSeconds) return false
|
||||
if (hmac == hash) return true
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
@@ -62,6 +118,7 @@ class AuthController(
|
||||
return hashBytes.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/test")
|
||||
fun test(): String {
|
||||
val authentication = SecurityContextHolder.getContext().authentication
|
||||
@@ -80,18 +137,45 @@ class AuthController(
|
||||
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)
|
||||
val ok = verifyTelegramAuth(tgUser.toTelegramMap(), botToken)
|
||||
if (!ok) throw IllegalArgumentException("Invalid Telegram login")
|
||||
return mapOf("token" to authService.tgAuth(tgUser))
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user