Skip to content

Commit

Permalink
alerts-server: Remove auth related dependencies from AbstractJsonCont…
Browse files Browse the repository at this point in the history
…roller

This is a part of a big refactor with the goal to allow the use of the
AbstractJsonController in different applications.

The changes include:
- AbstractJsonController is no longer tied to our UserId model,
  now it is possible to use any custom model supported by the
  custom AbstractAuthenticatorService.
- AbstractJsonController is no longer tied to our JWTService,
  now any strategy could be implemented in the custom
  AbstractAuthenticatorService.
  • Loading branch information
AlexITC committed Feb 24, 2018
1 parent 43aab04 commit 1889ca8
Show file tree
Hide file tree
Showing 19 changed files with 252 additions and 152 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.alexitc.coinalerts.commons

import play.api.libs.json.JsValue
import play.api.mvc.Request

/**
* Allow to authenticate a request and map it to a value with type [[T]].
*
* A dummy service that gets the user id from the "Authorization" header or uses -1
* when the header is not present could look like this, NEVER USE THIS PIECE OF CODE:
* {{{
* class DummyAuthenticatorService extends AbstractAuthenticatorService {
* override def authenticate[A](request: Request[A]): FutureApplicationResult[Int] = {
* val userId = request.headers.get(HeaderNames.AUTHORIZATION).map(_.toInt).getOrElse(-1)
* Future.successful { Good(userId) }
* }
* }
* }}}
*
* @tparam A the type representing what is useful to use in your controllers as the user or credentials.
*/
trait AbstractAuthenticatorService[A] {

def authenticate(request: Request[JsValue]): FutureApplicationResult[A]

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package com.alexitc.coinalerts.commons

import javax.inject.Inject

import com.alexitc.coinalerts.commons.FutureOr.Implicits.{FutureOps, OptionOps, OrOps}
import com.alexitc.coinalerts.core.{AuthorizationToken, ErrorId, MessageKey}
import com.alexitc.coinalerts.commons.FutureOr.Implicits.{FutureOps, OrOps}
import com.alexitc.coinalerts.core.{ErrorId, MessageKey}
import com.alexitc.coinalerts.errors._
import com.alexitc.coinalerts.models.UserId
import org.scalactic.TypeCheckedTripleEquals._
import org.scalactic.{Bad, Every, Good}
import org.slf4j.LoggerFactory
import play.api.i18n.Lang
Expand All @@ -22,8 +20,10 @@ import scala.util.control.NonFatal
*
* The controller handles the json serialization and deserialization as well
* as the error responses and http status codes.
*
* @tparam A the value type for an authenticated request, like User or UserId.
*/
abstract class AbstractJsonController @Inject() (components: JsonControllerComponents)
abstract class AbstractJsonController[A] @Inject() (components: JsonControllerComponents[A])
extends MessagesBaseController {

protected val logger = LoggerFactory.getLogger(this.getClass)
Expand Down Expand Up @@ -125,18 +125,14 @@ abstract class AbstractJsonController @Inject() (components: JsonControllerCompo
*/
def authenticatedWithInput[R: Reads, M](
successStatus: Status)(
block: AuthenticatedRequestContextWithModel[R] => FutureApplicationResult[M])(
block: AuthenticatedRequestContextWithModel[A, R] => FutureApplicationResult[M])(
implicit tjs: Writes[M]): Action[JsValue] = Action.async(parse.json) { request =>

val lang = messagesApi.preferred(request).lang
val result = for {
authorizationHeader <- request.headers
.get(AUTHORIZATION)
.toFutureOr(InvalidJWTError)

userId <- validateJWT(authorizationHeader).toFutureOr
input <- validate[R](request.body).toFutureOr
context = AuthenticatedRequestContextWithModel(userId, input, lang)
authValue <- components.authenticatorService.authenticate(request).toFutureOr
context = AuthenticatedRequestContextWithModel(authValue, input, lang)
output <- block(context).toFutureOr
} yield output

Expand All @@ -147,7 +143,7 @@ abstract class AbstractJsonController @Inject() (components: JsonControllerCompo
* Sets a default successStatus.
*/
def authenticatedWithInput[R: Reads, M](
block: AuthenticatedRequestContextWithModel[R] => FutureApplicationResult[M])(
block: AuthenticatedRequestContextWithModel[A, R] => FutureApplicationResult[M])(
implicit tjs: Writes[M]): Action[JsValue] = {

authenticatedWithInput[R, M](Ok)(block)
Expand All @@ -166,17 +162,13 @@ abstract class AbstractJsonController @Inject() (components: JsonControllerCompo
*/
def authenticatedNoInput[M](
successStatus: Status)(
block: AuthenticatedRequestContext => FutureApplicationResult[M])(
block: AuthenticatedRequestContext[A] => FutureApplicationResult[M])(
implicit tjs: Writes[M]): Action[JsValue] = Action.async(EmptyJsonParser) { request =>

val lang = messagesApi.preferred(request).lang
val result = for {
authorizationHeader <- request.headers
.get(AUTHORIZATION)
.toFutureOr(InvalidJWTError)

userId <- validateJWT(authorizationHeader).toFutureOr
context = AuthenticatedRequestContext(userId, lang)
authValue <- components.authenticatorService.authenticate(request).toFutureOr
context = AuthenticatedRequestContext(authValue, lang)
output <- block(context).toFutureOr
} yield output

Expand All @@ -187,27 +179,12 @@ abstract class AbstractJsonController @Inject() (components: JsonControllerCompo
* Sets a default successStatus.
*/
def authenticatedNoInput[M](
block: AuthenticatedRequestContext => FutureApplicationResult[M])(
block: AuthenticatedRequestContext[A] => FutureApplicationResult[M])(
implicit tjs: Writes[M]): Action[JsValue] = {

authenticatedNoInput[M](Ok)(block)
}

private def validateJWT(authorizationHeader: String): ApplicationResult[UserId] = {
val tokenType = "Bearer"
val headerParts = authorizationHeader.split(" ")

Option(headerParts)
.filter(_.length === 2)
.filter(_.head === tokenType)
.map(_.drop(1).head)
.map(AuthorizationToken.apply)
.map { token =>
components.jwtService.decodeToken(token).map(_.id)
}
.getOrElse(Bad(InvalidJWTError).accumulating)
}

private def validate[R: Reads](json: JsValue): ApplicationResult[R] = {
json.validate[R].fold(
invalid => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package com.alexitc.coinalerts.commons

import com.alexitc.coinalerts.services.JWTService
import play.api.mvc.MessagesControllerComponents

import scala.concurrent.ExecutionContext

trait JsonControllerComponents {
trait JsonControllerComponents[A] {

def messagesControllerComponents: MessagesControllerComponents

// TODO: allow to override it
def jwtService: JWTService

def executionContext: ExecutionContext

def publicErrorRenderer: PublicErrorRenderer

def applicationErrorMapper: ApplicationErrorMapper

def authenticatorService: AbstractAuthenticatorService[A]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.alexitc.coinalerts.commons

import com.alexitc.coinalerts.models.UserId
import play.api.i18n.Lang

sealed trait RequestContext {
Expand All @@ -15,6 +14,6 @@ final case class PublicRequestContext(lang: Lang) extends RequestContext
final case class PublicRequestContextWithModel[T](model: T, lang: Lang)
extends RequestContext with HasModel[T]

final case class AuthenticatedRequestContext(userId: UserId, lang: Lang) extends RequestContext
final case class AuthenticatedRequestContextWithModel[T](userId: UserId, model: T, lang: Lang)
final case class AuthenticatedRequestContext[A](auth: A, lang: Lang) extends RequestContext
final case class AuthenticatedRequestContextWithModel[A, T](auth: A, model: T, lang: Lang)
extends RequestContext with HasModel[T]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.alexitc.coinalerts.services

import javax.inject.Inject

import com.alexitc.coinalerts.commons.{AbstractAuthenticatorService, ApplicationResult, FutureApplicationResult}
import com.alexitc.coinalerts.core.AuthorizationToken
import com.alexitc.coinalerts.errors.{AuthorizationHeaderRequiredError, InvalidJWTError}
import com.alexitc.coinalerts.models.UserId
import org.scalactic.{Bad, One, Or}
import play.api.http.HeaderNames
import play.api.libs.json.JsValue
import play.api.mvc.Request

import scala.concurrent.Future

class JWTAuthenticatorService @Inject() (jwtService: JWTService) extends AbstractAuthenticatorService[UserId] {

override def authenticate(request: Request[JsValue]): FutureApplicationResult[UserId] = {
val result = for {
authorizationHeader <- Or.from(
request.headers.get(HeaderNames.AUTHORIZATION),
One(AuthorizationHeaderRequiredError))

userId <- decodeAuthorizationHeader(authorizationHeader)
} yield userId

Future.successful(result)
}

private def decodeAuthorizationHeader(header: String): ApplicationResult[UserId] = {
val tokenType = "Bearer"
val headerParts = header.split(" ")

Option(headerParts)
.filter(_.length == 2)
.filter(tokenType equalsIgnoreCase _.head)
.map(_.drop(1).head)
.map(AuthorizationToken.apply)
.map { token =>
jwtService.decodeToken(token).map(_.id)
}
.getOrElse(Bad(InvalidJWTError).accumulating)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ class DailyPriceAlertsController @Inject() (
extends MyJsonController(components) {

def create() = authenticatedWithInput { context: AuthCtxModel[CreateDailyPriceAlertModel] =>
dailyPriceAlertService.create(context.userId, context.model)
dailyPriceAlertService.create(context.auth, context.model)
}

def getAlerts(query: PaginatedQuery) = authenticatedNoInput { context: AuthCtx =>
dailyPriceAlertService.getAlerts(context.userId, query)
dailyPriceAlertService.getAlerts(context.auth, query)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ class FixedPriceAlertsController @Inject() (
extends MyJsonController(components) {

def create() = authenticatedWithInput(Created) { context: AuthCtxModel[CreateFixedPriceAlertModel] =>
alertService.create(context.model, context.userId)
alertService.create(context.model, context.auth)
}

def getAlerts(
query: PaginatedQuery,
filterQuery: FilterQuery,
orderByQuery: OrderByQuery) = authenticatedNoInput { context: AuthCtx =>

alertService.getAlerts(context.userId, query, filterQuery, orderByQuery)
alertService.getAlerts(context.auth, query, filterQuery, orderByQuery)
}

def delete(id: FixedPriceAlertId) = authenticatedNoInput { context: AuthCtx =>
alertService.delete(id, context.userId)
alertService.delete(id, context.auth)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import javax.inject.Inject

import com.alexitc.coinalerts.commons.{JsonControllerComponents, PublicErrorRenderer}
import com.alexitc.coinalerts.errors.MyApplicationErrorMapper
import com.alexitc.coinalerts.services.JWTService
import com.alexitc.coinalerts.models.UserId
import com.alexitc.coinalerts.services.JWTAuthenticatorService
import play.api.mvc.MessagesControllerComponents

import scala.concurrent.ExecutionContext

class MyJsonControllerComponents @Inject() (
override val messagesControllerComponents: MessagesControllerComponents,
override val jwtService: JWTService,
override val executionContext: ExecutionContext,
override val publicErrorRenderer: PublicErrorRenderer,
override val applicationErrorMapper: MyApplicationErrorMapper)
extends JsonControllerComponents
override val applicationErrorMapper: MyApplicationErrorMapper,
override val authenticatorService: JWTAuthenticatorService)
extends JsonControllerComponents[UserId]
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ class NewCurrencyAlertsController @Inject() (
extends MyJsonController(components) {

def create(exchange: Exchange) = authenticatedNoInput(Created) { context =>
service.create(context.userId, exchange)
service.create(context.auth, exchange)
}

def get() = authenticatedNoInput { context =>
service.get(context.userId)
service.get(context.auth)
}

def delete(exchange: Exchange) = authenticatedNoInput { context =>
service.delete(context.userId, exchange)
service.delete(context.auth, exchange)
}
}
6 changes: 3 additions & 3 deletions alerts-server/app/controllers/UsersController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ class UsersController @Inject() (
}

def whoAmI() = authenticatedNoInput { context: AuthCtx =>
userService.userById(context.userId)
userService.userById(context.auth)
}

def getPreferences() = authenticatedNoInput { context =>
userService.getPreferences(context.userId)
userService.getPreferences(context.auth)
}

def setPreferences() = authenticatedWithInput { context: AuthCtxModel[SetUserPreferencesModel] =>
userService.setPreferences(context.userId, context.model)
userService.setPreferences(context.auth, context.model)
}
}
5 changes: 3 additions & 2 deletions alerts-server/app/controllers/package.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@

import com.alexitc.coinalerts.commons.{AuthenticatedRequestContext, AuthenticatedRequestContextWithModel, PublicRequestContext, PublicRequestContextWithModel}
import com.alexitc.coinalerts.models.UserId

package object controllers {
/**
* These type alias help to not type the long request context names in the controllers
*/
type PublicCtx = PublicRequestContext
type PublicCtxModel[T] = PublicRequestContextWithModel[T]
type AuthCtx = AuthenticatedRequestContext
type AuthCtxModel[T] = AuthenticatedRequestContextWithModel[T]
type AuthCtx = AuthenticatedRequestContext[UserId]
type AuthCtxModel[T] = AuthenticatedRequestContextWithModel[UserId, T]
}
Loading

0 comments on commit 1889ca8

Please sign in to comment.