Skip to content

Commit

Permalink
#54 Develop the authentication service draft (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
vityaman authored Nov 12, 2024
1 parent 0b9c0d1 commit d18f230
Show file tree
Hide file tree
Showing 37 changed files with 842 additions and 3 deletions.
16 changes: 16 additions & 0 deletions backend/authik/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@file:Suppress("UNCHECKED_CAST")

plugins {
id("buildlogic.foundation-conventions")
}

dependencies {
implementation(libs.commons.codec.commons.codec)
implementation(project(":foundation"))
testImplementation(project(":foundation-test"))
}

val serviceName = "authik"

(extra["generateOAPIServer"] as (String) -> Unit)(serviceName)
(extra["generateJOOQ"] as (String) -> Unit)(serviceName)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ru.ifmo.se.dating.authik.api

import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import ru.ifmo.se.dating.authik.api.generated.AuthApiDelegate
import ru.ifmo.se.dating.authik.logic.AuthService
import ru.ifmo.se.dating.authik.model.generated.AuthGrantMessage
import ru.ifmo.se.dating.authik.model.generated.TelegramInitDataMessage
import ru.ifmo.se.dating.authik.telegram.InitDataParser
import ru.ifmo.se.dating.exception.AuthenticationException
import ru.ifmo.se.dating.exception.GenericException
import ru.ifmo.se.dating.text.abbreviated

