add gigawear auth repository
This commit is contained in:
@@ -10,7 +10,7 @@ sqldelight {
|
|||||||
databases {
|
databases {
|
||||||
create("AppDatabase") {
|
create("AppDatabase") {
|
||||||
packageName.set("ru.shadowsparky.gigawear.backend")
|
packageName.set("ru.shadowsparky.gigawear.backend")
|
||||||
dialect("app.cash.sqldelight:postgresql-dialect:2.0.2")
|
dialect("app.cash.sqldelight:postgresql-dialect:2.3.2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -6,8 +6,9 @@ import org.koin.core.annotation.Single
|
|||||||
import ru.shadowsparky.backend.data.EnvFetcher
|
import ru.shadowsparky.backend.data.EnvFetcher
|
||||||
import ru.shadowsparky.backend.domain.JwtInfo
|
import ru.shadowsparky.backend.domain.JwtInfo
|
||||||
import ru.shadowsparky.domain.AppConfig
|
import ru.shadowsparky.domain.AppConfig
|
||||||
|
import ru.shadowsparky.gigawear.backend.di.DbModule
|
||||||
|
|
||||||
@Module
|
@Module(includes = [DbModule::class])
|
||||||
@ComponentScan
|
@ComponentScan
|
||||||
@Configuration
|
@Configuration
|
||||||
class BackendModule {
|
class BackendModule {
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import io.ktor.http.HttpStatusCode
|
|||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
import org.koin.core.annotation.KoinApplication
|
import org.koin.core.annotation.KoinApplication
|
||||||
import org.koin.plugin.module.dsl.startKoin
|
import org.koin.plugin.module.dsl.startKoin
|
||||||
import ru.shadowsparky.gigawear.backend.presentation.MOCK_USER_ID
|
import ru.shadowsparky.backend.presentation.AUTH_JWT_NAME
|
||||||
|
import ru.shadowsparky.backend.presentation.configureJwt
|
||||||
|
import ru.shadowsparky.backend.presentation.installStatusPages
|
||||||
|
import ru.shadowsparky.gigawear.backend.presentation.setupAuthRouting
|
||||||
import ru.shadowsparky.gigawear.backend.presentation.setupChat
|
import ru.shadowsparky.gigawear.backend.presentation.setupChat
|
||||||
import ru.shadowsparky.koin
|
import ru.shadowsparky.koin
|
||||||
import ru.shadowsparky.updater.backend.presentation.setupUpdates
|
import ru.shadowsparky.updater.backend.presentation.setupUpdates
|
||||||
@@ -25,9 +29,14 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
install(ContentNegotiation) { json(koin.get()) }
|
install(ContentNegotiation) { json(koin.get()) }
|
||||||
|
configureJwt(koin.get(), koin.get(), koin.get())
|
||||||
|
installStatusPages()
|
||||||
routing {
|
routing {
|
||||||
get("health") { this.call.respond(HttpStatusCode.OK) }
|
setupAuthRouting(koin.get())
|
||||||
setupChat(koin.get())
|
authenticate(AUTH_JWT_NAME) {
|
||||||
setupUpdates(koin.get()) { MOCK_USER_ID }
|
get("healthCheck") { this.call.respond(HttpStatusCode.OK) }
|
||||||
|
setupChat(koin.get())
|
||||||
|
setupUpdates(koin.get())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
package ru.shadowsparky.gigawear.backend.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.annotation.Factory
|
||||||
|
import ru.shadowsparky.backend.data.JwtPreparer
|
||||||
|
import ru.shadowsparky.backend.data.StringFetcher
|
||||||
|
import ru.shadowsparky.backend.data.StringResource
|
||||||
|
import ru.shadowsparky.backend.data.generateRefreshToken
|
||||||
|
import ru.shadowsparky.backend.data.toTokenHash
|
||||||
|
import ru.shadowsparky.backend.domain.LoginVerifier
|
||||||
|
import ru.shadowsparky.domain.DispatcherProvider
|
||||||
|
import ru.shadowsparky.gigawear.backend.AppDatabase
|
||||||
|
import ru.shadowsparky.gigawear.common.AuthRepository
|
||||||
|
import ru.shadowsparky.gigawear.common.LoginRequest
|
||||||
|
import ru.shadowsparky.http.domain.BadRequestException
|
||||||
|
import ru.shadowsparky.http.domain.TokenInfo
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import kotlin.time.toJavaInstant
|
||||||
|
|
||||||
|
@Factory
|
||||||
|
class BackendAuthRepositoryImpl(
|
||||||
|
appDatabase: AppDatabase,
|
||||||
|
private val stringFetcher: StringFetcher,
|
||||||
|
private val loginVerifier: LoginVerifier,
|
||||||
|
private val jwtPreparer: JwtPreparer,
|
||||||
|
private val dispatcherProvider: DispatcherProvider
|
||||||
|
) : AuthRepository {
|
||||||
|
private val usersQueries = appDatabase.usersQueries
|
||||||
|
private val tokenQueries = appDatabase.refresh_tokensQueries
|
||||||
|
|
||||||
|
override suspend fun login(request: LoginRequest) = withContext(dispatcherProvider.io) {
|
||||||
|
val userCode = request.code
|
||||||
|
val user = usersQueries.selectByCode(userCode).executeAsOneOrNull()
|
||||||
|
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
||||||
|
loginVerifier.verify(user.user_id)
|
||||||
|
create(user.user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun update(oldTokenInfo: TokenInfo) = withContext(dispatcherProvider.io) {
|
||||||
|
checkNotNull(oldTokenInfo.refresh)
|
||||||
|
val tokenHash = oldTokenInfo.refresh!!.toTokenHash(SALT)
|
||||||
|
val savedToken = tokenQueries.selectByHash(tokenHash)
|
||||||
|
.executeAsOneOrNull()
|
||||||
|
?: throw BadRequestException("Token not found")
|
||||||
|
val now = Clock.System.now().toJavaInstant().atOffset(ZoneOffset.UTC)
|
||||||
|
if (savedToken.revoked || savedToken.expires_at < now) {
|
||||||
|
throw BadRequestException("Expired token")
|
||||||
|
}
|
||||||
|
tokenQueries.revokeById(savedToken.token_id)
|
||||||
|
val user = usersQueries.selectById(savedToken.user_id).executeAsOneOrNull()
|
||||||
|
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
||||||
|
create(user.user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun create(userId: Long): TokenInfo {
|
||||||
|
val newAccessToken = jwtPreparer.prepare(userId)
|
||||||
|
val expiresAt = Clock.System.now() + 30.days
|
||||||
|
val refreshToken = generateRefreshToken()
|
||||||
|
tokenQueries.insertToken(
|
||||||
|
user_id = userId,
|
||||||
|
token_hash = refreshToken.toTokenHash(SALT),
|
||||||
|
expires_at = expiresAt.toJavaInstant().atOffset(ZoneOffset.UTC)
|
||||||
|
).await()
|
||||||
|
return TokenInfo(newAccessToken, refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val SALT = "[=TkATMHb!`o}II{=/|!n?06?~z%h=5POwK|\$UUcj,mHI=\$R4<p;!D-=!14|EV%M"
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-2
@@ -1,10 +1,25 @@
|
|||||||
package ru.shadowsparky.gigawear.backend.data
|
package ru.shadowsparky.gigawear.backend.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
|
import ru.shadowsparky.backend.data.StringFetcher
|
||||||
|
import ru.shadowsparky.backend.data.StringResource
|
||||||
import ru.shadowsparky.backend.domain.LoginVerifier
|
import ru.shadowsparky.backend.domain.LoginVerifier
|
||||||
|
import ru.shadowsparky.domain.DispatcherProvider
|
||||||
|
import ru.shadowsparky.gigawear.backend.AppDatabase
|
||||||
|
import ru.shadowsparky.http.domain.BadRequestException
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
class LoginVerifierImpl : LoginVerifier {
|
class LoginVerifierImpl(
|
||||||
override suspend fun verify(login: String) {
|
private val appDatabase: AppDatabase,
|
||||||
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
|
private val stringFetcher: StringFetcher
|
||||||
|
) : LoginVerifier {
|
||||||
|
|
||||||
|
override suspend fun verify(userId: Long) {
|
||||||
|
withContext(dispatcherProvider.io) {
|
||||||
|
appDatabase.usersQueries.selectById(userId).executeAsOneOrNull()
|
||||||
|
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package ru.shadowsparky.gigawear.backend.di
|
||||||
|
|
||||||
|
import app.cash.sqldelight.db.SqlDriver
|
||||||
|
import app.cash.sqldelight.driver.jdbc.asJdbcDriver
|
||||||
|
import org.koin.core.annotation.Factory
|
||||||
|
import org.koin.core.annotation.Module
|
||||||
|
import org.koin.core.annotation.Single
|
||||||
|
import org.postgresql.ds.PGSimpleDataSource
|
||||||
|
import ru.shadowsparky.backend.data.EnvFetcher
|
||||||
|
import ru.shadowsparky.gigawear.backend.AppDatabase
|
||||||
|
|
||||||
|
@Module
|
||||||
|
class DbModule {
|
||||||
|
|
||||||
|
@Factory
|
||||||
|
fun provideSqlDriver(envFetcher: EnvFetcher): SqlDriver {
|
||||||
|
return PGSimpleDataSource().apply {
|
||||||
|
val db = envFetcher.get("POSTGRES_DB", "gigawear")
|
||||||
|
val uri = envFetcher.get("VIDEOBOX_SERVER_URI", "localhost")
|
||||||
|
val login = envFetcher.get("POSTGRES_USER", "login")
|
||||||
|
val pass = envFetcher.get("POSTGRES_PASSWORD", "password")
|
||||||
|
setUrl("jdbc:postgresql://$uri:5432/$db")
|
||||||
|
user = login
|
||||||
|
password = pass
|
||||||
|
}.asJdbcDriver()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Single
|
||||||
|
fun provideDb(driver: SqlDriver): AppDatabase {
|
||||||
|
return AppDatabase.invoke(driver)
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package ru.shadowsparky.gigawear.backend.presentation
|
||||||
|
|
||||||
|
import io.ktor.server.request.receive
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import ru.shadowsparky.gigawear.common.AuthRepository
|
||||||
|
|
||||||
|
fun Route.setupAuthRouting(authRepository: AuthRepository) {
|
||||||
|
post(AuthRepository.LOGIN_PATH) {
|
||||||
|
call.respond(authRepository.login(call.receive()))
|
||||||
|
}
|
||||||
|
post(AuthRepository.UPDATE_TOKEN_PATH) {
|
||||||
|
call.respond(authRepository.update(call.receive()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-3
@@ -4,14 +4,13 @@ import io.ktor.server.request.receive
|
|||||||
import io.ktor.server.response.respond
|
import io.ktor.server.response.respond
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.server.routing.post
|
||||||
|
import ru.shadowsparky.backend.presentation.obtainUserId
|
||||||
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
|
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
|
||||||
import ru.shadowsparky.chat.domain.Message
|
import ru.shadowsparky.chat.domain.Message
|
||||||
|
|
||||||
const val MOCK_USER_ID = -1L
|
|
||||||
|
|
||||||
fun Route.setupChat(useCase: ProcessUserMessageUseCase) {
|
fun Route.setupChat(useCase: ProcessUserMessageUseCase) {
|
||||||
post("chat") {
|
post("chat") {
|
||||||
val message = call.receive<Message>().copy(timestamp = System.currentTimeMillis())
|
val message = call.receive<Message>().copy(timestamp = System.currentTimeMillis())
|
||||||
call.respond(useCase.execute(message, MOCK_USER_ID))
|
call.respond(useCase.execute(message, call.obtainUserId()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE refresh_tokens (
|
||||||
|
token_id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
user_id int8 NOT NULL,
|
||||||
|
token_hash varchar NOT NULL,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
revoked bool NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
insertToken:
|
||||||
|
INSERT INTO refresh_tokens (user_id, token_hash, expires_at, revoked)
|
||||||
|
VALUES (?, ?, ?, FALSE);
|
||||||
|
|
||||||
|
selectByHash:
|
||||||
|
SELECT token_id, user_id, token_hash, expires_at, revoked
|
||||||
|
FROM refresh_tokens
|
||||||
|
WHERE token_hash = ?;
|
||||||
|
|
||||||
|
revokeById:
|
||||||
|
UPDATE refresh_tokens
|
||||||
|
SET revoked = TRUE
|
||||||
|
WHERE token_id = ?;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
CREATE TABLE "users" (
|
||||||
|
user_id int8 NOT NULL,
|
||||||
|
login varchar NOT NULL,
|
||||||
|
code int,
|
||||||
|
CONSTRAINT user_id_pk PRIMARY KEY (user_id),
|
||||||
|
CONSTRAINT login_unique UNIQUE (login),
|
||||||
|
CONSTRAINT code_unique UNIQUE (code)
|
||||||
|
);
|
||||||
|
|
||||||
|
selectByCode:
|
||||||
|
SELECT user_id, login, code
|
||||||
|
FROM "users"
|
||||||
|
WHERE code = ?;
|
||||||
|
|
||||||
|
selectById:
|
||||||
|
SELECT user_id, login, code
|
||||||
|
FROM "users"
|
||||||
|
WHERE user_id = ?;
|
||||||
|
|
||||||
|
selectByLogin:
|
||||||
|
SELECT user_id, login, code
|
||||||
|
FROM "users"
|
||||||
|
WHERE login = ?;
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package ru.shadowsparky.gigawear.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import ru.shadowsparky.http.domain.TokenInfo
|
||||||
|
|
||||||
|
interface AuthRepository {
|
||||||
|
suspend fun login(request: LoginRequest): TokenInfo
|
||||||
|
suspend fun update(oldTokenInfo: TokenInfo): TokenInfo
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LOGIN_PATH = "login"
|
||||||
|
const val UPDATE_TOKEN_PATH = "updateToken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoginRequest(
|
||||||
|
val login: String,
|
||||||
|
val code: Int
|
||||||
|
)
|
||||||
@@ -10,7 +10,7 @@ sqldelight {
|
|||||||
databases {
|
databases {
|
||||||
create("AppDatabase") {
|
create("AppDatabase") {
|
||||||
packageName.set("ru.shadowsparky.vbox.backend")
|
packageName.set("ru.shadowsparky.vbox.backend")
|
||||||
dialect("app.cash.sqldelight:postgresql-dialect:2.0.2")
|
dialect("app.cash.sqldelight:postgresql-dialect:2.3.2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-8
@@ -6,6 +6,7 @@ import ru.shadowsparky.backend.data.EnvFetcher
|
|||||||
import ru.shadowsparky.backend.data.JwtPreparer
|
import ru.shadowsparky.backend.data.JwtPreparer
|
||||||
import ru.shadowsparky.backend.data.StringFetcher
|
import ru.shadowsparky.backend.data.StringFetcher
|
||||||
import ru.shadowsparky.backend.data.StringResource
|
import ru.shadowsparky.backend.data.StringResource
|
||||||
|
import ru.shadowsparky.backend.data.toTokenHash
|
||||||
import ru.shadowsparky.backend.domain.LoginVerifier
|
import ru.shadowsparky.backend.domain.LoginVerifier
|
||||||
import ru.shadowsparky.domain.Log
|
import ru.shadowsparky.domain.Log
|
||||||
import ru.shadowsparky.http.domain.BadRequestException
|
import ru.shadowsparky.http.domain.BadRequestException
|
||||||
@@ -17,10 +18,8 @@ import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository
|
|||||||
import ru.shadowsparky.vbox.shared.domain.ChangePasswordRequest
|
import ru.shadowsparky.vbox.shared.domain.ChangePasswordRequest
|
||||||
import ru.shadowsparky.vbox.shared.domain.TokenRequest
|
import ru.shadowsparky.vbox.shared.domain.TokenRequest
|
||||||
import ru.shadowsparky.vbox.shared.domain.model.LoginInfo
|
import ru.shadowsparky.vbox.shared.domain.model.LoginInfo
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.io.encoding.Base64
|
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
class AuthTokenRepositoryFactory(
|
class AuthTokenRepositoryFactory(
|
||||||
@@ -131,10 +130,10 @@ class BackendAuthTokenRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun create(login: String): TokenInfo = withContext(dispatcherProvider.io) {
|
private suspend fun create(login: String): TokenInfo = withContext(dispatcherProvider.io) {
|
||||||
loginVerifier.verify(login)
|
|
||||||
val userInfo = userInfoCache.query(login)
|
val userInfo = userInfoCache.query(login)
|
||||||
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
||||||
val rsp = jwtPreparer.prepare(login, userInfo.userId)
|
loginVerifier.verify(userInfo.userId)
|
||||||
|
val rsp = jwtPreparer.prepare(userInfo.userId)
|
||||||
val refresh = ByteArray(32)
|
val refresh = ByteArray(32)
|
||||||
random.nextBytes(refresh)
|
random.nextBytes(refresh)
|
||||||
val refreshStr = refresh.toHexString()
|
val refreshStr = refresh.toHexString()
|
||||||
@@ -164,10 +163,7 @@ class BackendAuthTokenRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toHash(): String {
|
private fun String.toHash(): String {
|
||||||
val md = MessageDigest.getInstance("SHA-256")
|
return toTokenHash(SALT)
|
||||||
md.update(SALT.toByteArray())
|
|
||||||
val result = md.digest(this.toByteArray())
|
|
||||||
return Base64.encode(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|||||||
+2
-2
@@ -12,8 +12,8 @@ class BackendLoginVerifier(
|
|||||||
private val stringFetcher: StringFetcher,
|
private val stringFetcher: StringFetcher,
|
||||||
private val userInfoCache: UserInfoCache
|
private val userInfoCache: UserInfoCache
|
||||||
) : LoginVerifier {
|
) : LoginVerifier {
|
||||||
override suspend fun verify(login: String) {
|
override suspend fun verify(userId: Long) {
|
||||||
val user = userInfoCache.query(login)
|
val user = userInfoCache.query(userId)
|
||||||
?: throw VerifyTokenException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
?: throw VerifyTokenException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
||||||
user.flags?.let {
|
user.flags?.let {
|
||||||
if ((it and FLAG_USER_BLOCKED) != 0) {
|
if ((it and FLAG_USER_BLOCKED) != 0) {
|
||||||
|
|||||||
@@ -30,4 +30,4 @@ class DbModule {
|
|||||||
fun provideDb(driver: SqlDriver): AppDatabase {
|
fun provideDb(driver: SqlDriver): AppDatabase {
|
||||||
return AppDatabase.Companion.invoke(driver)
|
return AppDatabase.Companion.invoke(driver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ru.shadowsparky.backend.domain.JwtInfo
|
|||||||
import ru.shadowsparky.backend.domain.LoginVerifier
|
import ru.shadowsparky.backend.domain.LoginVerifier
|
||||||
import ru.shadowsparky.backend.domain.RemoteEventHandler
|
import ru.shadowsparky.backend.domain.RemoteEventHandler
|
||||||
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
|
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
|
||||||
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcherFactory
|
import ru.shadowsparky.updater.backend.data.BackendUpdateFetcherFactory
|
||||||
import ru.shadowsparky.vbox.backend.data.auth.AuthTokenRepositoryFactory
|
import ru.shadowsparky.vbox.backend.data.auth.AuthTokenRepositoryFactory
|
||||||
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepositoryFactory
|
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepositoryFactory
|
||||||
import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory
|
import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import ru.shadowsparky.backend.presentation.configureJwt
|
|||||||
import ru.shadowsparky.backend.presentation.obtainUserId
|
import ru.shadowsparky.backend.presentation.obtainUserId
|
||||||
import ru.shadowsparky.vbox.backend.di.AuthEntryPoint
|
import ru.shadowsparky.vbox.backend.di.AuthEntryPoint
|
||||||
|
|
||||||
const val AUTH_JWT_NAME = "auth-jwt"
|
|
||||||
|
|
||||||
fun Application.configureJwt(authEntryPoint: AuthEntryPoint) = with(authEntryPoint) {
|
fun Application.configureJwt(authEntryPoint: AuthEntryPoint) = with(authEntryPoint) {
|
||||||
configureJwt(jwtInfo, tokenVerifier, loginVerifier)
|
configureJwt(jwtInfo, tokenVerifier, loginVerifier)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -7,9 +7,9 @@ import io.ktor.server.response.respond
|
|||||||
import io.ktor.server.routing.Routing
|
import io.ktor.server.routing.Routing
|
||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.routing.post
|
import io.ktor.server.routing.post
|
||||||
|
import ru.shadowsparky.backend.presentation.AUTH_JWT_NAME
|
||||||
import ru.shadowsparky.vbox.backend.di.AuthEntryPoint
|
import ru.shadowsparky.vbox.backend.di.AuthEntryPoint
|
||||||
import ru.shadowsparky.vbox.backend.di.RoutingEntryPoint
|
import ru.shadowsparky.vbox.backend.di.RoutingEntryPoint
|
||||||
import ru.shadowsparky.vbox.backend.presentation.AUTH_JWT_NAME
|
|
||||||
import ru.shadowsparky.vbox.backend.presentation.obtainUserId
|
import ru.shadowsparky.vbox.backend.presentation.obtainUserId
|
||||||
import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository
|
import ru.shadowsparky.vbox.shared.domain.AuthTokenRepository
|
||||||
import ru.shadowsparky.vbox.shared.domain.HealthCheck
|
import ru.shadowsparky.vbox.shared.domain.HealthCheck
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.auth0.jwt.JWT
|
|||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
import ru.shadowsparky.backend.domain.JwtInfo
|
import ru.shadowsparky.backend.domain.JwtInfo
|
||||||
import ru.shadowsparky.backend.domain.LOGIN_NAME
|
|
||||||
import ru.shadowsparky.backend.domain.USER_ID_ARG
|
import ru.shadowsparky.backend.domain.USER_ID_ARG
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -15,15 +14,11 @@ class JwtPreparer(
|
|||||||
private val envFetcher: EnvFetcher
|
private val envFetcher: EnvFetcher
|
||||||
) {
|
) {
|
||||||
private val tokenTimeInMinutes = envFetcher.get("JWT_DURATION", "30").toLong()
|
private val tokenTimeInMinutes = envFetcher.get("JWT_DURATION", "30").toLong()
|
||||||
fun prepare(
|
fun prepare(userId: Long): String {
|
||||||
login: String,
|
|
||||||
userId: Long
|
|
||||||
): String {
|
|
||||||
val exp = Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(tokenTimeInMinutes))
|
val exp = Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(tokenTimeInMinutes))
|
||||||
val token = JWT.create()
|
val token = JWT.create()
|
||||||
.withAudience(jwtInfo.audience)
|
.withAudience(jwtInfo.audience)
|
||||||
.withIssuer(jwtInfo.issuer)
|
.withIssuer(jwtInfo.issuer)
|
||||||
.withClaim(LOGIN_NAME, login)
|
|
||||||
.withClaim(USER_ID_ARG, userId)
|
.withClaim(USER_ID_ARG, userId)
|
||||||
.withSubject(userId.toString())
|
.withSubject(userId.toString())
|
||||||
.withExpiresAt(exp)
|
.withExpiresAt(exp)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ru.shadowsparky.backend.data
|
||||||
|
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
|
||||||
|
fun generateRefreshToken(): String {
|
||||||
|
val refresh = ByteArray(32)
|
||||||
|
SecureRandom().nextBytes(refresh)
|
||||||
|
return refresh.toHexString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toTokenHash(salt: String): String {
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
md.update(salt.toByteArray())
|
||||||
|
val result = md.digest(this.toByteArray())
|
||||||
|
return Base64.encode(result)
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import com.auth0.jwt.algorithms.Algorithm
|
|||||||
import com.auth0.jwt.interfaces.DecodedJWT
|
import com.auth0.jwt.interfaces.DecodedJWT
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
import ru.shadowsparky.backend.domain.JwtInfo
|
import ru.shadowsparky.backend.domain.JwtInfo
|
||||||
import ru.shadowsparky.backend.domain.LOGIN_NAME
|
|
||||||
import ru.shadowsparky.backend.domain.LoginVerifier
|
import ru.shadowsparky.backend.domain.LoginVerifier
|
||||||
|
import ru.shadowsparky.backend.domain.USER_ID_ARG
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
class TokenVerifier(
|
class TokenVerifier(
|
||||||
@@ -22,7 +22,7 @@ class TokenVerifier(
|
|||||||
|
|
||||||
suspend fun verify(token: String): DecodedJWT {
|
suspend fun verify(token: String): DecodedJWT {
|
||||||
val jwt = verifier.verify(token)
|
val jwt = verifier.verify(token)
|
||||||
loginVerifier.verify(jwt.getClaim(LOGIN_NAME).asString())
|
loginVerifier.verify(jwt.getClaim(USER_ID_ARG).asLong())
|
||||||
return jwt
|
return jwt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package ru.shadowsparky.backend.domain
|
package ru.shadowsparky.backend.domain
|
||||||
|
|
||||||
interface LoginVerifier {
|
interface LoginVerifier {
|
||||||
suspend fun verify(login: String)
|
suspend fun verify(userId: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
const val LOGIN_NAME = "login"
|
|
||||||
const val USER_ID_ARG = "user_id"
|
const val USER_ID_ARG = "user_id"
|
||||||
|
|
||||||
const val INVALID_TOKEN_MSG = "Для выполнения этого действия требуется авторизация."
|
const val INVALID_TOKEN_MSG = "Для выполнения этого действия требуется авторизация."
|
||||||
|
|||||||
+3
-4
@@ -16,7 +16,6 @@ import ru.shadowsparky.backend.data.TokenVerifier
|
|||||||
import ru.shadowsparky.backend.domain.ExceptionInfo
|
import ru.shadowsparky.backend.domain.ExceptionInfo
|
||||||
import ru.shadowsparky.backend.domain.INVALID_TOKEN_MSG
|
import ru.shadowsparky.backend.domain.INVALID_TOKEN_MSG
|
||||||
import ru.shadowsparky.backend.domain.JwtInfo
|
import ru.shadowsparky.backend.domain.JwtInfo
|
||||||
import ru.shadowsparky.backend.domain.LOGIN_NAME
|
|
||||||
import ru.shadowsparky.backend.domain.LoginVerifier
|
import ru.shadowsparky.backend.domain.LoginVerifier
|
||||||
import ru.shadowsparky.backend.domain.USER_ID_ARG
|
import ru.shadowsparky.backend.domain.USER_ID_ARG
|
||||||
import ru.shadowsparky.backend.domain.VerifyTokenException
|
import ru.shadowsparky.backend.domain.VerifyTokenException
|
||||||
@@ -44,10 +43,10 @@ fun Application.configureJwt(
|
|||||||
realm = jwtInfo.realm
|
realm = jwtInfo.realm
|
||||||
verifier(tokenVerifier.verifier)
|
verifier(tokenVerifier.verifier)
|
||||||
validate { credential ->
|
validate { credential ->
|
||||||
val login = credential.payload.getClaim(LOGIN_NAME).asString()
|
val userId = credential.payload.getClaim(USER_ID_ARG).asLong()
|
||||||
try {
|
try {
|
||||||
if (login != null) {
|
if (userId != null) {
|
||||||
loginVerifier.verify(login)
|
loginVerifier.verify(userId)
|
||||||
if (credential.payload.expiresAt == null) {
|
if (credential.payload.expiresAt == null) {
|
||||||
throw VerifyTokenException("Static tokens not supported!")
|
throw VerifyTokenException("Static tokens not supported!")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user