We are going to create a simple application which shows how to work with authentication/authorization with use of JSON
Web Token (JWT). There will be sign in, sign up, change password endpoints, and all data will be persisted in the
database. We will use Play Framework to build the REST API, Silhouette to implement an authentication/authorization
layer for our application and PostgreSQL as a storage.
Most of the examples existing on the Internet don’t give enough information to understand how to work with Silhouette,
so I hope you will find the article useful enough.
The name of the header in which the token will be transferred
requestParts
Some request parts from which a value can be extracted or None to extract values from any part of the
request. In this case we use only the header
issuerClaim
The issuer claim identifies the principal that issued the JWT
authenticatorExpiry
The duration an authenticator expires after it was created. This means, if the timeout is set to 3 hours,
then the authenticator expires definitely after 3 hours
sharedSecret
The shared secret to sign the JWT. You can generate a secret key
here
JWT is not the only authenticator supported by Silhouette, you can explore all available authenticators and their
configuration here.
Environment Configuration
Since JWT is used for authentication, we need to configure the Play environment for this. There is a Play JWT
environment, we use a trait which contains the user model as Identifier, and JWTAuthenticator.
User model needs to extend the Silhouette's Identity trait if we want to use the User instance as an entity that
have to be authenticated.
123456789101112131415161718192021
caseclassUser(id:Option[Long],email:String,name:String,lastName:String,password:Option[String]=None)extendsIdentity{/** * Generates login info from email * * @return login info */defloginInfo=LoginInfo(CredentialsProvider.ID,email)/** * Generates password info from password. * * @return password info */defpasswordInfo=PasswordInfo(BCryptSha256PasswordHasher.ID,password.get)}
We use email field as a unique user key and Bcrypt hasher to hash the user's
password.
Slick User table
Here it is the User table projection which allows Slick to work with the database table:
123456789101112131415161718192021222324252627
classUserTable(tag:Tag)extendsTable[User](tag,Some("play_silhouette"),"users"){/** The ID column, which is the primary key, and auto incremented */defid=column[Option[Long]]("id",O.PrimaryKey,O.AutoInc,O.Unique)/** The name column */defname=column[String]("name")/** The email column */defemail=column[String]("email",O.Unique)/** The last name column */deflastName=column[String]("lastName")/** The password column */defpassword=column[Option[String]]("password")/** * This is the table's default "projection". * * It defines how the columns are converted to and from the User object. * * In this case, we are simply passing the id, name, email and password parameters to the User case classes * apply and unapply methods. */def*=(id,email,name,lastName,password)<>((User.apply_).tupled,User.unapply)}
We set name of database schema as play_silhouette and name of the database table as users:
It is necessary to define every field in the database table using column function. It describes the type and name of
the column. Moreover, you can set additional options like primary key or unique constraint.
def * is a table projection function which is used to get or update the entire database table row.
Data Access Objects implementation
To use Silhouette we need to implement a LoginInfo repository and a PasswordInfo repository which Silhouette is
using to get info about user authorization or authentication.
We also have to extend IdentityService[User]. This trait provides a method used by Silhouette to retrieve a user that
matches the specified login info used for secured endpoints. We mix this trait with our UserService trait which
provides additional methods to work with the user entity.
123456789101112131415161718192021
/*** Handles actions to users.*/traitUserServiceextendsIdentityService[User]{/** * Saves a user. * * @param user The user to save. * @return The saved user. */defsave(user:User):Future[User]/** * Updates a user. * * @param user The user to update. * @return The updated user. */defupdate(user:User):Future[User]}
To work with the User table we need to create a class which will implement finding, saving and updating users via
UserTable class. It uses a configured PostgreSQL database instance and a UserTable instance.
/*** Gives access to the user repository.*/classUserDAOImpl@Inject()(protectedvaldbConfigProvider:DatabaseConfigProvider)(implicitec:ExecutionContext)extendsUserDAO{privatevalusers=TableQuery[UserTable]privatevaldb=dbConfigProvider.get[JdbcProfile].db/** * Finds a user by its login info. * * @param loginInfo The login info of the user to find. * @return The found user or None if no user for the given login info could be found. */overridedeffind(loginInfo:LoginInfo):Future[Option[User]]=db.run{users.filter(_.email===loginInfo.providerKey).result.headOption}/** * Saves a user. * * @param user The user to save. * @return The saved user. */overridedefsave(user:User):Future[User]=db.run{usersreturningusers+=user}/** * Updates a user. * * @param user The user to update. * @return The saved user. */overridedefupdate(user:User):Future[User]=db.run{users.filter(_.email===user.email).update(user).map(_=>user)}}
/*** Handles actions to users.** @param userDAO The user DAO implementation.* @param ex The execution context.*/classUserServiceImpl@Inject()(userDAO:UserDAO)(implicitex:ExecutionContext)extendsUserService{/** * Retrieves a user that matches the specified login info. * * @param loginInfo The login info to retrieve a user. * @return The retrieved user or None if no user could be retrieved for the given login info. */defretrieve(loginInfo:LoginInfo):Future[Option[User]]=userDAO.find(loginInfo)/** * Saves a user. * * @param user The user to save. * @return The saved user. */defsave(user:User):Future[User]=userDAO.save(user)/** * Updates a user. * * @param user The user to update. * @return The updated user. */defupdate(user:User):Future[User]=userDAO.update(user)}
To integrate our password dao with Silhouette we need to implement a class which extends
DelegableAuthInfoDAO[PasswordInfo]. It's a minimalistic use case of this class which is used only to find and update
a user's password. For most cases, we just reusing UserDAO functionality.
You can see that the add and save methods call the update method, so they are the same. We did it because for this
case we don’t need other logic for these methods. All of these methods will update the user's password.
Credentials Provider
Credentials Provider is a Silhouette class for authentications with credentials (login & password). It uses
DelegableAuthInfo[PasswordInfo] which we implemented in
PasswordInfoImpl class
to work with the user's password storage and password hasher to hash and verify passwords.
You can use more than one Provider. For example, you can add
social providers support to your API.
We use Guice for Dependency Injection. You can read about it
here.
We need to bind dao and service classes to their implementations:
12345678910
classBaseModuleextendsAbstractModulewithScalaModule{/** * Configures the module. */overridedefconfigure():Unit={bind[UserDAO].to[UserDAOImpl]bind[UserService].to[UserServiceImpl]}}
Besides, it is necessary to bind the crypter for authentication, authenticator service, password hasher and already
created classes like JWT environment, auth and password info dao and other necessary
Silhouette components.
You can find the entire Silhouette module with scala docs in the
SilhouetteModule.scala file.
Silhouette controller
In order to work with Silhouette components we will create an abstract silhouette controller which will add silhouette
components to the default play controller. This is not required but it will simplify work with Silhouette components.
Before creating controllers you need to know about UnsecuredAction and SecuredAction.
UnsecuredAction handles requests which don't need any authorization.
SecuredAction handles requests only for authorized users. For our case, the user has to provide an X-Auth header
with valid JWT token value.
Also, you can create custom actions like adminSecuredAction. More information about actions in
Silhouette documentation.
Creating Controllers
To use Silhouette in controllers we need to extend the SilhouetteController class which was mentioned before.
Sign up controller:
123456789101112131415161718192021222324252627
classSignUpController@Inject()(components:SilhouetteControllerComponents)(implicitex:ExecutionContext)extendsSilhouetteController(components){implicitvaluserFormat=Json.format[User]/** * Handles sign up request * * @return The result to display. */defsignUp=UnsecuredAction.async{implicitrequest:Request[AnyContent]=>implicitvallang:Lang=supportedLangs.availables.headrequest.body.asJson.flatMap(_.asOpt[User])match{caseSome(newUser)ifnewUser.password.isDefined=>userService.retrieve(LoginInfo(CredentialsProvider.ID,newUser.email)).flatMap{caseSome(_)=>Future.successful(Conflict(JsString(messagesApi("user.already.exist"))))caseNone=>valauthInfo=passwordHasherRegistry.current.hash(newUser.password.get)valuser=newUser.copy(password=Some(authInfo.password))userService.save(user).map(u=>Ok(Json.toJson(u.copy(password=None))))}case_=>Future.successful(BadRequest(JsString(messagesApi("invalid.body"))))}}}
This endpoint parses the json body and tries to find the user with this email in the database. If a user exists,
the endpoint will return HTTP 409 Conflict response, otherwise the user's password will be hashed and the user will be
saved to the database.
Also, we use messagesApi to return messages text in required language by their code. Messages are stored in a
messages file. That're base components to work with if you'd like to add
i18n support to your API.
classSignInController@Inject()(scc:SilhouetteControllerComponents)(implicitex:ExecutionContext)extendsSilhouetteController(scc){caseclassSignInModel(email:String,password:String)implicitvalsignInFormat=Json.format[SignInModel]/** * Handles sign in request * * @return JWT token in header if login is successful or Bad request if credentials are invalid */defsignIn=UnsecuredAction.async{implicitrequest:Request[AnyContent]=>implicitvallang:Lang=supportedLangs.availables.headrequest.body.asJson.flatMap(_.asOpt[SignInModel])match{caseSome(signInModel)=>valcredentials=Credentials(signInModel.email,signInModel.password)credentialsProvider.authenticate(credentials).flatMap{loginInfo=>userService.retrieve(loginInfo).flatMap{caseSome(_)=>for{authenticator<-authenticatorService.create(loginInfo)token<-authenticatorService.init(authenticator)result<-authenticatorService.embed(token,Ok)}yield{logger.debug(s"User ${loginInfo.providerKey} signed success")result}caseNone=>Future.successful(BadRequest(JsString(messagesApi("could.not.find.user"))))}}.recover{case_:ProviderException=>BadRequest(JsString(messagesApi("invalid.credentials")))}caseNone=>Future.successful(BadRequest(JsString(messagesApi("could.not.find.user"))))}}}
Sign in endpoint checks if the user exists and if his password hash matches with password hash in the database. If the
login is successful the application will return a response with status 200 and with header X-Auth: “generated JWT
token for current user”. Specifically, it is injected into the response by calling
authenticatorService.embed(token, Status). We have to provide this token in the X-Auth request header for every
endpoint which requires authentication. Sign in and sign up endpoints are unsecured and they don’t need this header but
the next Change Password endpoint needs an X-Auth header.
Change password controller:
1234567891011121314151617181920212223242526
classChangePasswordController@Inject()(scc:SilhouetteControllerComponents)(implicitex:ExecutionContext)extendsSilhouetteController(scc){caseclassChangePasswordModel(oldPassword:String,newPassword:String)implicitvalchangePasswordFormat=Json.format[ChangePasswordModel]/** * Changes the password. */defchangePassword=SecuredAction(WithProvider[AuthType](CredentialsProvider.ID)).async{request:SecuredRequest[JWTEnvironment, AnyContent]=>implicitvallang:Lang=supportedLangs.availables.headrequest.body.asJson.flatMap(_.asOpt[ChangePasswordModel])match{caseSome(changePasswordModel)=>valcredentials=Credentials(request.identity.email,changePasswordModel.oldPassword)credentialsProvider.authenticate(credentials).flatMap{loginInfo=>valnewHashedPassword=passwordHasherRegistry.current.hash(changePasswordModel.newPassword)authInfoRepository.update(loginInfo,newHashedPassword).map(_=>Ok)}.recover{case_:ProviderException=>BadRequest(JsString(messagesApi("invalid.old.password")))}caseNone=>Future.successful(BadRequest(JsString(messagesApi("invalid.body"))))}}}
Change password endpoint is secured endpoint. It checks if the provided X-Auth header is valid and not expired. If the
password was checked successfully, the old password provided by the user will be compared with the password hash in the
database. If passwords match, the application will update the user's password, otherwise it will send 400 Bad Request
response.
Run the application with sbt run, open browser and go to http://localhost:9000/. At the
start we will see the message that we need to run the database migration script:
Click Apply this script to create schema and table in postgres database. If the process succeeds you will see a
“Hello” message.
So now it's high time to test our application. For this we will use Postman but you can use
any REST client.
Let's check the sign up endpoint. We need to do a POST request with the User's name, last name, password and email in
body:
We got a response with HTTP 200 code, it means the user was created successfully. So now we can do sign in with login
and password which we used to sign up:
We got HTTP 200 status and X-Auth header with JWT token. Let's apply it to the change password endpoint because
it’s secured.
To do change password request we need to add X-Auth header from previous response to our request:
and add json body:
If the password is changed we will get a response with HTTP 200 status code. But if we make a request without X-Auth
header, or with invalid JWT token for this header, or it’s expired we will get 401 error like this:
That’s how you can create a RESTful api with Scala / Play using Silhouette for implementing JWT authentication.
Don't hesitate to ask questions in the comments below if you want to know more!
If you're looking for a developer or considering starting a new project,
we are always ready to help!