@Controller
class HttpAuthApi(
private val telegramParser: InitDataParser,
private val auth: AuthService,
) : AuthApiDelegate {
override suspend fun authTelegramWebAppPut(
telegramInitDataMessage: TelegramInitDataMessage,
): ResponseEntity<AuthGrantMessage> {
val initData = try {
telegramParser.parse(telegramInitDataMessage)
} catch (error: GenericException) {
throw AuthenticationException(
"Corrupted ${telegramInitDataMessage.string.abbreviated()}: ${error.message}",
error,
)
}
val token = auth.authenticate(initData)
val response = AuthGrantMessage(access = token.text)
return ResponseEntity.ok(response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ru.ifmo.se.dating.authik.api

import kotlinx.coroutines.reactor.awaitSingle
import org.springframework.http.ResponseEntity
import org.springframework.r2dbc.core.DatabaseClient
import org.springframework.stereotype.Controller
import ru.ifmo.se.dating.authik.api.generated.MonitoringApiDelegate

@Controller
internal class HttpMonitoringApi(
private val data: DatabaseClient,
) : MonitoringApiDelegate {
override suspend fun monitoringHealthcheckGet(): ResponseEntity<String> =
data
.sql("SELECT current_schema")
.map { row, _ -> row.get(0, String::class.java) }
.one()
.map { pong -> ResponseEntity.ok(pong) }
.awaitSingle()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ru.ifmo.se.dating.authik.logic

import ru.ifmo.se.dating.authik.model.generated.TelegramWebAppInitDataMessage
import ru.ifmo.se.dating.security.auth.AccessToken

interface AuthService {
suspend fun authenticate(telegram: TelegramWebAppInitDataMessage): AccessToken
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ru.ifmo.se.dating.authik.logic.basic

import org.springframework.stereotype.Service
import ru.ifmo.se.dating.authik.logic.AuthService
import ru.ifmo.se.dating.authik.model.generated.TelegramWebAppInitDataMessage
import ru.ifmo.se.dating.authik.security.auth.JwtTokenIssuer
import ru.ifmo.se.dating.authik.storage.TelegramAccountStorage
import ru.ifmo.se.dating.security.auth.AccessToken

@Service
class BasicAuthService(
private val telegramAccountStorage: TelegramAccountStorage,
private val issuer: JwtTokenIssuer,
) : AuthService {
override suspend fun authenticate(telegram: TelegramWebAppInitDataMessage): AccessToken {
val userId = telegramAccountStorage.getOrInsert(telegram.user.id)
return issuer.issue(AccessToken.Payload(userId))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ru.ifmo.se.dating.authik.security.auth

import io.jsonwebtoken.Jwts
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import ru.ifmo.se.dating.security.auth.AccessToken
import ru.ifmo.se.dating.security.auth.Jwt
import ru.ifmo.se.dating.security.key.Keys
import java.security.PrivateKey
import java.time.Clock
import java.util.*
import kotlin.time.toJavaDuration
import kotlin.time.toKotlinDuration
import java.time.Duration as JavaDuration
import kotlin.time.Duration as KotlinDuration

@Configuration
class JwtTokenIssuerConfiguration {
@Bean
fun jwtTokenIssuer(
clock: Clock,

@Value("\${security.auth.token.sign.private}")
privateSignKey: String,

@Value("\${security.auth.token.duration}")
duration: JavaDuration,
) = JwtTokenIssuer(
clock = clock,
privateSignKey = Keys.deserializePrivate(privateSignKey),
duration = duration.toKotlinDuration(),
)
}

class JwtTokenIssuer(
private val clock: Clock,
private val privateSignKey: PrivateKey,
duration: KotlinDuration,
) : TokenIssuer {
private val duration: JavaDuration = duration.toJavaDuration()

override fun issue(payload: AccessToken.Payload): AccessToken {
val now = clock.instant()
return Jwts.builder()
.claims(Jwt.serialize(payload))
.issuedAt(Date.from(now))
.expiration(Date.from(now + duration))
.signWith(privateSignKey)
.compact()
.let { AccessToken(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.ifmo.se.dating.authik.security.auth

import ru.ifmo.se.dating.security.auth.AccessToken

interface TokenIssuer {
fun issue(payload: AccessToken.Payload): AccessToken
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.ifmo.se.dating.authik.storage

import ru.ifmo.se.dating.security.auth.User

interface TelegramAccountStorage {
suspend fun getOrInsert(telegramId: Long): User.Id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ru.ifmo.se.dating.authik.storage.jooq

import org.jooq.generated.tables.references.TELEGRAM_ACCOUNT
import org.springframework.stereotype.Repository
import ru.ifmo.se.dating.authik.storage.TelegramAccountStorage
import ru.ifmo.se.dating.security.auth.User
import ru.ifmo.se.dating.storage.TxEnv
import ru.ifmo.se.dating.storage.jooq.JooqDatabase

@Repository
class JooqTelegramAccountStorage(
private val database: JooqDatabase,
private val txEnv: TxEnv,
) : TelegramAccountStorage {
override suspend fun getOrInsert(telegramId: Long): User.Id = txEnv.transactional {
database.maybe {
selectFrom(TELEGRAM_ACCOUNT)
.where(TELEGRAM_ACCOUNT.TELEGRAM_ID.eq(telegramId))
} ?: database.only {
insertInto(TELEGRAM_ACCOUNT)
.set(TELEGRAM_ACCOUNT.TELEGRAM_ID, telegramId)
.returning()
}
}.let { User.Id(it.accountId!!) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ru.ifmo.se.dating.authik.telegram

import com.fasterxml.jackson.databind.ObjectMapper
import org.apache.commons.codec.digest.HmacUtils
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import ru.ifmo.se.dating.authik.model.generated.TelegramInitDataMessage
import ru.ifmo.se.dating.authik.model.generated.TelegramWebAppInitDataMessage
import ru.ifmo.se.dating.authik.model.generated.TelegramWebAppUserMessage
import ru.ifmo.se.dating.exception.InvalidValueException
import ru.ifmo.se.dating.validation.expect

@Component
class InitDataParser(
private val jackson: ObjectMapper,

@Value("\${security.auth.telegram.token}")
telegramToken: String,
) {
private val algorithm = "HmacSHA256"
private val hmac =
HmacUtils(algorithm, HmacUtils(algorithm, "WebAppData").hmac(telegramToken))

fun parse(initData: TelegramInitDataMessage): TelegramWebAppInitDataMessage {
expect(hmac.hmacHex(initData.string) == initData.hash, "hashes are not equal")

val map = parseToMap(initData)

val user = jackson.readValue(
map["user"] ?: throw InvalidValueException("no user"),
TelegramWebAppUserMessage::class.java,
)

val authDate = map["auth_date"]?.toLongOrNull()
?: throw InvalidValueException("no auth_date or invalid")

return TelegramWebAppInitDataMessage(
queryId = map["query_id"],
user = user,
authDate = authDate,
raw = initData.string,
hash = initData.hash,
)
}

private fun parseToMap(initData: TelegramInitDataMessage): Map<String, String> {
val map = mutableMapOf<String, String>()
for (pair in initData.string.split("\n")) {
val kv = pair.split("=", limit = 2)
expect(kv.size == 2, "syntax error")
map[kv[0]] = kv[1]
}
return map
}
}
8 changes: 8 additions & 0 deletions backend/authik/src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
security:
auth:
token:
sign:
public: RSA:MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEArnKw3YcR3WJeLW64J6gc+8dT/ptl4Oi1kdfgib1EQBJmiNVmzgx6hnmf60MhTCbPHeKhbBKzozyFlboO32Aqx5Nfb0UAU2ssl99tuNi8R2VsYby6wkog58GgFidffKohdhWjOZaa3rBNI1D8CQXckk5WW4eFbonB6Vo84OLsebW5CX9ob8bCsJBX2iZYwS+WNCluUMFgxRyaLuyhtyKp0YRa7oje7iu3EXiLnaXTAFhGSP+iK6GxMUPORvGZYfJ7z+tpj6OYQId5cwYD/+5EXFM4wCkq82VDbj99mJqClpHs+1DhPP7sO/aSDM9SONXjAsMTtq27jJgdvEADpd6pHtwv/tHv1PsRS6DiQYFQSx5egc48JEiVDsBkMy3TzOmvf2dAU1KLWImNSwCybnwQiBhoRr2xPuUB6gNwyrUM8gSiX5HfK9pPX2LueberFzBYnzi8yR1phkLlqfvMZn9q6uRp9ysrtsw2tGf+Wn8BlbAoq3W8hD8ufr5pR03zHGvnAgMBAAE=
private: RSA:MIIG/QIBADANBgkqhkiG9w0BAQEFAASCBucwggbjAgEAAoIBgQCucrDdhxHdYl4tbrgnqBz7x1P+m2Xg6LWR1+CJvURAEmaI1WbODHqGeZ/rQyFMJs8d4qFsErOjPIWVug7fYCrHk19vRQBTayyX32242LxHZWxhvLrCSiDnwaAWJ198qiF2FaM5lpresE0jUPwJBdySTlZbh4VuicHpWjzg4ux5tbkJf2hvxsKwkFfaJljBL5Y0KW5QwWDFHJou7KG3IqnRhFruiN7uK7cReIudpdMAWEZI/6IrobExQ85G8Zlh8nvP62mPo5hAh3lzBgP/7kRcUzjAKSrzZUNuP32YmoKWkez7UOE8/uw79pIMz1I41eMCwxO2rbuMmB28QAOl3qke3C/+0e/U+xFLoOJBgVBLHl6BzjwkSJUOwGQzLdPM6a9/Z0BTUotYiY1LALJufBCIGGhGvbE+5QHqA3DKtQzyBKJfkd8r2k9fYu55t6sXMFifOLzJHWmGQuWp+8xmf2rq5Gn3Kyu2zDa0Z/5afwGVsCirdbyEPy5+vmlHTfMca+cCAwEAAQKCAYBP2XOXkvHcceBFz34/uLW7kZui2SKi9iHWJghDQ/zvjvyb+YJbIl8bGqTWnR2qq8D2HvxgaZcMSvGifU29dVlfjNeMKPtjM5Vv1vd0OtDDpWscubSKpj+1lW1fdppAh+dVE8Zo38T31Z8ZYUJcJvC1j2H792ZeGHRICeP/1B8F/uY5sLXvI/2NsCRmWFMb6lpIegZitIFE+Dii7fF/0EAHBRxSPxg70Iq1VoYhnPueFsnlNA3ZBuQCdtT+qCvbJ5A9q1EHxlNMLkX2CyLat03G3zit5Pz5frWxGjDNf4Ep0SdL3IBZY2CW8SF0yi2WXWlFPM+Q2ozy2VMx2ESTdKmlNxgIpWWY1p+oJVltKFdTX7Aqq00P6wIhHoOU6+JFtMkyGw0M79tohBjpVV/7DpvTcY7pVSF4jSoTWEaS20LBrHvePTs6YfxnTVDcG7JyfhPSbktF6llN3L8Zi53n6JPtMwXTc3VIJDfs4hLCq8P52eexWnlpWOJN576JopvhLYECgcEAuf91450eMPIgyf8aAx6eUSBvYSPJrF8Xeh1P1CV1AdCYavi2va6rjH01PFcSweyheOfbFxbe+Kto8z3NhRLuLksC+gDWZRLhInTCNwxJ7G5L/mPN2eauZZ0pUGbHrtl3JTMt8FXBB0afK/WPfqepIL9H/3PWBC3jY6xAo64RIX4USjnCdriAQtdCkcGAajX6vEIN/KTifHYOHLzsn8gM2EFXHWAzAmkKcthk1AIU7INY4gldZunkM50TBQRw2vfhAoHBAPAaa+fEoxrIlSm1edG2rJil2DGOIwtKG8ZkjmhxLEq2XQD1obvjs54IfWFg5VviYXvgihujGdbcuviHrZaJOYP+EynOve9D2uaMUq08GYFcZI4jjO8WDSFkUhX361nKqT+O9w+r+KdA1fDoIuHHInvEOTlX/6v9KQADktiNCZjs2KV9TrPTFeoyrbhfLh8smDhRlqVfJ4IK27JcqYEx6mxatucGybQM4KLmDRondd9H77FflMtanktixWjm3G08xwKBwH1B+7tgaR+fP9Oo13S4XvfVdwydFEjf9SiIquT8oLKrLqoDetV81wySmZJcNUahvBB3XAVNorUmglQlD84JdJt6arPAcqG4uCMDLHPz86ikksrrnYqcHmBSGauKu/kVfHZx5AMRTSBAQBtTkOJDuNNT3gG7mapQ2Oyb6SARrnm2taVTBpH7KG1bF/qerINafNPhTBgTVm9o9ZIG7Pehuny8bBVdXpzF7oJvFl/sUvkAb5AxrFQNOWBE7LUZS4M7IQKBwDLNtGVPAyAIrx8rKgKIv45xEQSzSZD69lONNWC+CZwpaBZq4vTpojjfHQB8yysdBHl8slxUr4P6Iomx07YVhRj7qrxe5Wt6FRhROrEzFUZ88T3uIcT5CoA1RPUnByJxskwjiP1E6xEgs+QMikzxoMdFZsJOb2fJ4mIBX5H4jb5Q5yplEEEWef2bCY0Ifq7T9cV85f5J2wc2GvRrjOYsVKjmrOrHUeiKDQIK4VzWWqeLBhmm2soIe5QB6zleF+f5QwKBwQCX68V+DPfc0HgzUFkTlV5LaVxt1oQWZFVNsiABjaaku4XHpe02kaiS1qY9ul4AHhCdCJf9u+7lQdRX2quN76WdvurQRZ8/zxXlLv3kIm6DdUPoz99nAEX02vY8dZSI8saKALmeMT0zgQtmmWyJeyl7kd7T/Xl6ePgWPDM/e3MFhKuT6utaAY2/2jJJx/7ULzNIW9JFcijohVKbTxLA4qoKUPxgZyoWS0In9p4s9mKNkKxrD2MJylpk9ro78T7Qv0g=
telegram:
token: fake-telegram-bot-token
20 changes: 20 additions & 0 deletions backend/authik/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
spring:
config:
import: application-foundation.yml
datasource:
url: jdbc:postgresql://authik-database:5432/${POSTGRES_DB}
username: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
r2dbc:
url: r2dbc:postgresql://authik-database:5432/${POSTGRES_DB}
username: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
security:
auth:
token:
sign:
public: ${TOKEN_SIGN_KEY_PUBLIC}
private: ${TOKEN_SIGN_KEY_PRIVATE}
duration: PT2H
telegram:
token: ${TELEGRAM_BOT_TOKEN}
13 changes: 13 additions & 0 deletions backend/authik/src/main/resources/database/changelog.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--liquibase formatted sql

--changeset vityaman:initialize
CREATE SCHEMA authik;

--changeset vityaman:account
CREATE SEQUENCE authik.account_id_seq AS integer START 1;

CREATE TABLE authik.telegram_account (
account_id integer PRIMARY KEY DEFAULT nextval('authik.account_id_seq'),
telegram_id bigint NOT NULL UNIQUE,
creation_moment timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Loading

0 comments on commit d18f230

Please sign in to comment.