Implemented logout
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
package controllers.authentication
|
||||||
|
|
||||||
|
import dao.AuthTokensDAO
|
||||||
|
import jakarta.inject.Inject
|
||||||
|
import models.AuthToken
|
||||||
|
import play.api.libs.json.*
|
||||||
|
import play.api.mvc.Results.Unauthorized
|
||||||
|
import play.api.mvc.{ActionRefiner, Request, Result, WrappedRequest}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class AuthenticatedRequest[A](val token: AuthToken, request: Request[A]) extends WrappedRequest[A](request)
|
||||||
|
|
||||||
|
class AuthenticatedAction @Inject()(authTokensDao: AuthTokensDAO)(implicit val executionContext: ExecutionContext) extends ActionRefiner[Request, AuthenticatedRequest] {
|
||||||
|
private final val tokenPrefix = "Token"
|
||||||
|
|
||||||
|
override protected def refine[A](request: Request[A]): Future[Either[Result, AuthenticatedRequest[A]]] = {
|
||||||
|
val unauthenticatedRequestError = Left(Unauthorized(Json.obj("error" -> "Unauthenticated")))
|
||||||
|
val authHeader = request.headers.get("Authorization")
|
||||||
|
.flatMap { header =>
|
||||||
|
if header.startsWith(tokenPrefix) then Some(header.substring(tokenPrefix.length).trim)
|
||||||
|
else None
|
||||||
|
}
|
||||||
|
.filter(_.nonEmpty)
|
||||||
|
|
||||||
|
authHeader match {
|
||||||
|
case Some(token) =>
|
||||||
|
authTokensDao.find(token).map {
|
||||||
|
case Some(authToken) => Right(AuthenticatedRequest[A](authToken, request))
|
||||||
|
case None => unauthenticatedRequestError
|
||||||
|
}
|
||||||
|
case None => Future.successful(unauthenticatedRequestError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,4 +40,8 @@ class SessionsController @Inject()(controllerComponents: ControllerComponents, u
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def logout = Action.andThen(new AuthenticatedAction(authTokensDAO)).async { request =>
|
||||||
|
authTokensDAO.destroy(request.token).map{ _ => Ok("") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,30 @@ package dao
|
|||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
||||||
import slick.jdbc.PostgresProfile
|
import slick.jdbc.PostgresProfile
|
||||||
import slick.jdbc.PostgresProfile.api._
|
import slick.jdbc.PostgresProfile.api.*
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import java.time.{Instant, Duration}
|
import java.time.{Duration, Instant}
|
||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import models.AuthToken
|
import models.AuthToken
|
||||||
import schemas.AuthTokens
|
import schemas.AuthTokens
|
||||||
|
|
||||||
|
import scala.language.postfixOps
|
||||||
|
|
||||||
class AuthTokensDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit val ec: ExecutionContext) extends HasDatabaseConfigProvider[PostgresProfile] {
|
class AuthTokensDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit val ec: ExecutionContext) extends HasDatabaseConfigProvider[PostgresProfile] {
|
||||||
private lazy val random = SecureRandom()
|
private lazy val random = SecureRandom()
|
||||||
private lazy val base64 = Base64.getEncoder
|
private lazy val base64 = Base64.getEncoder
|
||||||
private final lazy val tokenValidDuration = Duration.ofDays(30)
|
private final lazy val tokenValidDuration = Duration.ofDays(30)
|
||||||
|
|
||||||
|
def find(token: String): Future[Option[AuthToken]] = {
|
||||||
|
val query = AuthTokens.filter(row => row.token === token && row.expiresAt > Instant.now()).take(1).result.headOption
|
||||||
|
db.run(query)
|
||||||
|
}
|
||||||
|
|
||||||
def createToken(user: models.User): Future[AuthToken] = {
|
def createToken(user: models.User): Future[AuthToken] = {
|
||||||
val tokenRaw = ByteBuffer.allocate(10)
|
val tokenRaw = ByteBuffer.allocate(12)
|
||||||
random.nextBytes(tokenRaw.array())
|
random.nextBytes(tokenRaw.array())
|
||||||
val token = AuthToken(
|
val token = AuthToken(
|
||||||
id = None,
|
id = None,
|
||||||
@@ -32,4 +38,9 @@ class AuthTokensDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProv
|
|||||||
|
|
||||||
db.run((AuthTokens returning AuthTokens.map(_.id)) += token).map { tokenId => token.copy(id = Some(tokenId)) }
|
db.run((AuthTokens returning AuthTokens.map(_.id)) += token).map { tokenId => token.copy(id = Some(tokenId)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def destroy(token: AuthToken): Future[Unit] = {
|
||||||
|
val q = AuthTokens.filter(_.token === token.token).delete
|
||||||
|
db.run(q).map{ _ => () }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- !Ups
|
||||||
|
ALTER TABLE auth_tokens ADD CONSTRAINT auth_tokens_token_unique UNIQUE (token);
|
||||||
|
|
||||||
|
-- !Downs
|
||||||
|
ALTER TABLE auth_tokens DROP CONSTRAINT auth_tokens_token_unique;
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
GET / controllers.HomeController.index()
|
GET / controllers.HomeController.index()
|
||||||
|
|
||||||
POST /login controllers.authentication.SessionsController.login()
|
POST /login controllers.authentication.SessionsController.login()
|
||||||
|
# TODO: enable it later
|
||||||
|
+nocsrf
|
||||||
|
DELETE /logout controllers.authentication.SessionsController.logout()
|
||||||
|
|
||||||
# Map static resources from the /public folder to the /assets URL path
|
# Map static resources from the /public folder to the /assets URL path
|
||||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
||||||
|
|||||||
Reference in New Issue
Block a user