Blog

Play authentication and authorization with Silhouette + rate limiting

We assume you are already familiar with Scala v2.11, Play v2.5, and have basic knowledge of ReactiveMongo.

This sample app demonstrates how to implement authentication and authorization using Silhouette library v4 and MongoDB for persistence.
We also show a simple approach to rate limiting. The code is available at our GitHub repo.

JSON Web Tokens


This app uses JWT(JSON Web Tokens) based authentication. It has many advantages:

  • stateless, self-contained, so there is no need for storing tokens in a db
  • no need for CSRF protection since the browser doesn’t automatically add the header to your request
  • CORS friendly
  • mobile ready
  • standardized

One notable disadvantage is dealing with token invalidation(when user logs out), explained in this answer. We won’t go into details, but if you want to learn more, see here and here and here.

The flow is simple:

  • When user signs up or signs in with its credentials(usually an email and password), he gets a token.
  • Every following secured request must have this token in HTTP header(e.g. “X-Auth-Token” : “token”, configurable in silhouette.conf).

Note: This token is usually stored on client side in browser local storage (example with AngularJS), or less preferable, in cookie.

Silhouette


Silhouette is an authentication library for Play apps, supports many authentication methods: OAuth1, OAuth2, OpenID, CAS, Credentials, Basic Authentication, Two Factor Authentication or custom authentication schemes. Starting top-down we will try to explain essential Silhouette parts.

Silhouette[Environment]

This is the top level class that represents Silhouette “stack”. It is supposed to be injected in controllers to provide all Silhouette actions. In our class AppController :

@Inject() (silhouette: Silhouette[DefaultEnv] ...)

Environment

Defines key components used for user authentication, which authenticator to use(JWT, Cookie, Bearer token etc.). Environment consists of two types, Identity(represents a User) and Authenticator(user validator). You can have multiple environments in one app, and consequently, many Silhouette[Environment] types.

The DefaultEnv(in example above) is our environment that connects User type and Silhouette’s JWTAuthenticator:

trait DefaultEnv extends Env {
    type I = User
    type A = JWTAuthenticator
}

Every endpoint (action or websocket in Play) can be secured with one of Silhouette actions:

  • silhouette.SecuredAction – permits only users that are signed in(else NotAuthorized response, returned by Silhouette’s global error, can be overriden or use local error handler(per-action))
  • silhouette.UnsecuredAction – opposite of SecuredAction, only for not-authenticated users
  • silhouette.UserAwareAction – permits both not-authenticated and authenticated users, so you can decide which result to return

Play’s .async, body parsers etc, work as espected.

Identity

Represents a user. Its key component is LoginInfo that binds user id and authentication provider. Silhouette needs an IdentityService to handle all the operations related to retrieving identities(users). Our UserServiceImpl uses a UserDAO trait implemented by UserDAOMongo, but we won’t bother you with boring details. UserDAO trait can be used for a mock implementation(in-memory) in tests. An identity could have multiple login information(3rd party providers like Facebook, Twitter etc.) also (see excellent tutorial by Pablo Pedemonte from IBM).

Silhouette also needs a PasswordInfo DAO for storing passwords(in this example password is stored together with user).

Our User class looks like this:

case class User(
  userId: UUID,
  loginInfo: LoginInfo,
  firstName: Option[String],
  lastName: Option[String],
  fullName: Option[String],
  email: Option[String],
  passwordInfo: Option[PasswordInfo],
  role: Role = UserRole,
  rateLimit: Long = RateLimitActor.DefaultLimit) extends Identity

There are other pecularities for setting up Silhouette’s dependencies in a Guice module(modules.SilhouetteModule) that are left for reader as an excercise.

Authorization


By implementing Silhouette’s trait Authorization[I <: Identity, A <: Authenticator] we get the flexibility needed to authorize certain users(in this example by a role):

object Roles {
  sealed abstract class Role(val name: String)
  case object AdminRole extends Role("admin")
  case object UserRole extends Role("user")
}

