Tags: ohnosequences/webapp.auth
Tags
Second release: * Deletes manual queries and leaves them to an underlying newly created dependency: [`webapp.db.postgrest`](https://github.com/ohnosequences/webapp.db.postgREST). * Minimal refactoring of the code to produce more verbose errors. The README for this version is [here](https://github.com/ohnosequences/webapp.auth/blob/77a9ee6fe6/README.md).
First release. Utilities for login, logout, password change and sessi… …on management in webapps backends in Scala Play. *This is a copy of the companion README*. In a Scala Play project, you should add this to your dependencies: ``` resolvers = "Era7 maven releases" at "https://s3-eu-west-1.amazonaws.com/releases.era7.com" libraryDependencies = "ohnosequences" %% "webapp.auth" % "x.y.z" ``` where `x.y.z` is the version of the [latest release](https://github.com/ohnosequences/webapp-auth/releases/latest) This package is intended to be used with [PostgreSQL](https://www.postgresql.org) and [PostgREST](http://postgrest.org) as database system. The database should have tables to store users and their sessions with the following minimal fields: ``` CREATE TABLE users ( id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE, password BYTEA NOT NULL ); CREATE TABLE sessions ( id INTEGER REFERENCES users(id), token BYTEA NOT NULL, expires TIMESTAMP NOT NULL, valid BOOLEAN NOT NULL DEFAULT TRUE ); ``` Apart from that, Scala Play should include the following configuration in `conf/application.conf` file: ``` play.http.session { cookieName = "MY_SESSION_COOKIE" httpOnly = false # 2 hours for max age of session, in milliseconds maxAge = 7200000 # TODO This should be set to true when if our site has https secure = false sameSite = "lax" } ``` This package includes: It has two abstract fields: * `usersTable` which should point, when extended, to the endpoint of PostgREST corresponding to the users table: for example `https://localhost:3000/users` * `sessionsTable` which should point, when extended, to the endpoint of PostgREST corresponding to the sessions table: for example `https://localhost:3000/sessions` This controller contains code to perform the login and logout action of an user with two methods: * `login` * `logout` Following arguments for `login` should be passed in the body: * `email` * `password` Password should be sent in plain text using HTTPs in the client. When an user performs his/her logout, the token is marked as `valid = False` in the database, and the cookie gets erased from the client. We DO NOT reuse tokens. That means that every time an user logins, a new token gets assigned to him/her. Tokens are randomly generated with a length of 128 bytes. That class should be extended because it contains a `sessionsTable` abstract value, that should point to the same endpoint the `Login` class points to (e.g. `https://localhost:3000/sessions`). Imagine we have code to do something once the user has identified himself/herself: ``` import javax.inject._ import play.api.mvc.Results._ class MyController @Inject (...) { doAction: Result = Action { request => // parse the request for parameters and do actual stuff with it ... } doAsyncAction: Future[Result] = Action.async { request => // parse the request for parameters and do actual stuff with it ... } } ``` Then to restrict those actions to authenticated requests (those which carry a valid token) we should inject an instance of `Authenticated`, for example `authenticated: Authenticated` and substitute `Action` for `authenticated`: ``` import javax.inject._ import play.api.mvc.Results._ class MyController @Inject (..., authenticated: Authenticated) { doAction: Result = authenticated { request => // parse the request for parameters and do actual stuff with it ... } doAsyncAction: Future[Result] = authenticated.async { request => // parse the request for parameters and do actual stuff with it ... } } ``` If the authentication fails, or the token is expired, a `Results.Unauthorized` instance is thrown (HTTP code 401). That's it, we do not have to worry about checking the credentials ourselves, the authenticated action does it for us, and if they are correct, then the code included among `{ }` is executed. This controller is abstract because it includes a value `usersTable` which should point to the users table when the class is extended (e.g. `https://localhost:3000/users`). It contains a `changePassword` method which receives the current password, the new password and the new password repeated in the following body fields, respectively: * `current` * `new` * `renew` Those fields should be transmited in plain text using HTTPs. Interesting thing about this `Auth` object is that it could be used to hash passwords and introduce them, by hand, in the database: ``` import webapp.auth.Auth Auth.password.hash("pass") //24372443362e2e2e2e2f2e2e2e2e3774554b6631657252587931706f5469646c76757a4d74526a7949797058423972364a537a6944495061332470394c537a74316b496e454b52583333435453626d462e30664f4f2f706a64503141435636514c2f78633200 ``` To introduce them in the database: ``` psql INSERT INTO users ( email, password ) VALUES ( '[email protected]', '\x24372443362e2e2e2e2f2e2e2e2e3774554b6631657252587931706f5469646c76757a4d74526a7949797058423972364a537a6944495061332470394c537a74316b496e454b52583333435453626d462e30664f4f2f706a64503141435636514c2f78633200'); ``` Note the initial `\x` marker when introducing it in the database, because this data is encoded in hexadecimal. This is an example coming from a real project. ``` package controllers import javax.inject._ import play.api.mvc.{BodyParsers, ControllerComponents} import play.api.libs.ws.WSClient import scala.concurrent.ExecutionContext class Authenticated @Inject()(parser: BodyParsers.Default, ws: WSClient)( implicit override val executionContext: ExecutionContext ) extends webapp.auth.Authenticated(parser, ws) { val sessionsTable = "https://localhost:3000/sessions" } @singleton class Auth @Inject()(cc: ControllerComponents, authenticated: Authenticated, ws: WSClient)( implicit override val ec: ExecutionContext ) extends webapp.auth.Login(cc, authenticated, ws) { val usersTable = "https://localhost:3000/users" val sessionsTable = "https://localhost:3000/sessions" } ``` ``` package controllers import javax.inject._ import play.api.mvc.ControllerComponents import play.api.libs.ws.WSClient import scala.concurrent.{ExecutionContext, Future} @singleton class Settings @Inject()(cc: ControllerComponents, authenticated: Authenticated, ws: WSClient)( implicit override val ec: ExecutionContext ) extends webapp.auth.PasswordChange(cc, authenticated, ws) { val usersTable = Routes.db.users } ``` ``` POST /login controllers.Auth.login GET /logout controllers.Auth.logout POST /settings/password controllers.Settings.changePassword ```