This library provides support for using OAuth2 for authentication, mainly for backends, that use the Play framework (2.4). Additionally, it brings
some support for logging and flow ids that can be used to follow requests through several microservices. It should
not require much setup before using it - see Configuration
for more details.
The name stems from the german mining term Hutmann, who was responsible to make sure that no unauthorized person drove into the pit.
- OAuth2 handling for backend services
- Extended logging functionality
- Flow ID handling
We tried to minimize the needed user setup if you want to have a working minimal solution. There is a special play
Action
that you can use to mark services that require authentication, so places that have beforehand been like
import org.zalando.hutmann.authentication.OAuth2Action
import play.api.mvc._
import play.api.mvc.Results._
def heartbeat = Action {
Ok("<3")
}
become
import scala.concurrent.ExecutionContext
import play.api.libs.ws.WSClient
import play.api.Configuration
//these come from the application normally
implicit val ws: WSClient = null
implicit val config: Configuration = null
import scala.concurrent.ExecutionContext.Implicits.global
def heartbeat = OAuth2Action()(implicitly[ExecutionContext], implicitly[WSClient], implicitly[Configuration]) {
Ok("<3")
}
or use(recommended) EssentialAction to authenticate request first and then proceed with everything else like request body parsing etc. side effects of not using EssentialAction is explained in detail in this issue
import scala.concurrent.ExecutionContext
import play.api.libs.ws.WSClient
import play.api.Configuration
import scala.util.Future
//these come from the application normally
implicit val ws: WSClient = null
implicit val config: Configuration = null
import scala.concurrent.ExecutionContext.Implicits.global
def heartbeat = OAuth2Action()(implicitly[ExecutionContext], implicitly[WSClient], implicitly[Configuration]).essentialAction(parse.default) {
Future.successful(Ok("<3"))
}
and it automatically makes sure that requests to that route have a valid authentication token - although most probably, you won't make your heartbeat endpoint secured.
tell something about the other frameworks etc
The library currently only works with Play 2.4. It depends on its JSON and WS libraries, which your project must bring.
Versioning follows the Play version number it works with. 2.4.x therefore is a version that works with Play 2.4, 2.5.x (if any) works with Play 2.5.
libraryDependencies += "org.zalando" %% "hutmann" % "2.5.1"
If you are a user of the Zalando OAuth2 infrastructure, you don't need to configure anything. The following configuration keys may be interesting though:
org.zalando.hutmann.authentication.oauth2: {
tokenInfoUrl: "https://info.services.auth.zalando.com/oauth2/tokeninfo"
tokenQueryParam: "access_token"
}
The OAuth2Action
is used to secure your backend endpoints. It is made to return simple error messages, but you are free to extend the
implementation if you need it by simply subclassing the Action. Besides the simple usage example that you already found above, this is a
more elaborate example:
import org.zalando.hutmann.authentication.Filters._
def heartbeat = OAuth2Action(scope("myservice.read"))(implicitly[ExecutionContext], implicitly[WSClient], implicitly[Configuration]) {
Ok("<3")
}
will only allow the users with scope myservice.read
, and only if he has a valid access token, while
def heartbeat = OAuth2Action(isEmployee)(implicitly[ExecutionContext], implicitly[WSClient], implicitly[Configuration]) {
Ok("<3")
}
will check if the token is from realm "/employees" and has a scope "uid" property set.
In both cases, the body of the action will only get called when the user may access it, and error responses are generated accordingly. If you do not want this behaviour, but care yourself for the handling, you have two possibilities:
- You can extend
OAuth2Action
in your project andoverride def autoRejectBehaviour
, which allows you to specify in which case you want you want what behaviour. This is the recommended approach. - You can set
autoReject = false
as a parameter, and have a look at the request propertyuser
which is either a user, or anAuthorizationProblem
. This is basically "doing everything by hand".
The default timeout for contacting the server is 1 second, a request will be repeated once in case a timeout occurs, with circuit breakers applied.
You can do so by subclassing OAuth2Action and override def validateToken
.
Just create this in your project:
import scala.concurrent.duration._
import scala.concurrent.Future
import com.typesafe.config.Config
import org.zalando.hutmann.authentication._
trait Auth {
def authAction(
filter: User => Future[Boolean] = { user: User => Future.successful(true) },
autoReject: Boolean = true,
requestTimeout: Duration = 1.second
)(implicit config: Config, ec: ExecutionContext, ws: WSClient): OAuth2Action =
new OAuth2Action(filter, autoReject, requestTimeout)
}
trait FakeAuth extends Auth {
val userResult: Either[AuthorizationProblem, User]
override def authAction(
filter: User => Future[Boolean] = { user: User => Future.successful(true) },
autoReject: Boolean = true,
requestTimeout: Duration = 1.second
)(implicit config: Config, ec: ExecutionContext, ws: WSClient): OAuth2Action =
new OAuth2Action(filter, autoReject, requestTimeout) {
override def transform[A](request: Request[A]): Future[UserRequest[A]] =
Future.successful(new UserRequest(userResult, request)(request))
}
}
You now only need to mix in Auth
in your controller, and FakeAuth
in your controller in your tests, additionally provide the
result you'd like to have in the FakeAuth
.
Extend OAuth2Action
and override autoRejectBehaviour
. You can copy the initial implementation and adjust for your needs.
Play filters are used to run right after the routing and before invoking the action. This makes them particularly useful for cases like
- Logging request information (headers and response codes)
- Adding flow information (flow ids) to the requests
Refer to the Play documentation on how to add filters to your project.
This one is used to log headers (including flow ids) and response codes of your requests, as well as the duration between an incoming request and the outgoing response.
What is a Flow ID? A flow id is a specific header that a service consumes and sends to all connected services that are called from the same flow. This flow id shall additionally be added to all log entries of your services. This way, you can track your requests through your microservice infrastructure.
The FlowIdFilter
inspects the requests that come in if they have a flow id and - depending on the configuration - either adds one if it there is none,
or rejects the request.
There are four FlowIdFilter implementations:
final class CreateFlowIdFilter
final class StrictFlowIdFilter
final class MdcCreateFlowIdFilter
final class MdcStrictFlowIdFilter
The two last ones sets the Flow Id value in the SLF4J MDC, so that it can be used in the logs just adding %X{X-Flow-ID}
to the log pattern in yout logback.xml file.
In to your filters file:
import javax.inject.Inject
import play.api.http.DefaultHttpFilters
import org.zalando.hutmann.filters.[FlowIdFilterImplementation]
class Filters @Inject() (
flowIdFilter: [FlowIdFilterImplementation],
/*...your other filters ... */
) extends DefaultHttpFilters(flowIdFilter, /*...*/)
The Mdc FlowIdFilter implementations works capturing the request in a thread local, making it available to any code executed by the Play default execution context. In order to make it works you need to configure the akka dispatcher as following:
akka {
actor {
default-dispatcher {
type: org.zalando.hutmann.dispatchers.ContextPropagatingDispatcherConfigurator
}
}
}
And use the Execution context injected by play
You can use the integrated logger in your projects as well, and benefit from the automatic context evaluation that will take care that you have your flow ids in every log entry.
import org.zalando.hutmann.logging._
import play.api.libs.json._
object MyController extends Controller {
def doSomething(implicit context: Context): Unit = {
Logger.warn("watch out!")
}
def createSession: Action[JsValue] = OAuth2Action()(implicitly[ExecutionContext], implicitly[WSClient], implicitly[Configuration])(parse.tolerantJson) { request =>
implicit val context: Context = request //there is an implicit conversion for the request
doSomething
Logger.info("some message")
Created
}
}
MyController.createSession(FakeRequest().withJsonBody(Json.obj()))
09:52:00.578 [pool-1-thread-1-ScalaTest-running-LoggerDemo] WARN application - watch out! - 1ms/TheSourceFile.scala:15/THEFLOWID
09:52:00.578 [pool-1-thread-1-ScalaTest-running-LoggerDemo] INFO application - some message - 1ms/TheSourceFile.scala:20/THEFLOWID
will automatically log the passed time since the beginning of the flow (creation of the context), the flow id, and the source code line where the log event was created.