diff --git a/app/controllers/authentication/AuthenticatedAction.scala b/app/controllers/authentication/AuthenticatedAction.scala new file mode 100644 index 0000000..0938b50 --- /dev/null +++ b/app/controllers/authentication/AuthenticatedAction.scala @@ -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) + } + } +} diff --git a/app/controllers/authentication/SessionsController.scala b/app/controllers/authentication/SessionsController.scala index 50c7218..56b130b 100644 --- a/app/controllers/authentication/SessionsController.scala +++ b/app/controllers/authentication/SessionsController.scala @@ -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("") } + } } diff --git a/app/dao/AuthTokensDAO.scala b/app/dao/AuthTokensDAO.scala index 39f73c7..b699b82 100644 --- a/app/dao/AuthTokensDAO.scala +++ b/app/dao/AuthTokensDAO.scala @@ -3,24 +3,30 @@ package dao import jakarta.inject.Inject import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider} import slick.jdbc.PostgresProfile -import slick.jdbc.PostgresProfile.api._ +import slick.jdbc.PostgresProfile.api.* import java.nio.ByteBuffer import java.security.SecureRandom import java.util.Base64 -import java.time.{Instant, Duration} - +import java.time.{Duration, Instant} import scala.concurrent.{ExecutionContext, Future} import models.AuthToken import schemas.AuthTokens +import scala.language.postfixOps + class AuthTokensDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit val ec: ExecutionContext) extends HasDatabaseConfigProvider[PostgresProfile] { private lazy val random = SecureRandom() private lazy val base64 = Base64.getEncoder 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] = { - val tokenRaw = ByteBuffer.allocate(10) + val tokenRaw = ByteBuffer.allocate(12) random.nextBytes(tokenRaw.array()) val token = AuthToken( 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)) } } + + def destroy(token: AuthToken): Future[Unit] = { + val q = AuthTokens.filter(_.token === token.token).delete + db.run(q).map{ _ => () } + } } diff --git a/conf/evolutions/default/2.sql b/conf/evolutions/default/2.sql new file mode 100644 index 0000000..317d66e --- /dev/null +++ b/conf/evolutions/default/2.sql @@ -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; \ No newline at end of file diff --git a/conf/routes b/conf/routes index 4db9750..caacc98 100644 --- a/conf/routes +++ b/conf/routes @@ -7,6 +7,9 @@ GET / controllers.HomeController.index() 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 GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)