Akka HTTP Adapter (csw-aas-http)
This library is a security adapter for Akka HTTP server applications. csw-aas
uses OpenId Connect for authentication and authorization. The authentication server used by AAS is Keycloak. We recommended that you get familiar with Keycloak’s documentation and configurations to fully leverage this adapter’s features.
This adapter provides authentication via security directives such as sGet
, sPost
, sPut
, etc. These directives are used in routing and replace the default get
, post
, put
, etc. directives from Akka HTTP. This allows custom policies to be enforced at the routing level. For authorization, these secure directives accept a wide range of policy expressions. The usage of these directives are described below.
Dependencies
To use the Akka HTTP Adapter (csw-aas-http), add this to your build.sbt
file:
- sbt
-
libraryDependencies += "com.github.tmtsoftware.csw" %% "csw-aas-http" % "5.0.1"
Prerequisites
To run an Akka HTTP server app, which uses this adapter, we need
- The Location Service running
- An AAS instance running and registered with the Location Service
The Location Service and AAS can be running on different machines. To start the Location Service and AAS server on a local machine, you can make use of the csw-services application.
Application Configurations
All auth related configurations go inside an auth-config
block. There are two configurations applicable for an Akka HTTP server application i.e. realm
, & client-id
.
THe realm
has a default value of TMT
, if not specified. Ideally all apps in TMT should not have to override this, however it might be useful to override this while testing your app.
client-id
is a mandatory configuration which specifies the client ID of the app as per its registration in AAS.
disabled
is an optional config with default value of false
. This flag can be turned on for local development and testing purposes. When turned on, all http requests bypass all security policies. Clients don’t have to pass any token in the requests. It can greatly ease the process of testing business logic without having to go through the process of creating users, managing roles and logging in with user credentials to generate valid access tokens.
Please use disabled
flag with caution. If accidentally turned on in production, confidential data can be compromised
auth-config {
realm = TMT # DEFAULT
client-id = demo-cli # REQUIRED
disabled = false # DEFAULT
}
Building a Secure Akka HTTP server application
The core of this adapter is the SecurityDirectives
class. The recommended way to instantiate SecurityDirectives
is as shown below.
- Scala
-
source
object SampleHttpApp extends App { implicit val actorSystem: ActorSystem[SpawnProtocol.Command] = typed.ActorSystem(SpawnProtocol(), "sample-http-app") implicit val ec: ExecutionContext = actorSystem.executionContext private val config = actorSystem.settings.config val locationService = HttpLocationServiceFactory.makeLocalClient val directives = SecurityDirectives(config, locationService) import directives._ def routes: Route = pathPrefix("api") { get { complete("SUCCESS") } ~ sPost(RealmRolePolicy("admin")) { complete("SUCCESS") } } private val host = "0.0.0.0" private val port = 9003 Http().newServerAt(host, port).bind(routes) }
Importing everything from security directives is recommended as it imports some implicit methods along with all security directives.
In the above example,
-
GET http://localhost:9003/api
does not use any security directive and hence is accessible to all. -
POST http://localhost:9003/api
usessPost
which is secure directive. This directive takes care of authentication (access token signature and expiration validation). For authorization, it needs an authorization policy. The authorizing policy specifies one or more conditions for request validation.
In this instance, the sPost
directive has been given a RealmRolePolicy
policy with the parameter value admin
.
This results into following sequence of actions when a request arrives for a secure directive route:
- Check request header to look for an access token
- Validate the token signature and expiry
- Check the token for roles and validate that it has the
admin
realm role - After all the above checks/validations pass, execute the route logic
If any of the validations fails, an appropriate HTTP status code is returned to the requester. For authentication failure, 401
is sent and for authorization failure, 403
is sent.
To know more about realm roles, check out the Keycloak documentation
Authorization Policies
An authorization policy is a way to provide filtering on incoming HTTP requests based on standard rules. Following policies can be applied to protect routes.
RealmRolePolicy
This policy filters requests based on Realm Role. A Realm Role is global and is applicable for all clients within a realm.
In the following example, the policy will authorize a request if the user has been assigned the admin
role
- Scala
-
source
val routeWithRealmRolePolicy: Route = sGet(RealmRolePolicy("admin")) { complete("OK") }
CustomPolicy
This policy allows custom request filtering based on access token properties. It expects a predicate function which accepts an access token and returns a boolean. If the predicate returns true, it indicates the user is authorized.
In the following example, the policy will authorize a request if the user’s given name contains test-user
.
- Scala
-
source
val routeWithCustomPolicy: Route = sPost(CustomPolicy(token => token.given_name.contains("test-user"))) { complete("OK") }
CustomPolicyAsync
This policy is similar to CustomPolicy, with only difference being that it expects a predicate which returns a Future of Boolean instead of a Boolean. This could be very useful for custom validations which need to make an IO call. For example,
- Scala
-
source
// GET http://[host]:[port]/files?fileId=[fileId] val route: Route = path("files" / LongNumber) { fileId => sGet(CustomPolicyAsync(token => Database.doesUserOwnFile(token.preferred_username, fileId))) { complete(Database.getFileContents(fileId)) } }
This forms an HTTP route for a secure GET request for the path /files/[fileId]
and expects a path parameter of type Long
. The async custom policy makes an async database call to check whether the file being requested belongs to the user who made the HTTP request.
EmptyPolicy
This policy is used this when only authentication is needed but not authorization. EmptyPolicy is an object and not a class like other policies and it does not need any parameters.
- Scala
-
source
val authenticationOnlyRoute: Route = // GET http://[host]:[post]/api path("api") { sGet(EmptyPolicy) { complete("OK") } }
Security Directives
The csw-aas-http
adapter supports following secure HTTP verbs:
Name | Description |
---|---|
sGet | Rejects all unauthorized and non-GET requests |
sPost | Rejects all unauthorized and non-POST requests |
sPut | Rejects all unauthorized and non-PUT requests |
sDelete | Rejects all unauthorized and non-DELETE requests |
sHead | Rejects all unauthorized and non-HEAD requests |
sConnect | Rejects all unauthorized and non-CONNECT requests |
Using Access Token
A handle of the access token type is given to all secure routes. It is optional to define a parameter for it.
For example:
- Scala
-
source
val routeExampleWithToken: Route = sDelete(EmptyPolicy) { token => parameter("entityId".as[Long]) { entityId => ThirdPartyService.deleteEntity(entityId, token.preferred_username) complete(s"user ${token.given_name} ${token.family_name} deleted entity $entityId") } } val routeExampleWithoutToken: Route = sDelete(EmptyPolicy) { parameter("entityId".as[Long]) { entityId => ThirdPartyService.deleteEntity(entityId) complete(s"entity $entityId deleted") } }
Both of the above approaches compile and are valid. The access token holds basic information about the user or the client who has made the request.
When disabled
flag is true in auth-config
, all fields of access token will be set to None
Please go through the API documentation to know more about Access Tokens.
Policy Expressions
So far, we have seen that security directives can accept an authorization policy. It can however also accept an expression of multiple authorization policies. This could be useful to express complex authorization logic. For example:
- Scala
-
source
val routes: Route = sGet(RealmRolePolicy("admin") | CustomPolicy(_.email.contains("super-admin@tmt.org"))) { complete("OK") } ~ sPost(RealmRolePolicy("finance_user") & RealmRolePolicy("finance_admin")) { complete("OK") }
Note the |
, &
operators which help compose an expression. A Policy expression could be more complex than this and can contain braces to group more expressions. For example:
val policy = policy1 | (policy2 & (policy3 | policy4)) | policy5
Directive Composition
Since security directives extend from akka.http.scaladsl.server.Directive
, they give you all the benefits of a usual directive. These benefits include being able to label and compose higher level directives.
With the help of directive labeling you could write a route like below:
- Scala
-
source
sGet(RealmRolePolicy("admin")) & sGet(RealmRolePolicy("sales_admin"))
The same can be achieved via Policy Expressions as shown below
- Scala
-
source
sGet(RealmRolePolicy("admin") & RealmRolePolicy("sales_admin"))
If you want to combine two directives and both of them are CSW security directives, we strongly recommend that you use Policy Expressions. The reason for this is that when you combine two CSW security directives, the authentication check happens twice (or multiple times based on how many CSW security directives are combined). Since this was meant to happen only once, it causes performance slowdown. You can however combine CSW security directives with other directives freely without worrying about performance.