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 ofSecuredAction
, only for not-authenticated userssilhouette.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:
- The actor is bound in Guice module
asEagerSingleton
and loads all users from DAO and stores them inuserLimits
map(with their limits). - When a user signs up, it is also added to this map(see
SignUpController
). - When a user(signed in) hits an endpoint with
SecuredRateLimitingAction
(our customAction
) it is checked against it’s rate limit.
If user has more requests remaining, the action will proceed, else it will return aTooManyRequests
status. - When
RateLimitActor
is instantiated it schedules a function to be run once everyWindowSize
minutes. That function sends a simpleRefresh()
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 periodX-Rate-Limit-Remaining
– the number of remaining requests in the current periodX-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 calleddata
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