case class WithRole(role: Role) extends Authorization[User, DefaultEnv#A] {
  override def isAuthorized[B](user: User, authenticator: DefaultEnv#A)(implicit request: Request[B]): Future[Boolean] =
    Future.successful(user.role == role)
}

In our example we made a class WithRole, that allows us to specify which role a user needs to have in order to proceed, else he should get a 403 Forbidden result of course. An example:

def adminOnly = silhouette.SecuredAction(WithRole(AdminRole)) { implicit request =>
  Ok("SUCCESS! (only ADMIN)")
}
def userOrAdmin = silhouette.SecuredAction(WithRole(UserRole) || WithRole(AdminRole)) { implicit request =>
  Ok("SUCCESS! (USER or ADMIN)")
}

The result in case of error(not-authenticated or not-authorized) can be configured by implementing custom error hanlder, a global one (for every secured action) or local(per action). Instances of this class can be composed with classic boolean operations, like &&|| and !(don’t forget to import an implicit ExecutionContext). Of course, we could give every user a list of roles, but that is just an implementation detail…

Rate limiting


We want to have a per-user rate limiting(num of requests / time frame) in our application.
Each user has a field rateLimit: Long that is by default set to DefaultLimit, lets say 100.
Time frame variable is stored in WindowSize variable.

Global actor implementation

User limits are stored in a global actor called RateLimitActor that contains a mutable :/ map userLimits: Map[UUID, UserLimit]. Notable implementation details are listed here:

  1. The actor is bound in Guice module asEagerSingleton and loads all users from DAO and stores them in userLimits map(with their limits).
  2. When a user signs up, it is also added to this map(see SignUpController).
  3. When a user(signed in) hits an endpoint with SecuredRateLimitingAction(our custom Action) it is checked against it’s rate limit.
    If user has more requests remaining, the action will proceed, else it will return a TooManyRequests status.
  4. When RateLimitActor is instantiated it schedules a function to be run once every WindowSize minutes. That function sends a simple Refresh() message to actor. The actor then refreshes all user limits to their maximum.

Redis implementation

One alternative implementation can be found in branch called RateLimiting-Redis. It is implemented with help of Redis.
Redis is an in-memory data structure store. It is very handy for these simple tasks where great speed and throughput is expected.
Compared to actor implementation, this example is easier to understand and implement.
Instead of sending messages to actor, now we have methods from UserLimitDAO(implemented in UserLimitDAORedis).
Hopefully, this DAO will be thread-safe so we can use it in many actions, so there is no blocking.
We used the Rediscala driver here.

Redis has only a few data structure types: string(handles integers also), hash(map), list, set, sorted-set, bitmap, hyperloglog and geospatial index. Every key(a string) is associated with one of these data structures.
In our example, we’ve used hash for storing user-limit information in Redis. The key has the form of user:limit:userUUID. Since we could have more keys in Redis, we must differentiate it by some prefix/sufix, so we added “user:limit:“.
When fetching all users, we first get all these keys by a regex and parse UUIDs back(see UserLimitDAORedis#getAll).

The key part, method update uses a transaction when fetching user’s limit and remaining so that they don’t get changed by the time they get fetched. Method refresh is used for setting all user limits back to maximum every WindowSize minutes(see eagerly instantiated RateLimit class).
The cleanup method is used to remove all user-limit records when the application is shut down(see RateLimit ... lifecycle.addStopHook).

Note about response headers

Every SecuredRateLimitingAction returns corresponing headers to each user, as described here:

  • X-Rate-Limit-Limit – the number of allowed requests in the current period
  • X-Rate-Limit-Remaining – the number of remaining requests in the current period
  • X-Rate-Limit-Reset – the number of seconds left in the current period

Conclusion

There are some things that maybe could be improved here:

  • Our RateLimitActor is a singleton so it could be a possible bottleneck. Another way could be using Redis or something similar in a per-user fashion
  • All user limits are refreshed at the same time, this could’ve been implemented differently
  • The behavior of SecuredRateLimitingAction could be extracted in a separate class so it could be reused.

Note: There is also a batch script called run-mongo-server.bat in repo. It should start a Mongo server instance on default port. You should create a folder called data before running it.

I hope this post helped you to widen your knowledge about Play, Silhouette and security in general. Any comments, suggestions, questions are welcome. Thank you for your patience. 😉

Hadžiavdić Sakib, 30.08.2016 OliveBH Dev Team