rewrite authentication flow
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package ru.shadowsparky.vbox.backend.di
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.annotation.Single
|
||||
import ru.shadowsparky.backend.data.TokenVerifier
|
||||
import ru.shadowsparky.backend.domain.JwtInfo
|
||||
@@ -34,7 +35,8 @@ class RoutingEntryPoint(
|
||||
@Single
|
||||
class WebSocketEntryPoint(
|
||||
val tokenVerifier: TokenVerifier,
|
||||
val sessionRegistry: SessionRegistry
|
||||
val sessionRegistry: SessionRegistry,
|
||||
val json: Json
|
||||
)
|
||||
|
||||
@Single
|
||||
|
||||
+60
-2
@@ -1,5 +1,8 @@
|
||||
package ru.shadowsparky.vbox.backend.presentation
|
||||
|
||||
import com.auth0.jwt.exceptions.TokenExpiredException
|
||||
import com.auth0.jwt.interfaces.DecodedJWT
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.routing.routing
|
||||
@@ -11,8 +14,16 @@ import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.shadowsparky.backend.data.TokenVerifier
|
||||
import ru.shadowsparky.http.domain.HttpException
|
||||
import ru.shadowsparky.vbox.backend.data.SessionRegistry
|
||||
import ru.shadowsparky.vbox.backend.data.eventLogger
|
||||
import ru.shadowsparky.vbox.backend.di.WebSocketEntryPoint
|
||||
import ru.shadowsparky.vbox.shared.domain.AuthRequest
|
||||
import ru.shadowsparky.vbox.shared.domain.AuthResponse
|
||||
import ru.shadowsparky.vbox.shared.domain.RecentlyWatchedRepository
|
||||
import ru.shadowsparky.vbox.shared.domain.RemoteEventHandler
|
||||
import ru.shadowsparky.vbox.shared.domain.SavedMovieRepository
|
||||
@@ -40,8 +51,7 @@ fun Application.configureWebSocket(socketEntryPoint: WebSocketEntryPoint) = with
|
||||
webSocket(it) { awaitCancellation() }
|
||||
}
|
||||
webSocket(RemoteEventHandler.ON_EVENT) {
|
||||
val frame = (incoming.receive() as Frame.Text).readText()
|
||||
val userId = tokenVerifier.verify(frame).getClaim(USER_ID_ARG).asLong()
|
||||
val userId = incoming.authFlow(json, tokenVerifier, outgoing)
|
||||
val session = SessionRegistry.Writer { text -> outgoing.trySend(Frame.Text(text)) }
|
||||
sessionRegistry.put(userId, session)
|
||||
val deferred = CompletableDeferred<Unit?>()
|
||||
@@ -54,3 +64,51 @@ fun Application.configureWebSocket(socketEntryPoint: WebSocketEntryPoint) = with
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ReceiveChannel<Frame>.receiveTextOrNull(): String? {
|
||||
val frame = receive()
|
||||
return if (frame is Frame.Text) frame.readText() else null
|
||||
}
|
||||
|
||||
private suspend fun ReceiveChannel<Frame>.authFlow(
|
||||
json: Json,
|
||||
tokenVerifier: TokenVerifier,
|
||||
sendChannel: SendChannel<Frame>
|
||||
): Long {
|
||||
val rawRequest = receiveTextOrNull() ?: throw HttpException(HttpStatusCode.BadRequest)
|
||||
val request = runCatching { json.decodeFromString<AuthRequest>(rawRequest) }.getOrNull()
|
||||
val decodedJwt = if (request == null) {
|
||||
tokenVerifier.verify(rawRequest)
|
||||
} else {
|
||||
authFlowV2(request, json, tokenVerifier, sendChannel)
|
||||
}
|
||||
return decodedJwt.getClaim(USER_ID_ARG).asLong()
|
||||
}
|
||||
|
||||
private const val MAX_ATTEMPTS = 3
|
||||
|
||||
private suspend fun ReceiveChannel<Frame>.authFlowV2(
|
||||
initialRequest: AuthRequest,
|
||||
json: Json,
|
||||
tokenVerifier: TokenVerifier,
|
||||
sendChannel: SendChannel<Frame>
|
||||
): DecodedJWT {
|
||||
var currentRequest = initialRequest
|
||||
repeat(MAX_ATTEMPTS) { attempt ->
|
||||
try {
|
||||
val jwt = tokenVerifier.verify(currentRequest.token)
|
||||
sendChannel.send(Frame.Text(json.encodeToString(AuthResponse(true))))
|
||||
return jwt
|
||||
} catch (e: TokenExpiredException) {
|
||||
eventLogger.error("token expired. attempt=$attempt", e)
|
||||
if (attempt == MAX_ATTEMPTS - 1) return@repeat
|
||||
sendChannel.send(Frame.Text(json.encodeToString(AuthResponse(false, e.message))))
|
||||
val nextRaw = receiveTextOrNull() ?: return@repeat
|
||||
currentRequest = json.decodeFromString<AuthRequest>(nextRaw)
|
||||
} catch (e: Exception) {
|
||||
eventLogger.error("unable to verify token", e)
|
||||
throw HttpException(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
}
|
||||
throw HttpException(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ 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.shared.domain.AuthTokenRepository
|
||||
import ru.shadowsparky.vbox.shared.domain.HeathCheck
|
||||
import ru.shadowsparky.vbox.shared.domain.HealthCheck
|
||||
|
||||
fun Routing.setupAuthMethods(
|
||||
routingEntryPoint: RoutingEntryPoint,
|
||||
@@ -40,7 +40,7 @@ fun Routing.setupAuthMethods(
|
||||
setupTagsRouting(userTagFactory, movieTagFactory)
|
||||
setupUpdates(updateFetcherFactory)
|
||||
setupChat(chatRepositoryFactory, processUserMessageUseCase, remoteEventHandler)
|
||||
get(HeathCheck.PATH) { call.respond(HttpStatusCode.OK) }
|
||||
get(HealthCheck.PATH) { call.respond(HttpStatusCode.OK) }
|
||||
post(AuthTokenRepository.CHANGE_PASS_PATH) {
|
||||
authEntryPoint.authTokenRepositoryFactory.create(call.obtainUserId())
|
||||
.changePassword(call.receive())
|
||||
|
||||
Reference in New Issue
Block a user