187 lines
7.2 KiB
Kotlin
187 lines
7.2 KiB
Kotlin
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()
|
||
}
|
||
}
|