How to implement user authentication in a Play Framework application

This past week I started working with the Play Framework (version 2.6), and this is a quick look at how to implement user authentication in a Play application. Specifically this blog post focuses on how to create a custom action so you can secure your Play controllers methods, where you’ll implement those methods using this new, custom action.

Resources

Before I begin, note that the Play Action composition and Handling form submission pages are the two primary resources for this tutorial, along with their Session and Flash scopes document.

A custom user authentication action

If you read through the “Action Composition” document you’ll see that you need to create a custom action. This is one that I pieced together based on that document:

package controllers

import javax.inject.Inject
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent.{ExecutionContext, Future}

/**
  * Cobbled this together from:
  * https://www.playframework.com/documentation/2.6.x/ScalaActionsComposition#Authentication
  */
class AuthenticatedUserAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser) {

    private val logger = play.api.Logger(this.getClass)

    override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
        logger.debug("ENTERED AuthenticatedUserAction::invokeBlock")
        val maybeUsername = request.session.get("USERNAME")
        maybeUsername match {
            case None => {
                logger.debug("CAME INTO 'NONE'")
                Future.successful(Forbidden("Dude, you’re not logged in."))
            }
            case Some(u) => {
                logger.debug("CAME INTO 'SOME'")
                val res: Future[Result] = block(request)
                res
            }
        }
    }
}

Use that action in your controller methods

Once you have a custom user authentication action like that, you need to use it for all of the actions that you want to secure. This controller shows how to use that action for several different methods that users need to be logged in to access. I stripped down some of the controller code that wasn’t necessary, but the important detail here is what the method signatures look like. I underlined the authenticatedUserAction in the add method to make it easier to find:

package controllers

import javax.inject.Inject
import models.{BlogPost, BlogPostDao, FrontPageDao, FrontPageNode}
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._

class BlogPostController @Inject()(
    cc: MessagesControllerComponents,
    frontPageDao: FrontPageDao,
    blogPostDao: BlogPostDao,
    authenticatedUserAction: AuthenticatedUserAction   //<-- IT’S INJECTED HERE
) extends AbstractController(cc) with play.api.i18n.I18nSupport {

    // NOTE: search the Action Composition page for “with play.api.i18n.I18nSupport”
    // to see why i use that

    private val logger = play.api.Logger(this.getClass)

    //see https://www.playframework.com/documentation/2.6.x/ScalaCustomValidations
    //see https://www.playframework.com/documentation/2.6.x/ScalaForms
    val form: Form[BlogPost] = Form (
        mapping(
            "title" -> nonEmptyText,
            "blogContent" -> nonEmptyText,
            "tags" -> nonEmptyText
                          .verifying("error: invalid characters", u => charsAreAllowedInTagsField(u)),
            "description" -> nonEmptyText(maxLength = 160),
            "uri" -> nonEmptyText
                         .verifying("error: must begin with a /", u => beginsWithSlash(u))
                         .verifying("error: invalid characters in uri", u => charsAreAllowedInUri(u))
        )(BlogPost.apply)(BlogPost.unapply)
    )

    private val formSubmitUrl = routes.BlogPostController.save()

    //        -----------------------  
    def add = authenticatedUserAction { implicit request =>
    //        -----------------------  
        // pass an unpopulated form to the template
        Ok(views.html.admin.editBlogPost(form, formSubmitUrl))
    }

    def save = authenticatedUserAction { implicit request =>
        val errorFunction = { formWithErrors: Form[BlogPost] =>
            // this is the failure case, where the form had validation errors.
            // show the form again, with the errors highlighted.
            BadRequest(views.html.admin.editBlogPost(formWithErrors, formSubmitUrl))
        }

        val successFunction = { data: BlogPost =>
            // this is the success case
            val blogPost = BlogPost(
                data.title,
                data.body,
                data.tagsAsString,
                data.metaDescription,
                data.uri
            )
            blogPostDao.insertNewBlogPost(blogPost)
            Redirect(routes.BlogPostController.list())
                .flashing("info" -> "The blog post was added.")
        }
        val formValidationResult: Form[BlogPost] = form.bindFromRequest
        formValidationResult.fold(
            errorFunction,   // sad case
            successFunction  // happy case
        )
    }

    // authentication required
    def list() = authenticatedUserAction { implicit request: Request[AnyContent] =>
        val nodes: Seq[FrontPageNode] = frontPageDao.getAllNodesForAdminUse(0)
        Ok(views.html.admin.listBlogPosts(nodes))
    }

    // authentication required
    def delete(nid: Long) = authenticatedUserAction { implicit request: Request[AnyContent] =>
        Ok(views.html.admin.deleteBlogPost(nid))
    }


    // more methods here ...


    // verify that a string begins with a slash
    private def beginsWithSlash(s: String): Boolean = s.startsWith("/")

    // make sure the characters in the string are legal in a uri
    private def charsAreAllowedInUri(s: String): Boolean = {
        val regex = "[0-9a-zA-Z-_/]*"
        s.matches(regex)
    }

    // make sure the characters in the string are legal in a blog post "tags" field
    private def charsAreAllowedInTagsField(s: String): Boolean = {
        val regex = "[0-9a-zA-Z-_,. ]*"
        s.matches(regex)
    }
}

Again, the key is that all of those methods use authenticatedUserAction rather than a plain Action.

A sample form

Besides seeing how the "USERNAME" is set in the Play Framework session, the only other thing you might need to see is the form/template that I use to edit a blog post. The key here is the parameters that are passed into the template:

@(
    form: Form[BlogPost],
    postUrl: Call
)(implicit request: RequestHeader, messagesProvider: MessagesProvider)

@* this is how i passed the parameters into the template *before* i enabled user authentication *@
@* 
@(
    form: Form[BlogPost],
    postUrl: Call
)(implicit request: MessagesRequestHeader)
*@


<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
    <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/admin.css")">
</head>

<body id="blog-post-edit">
<div id="content">

    <div id="blog-post-edit-form">

    <h1>Blog Post</h1>

    @* the only other thing interesting about this template is how Flash and Form errors are shown *@
    @request.flash.data.map{ case (name, value) =>
        <div>@name: @value</div>
    }

    @* Global errors are not tied to any particular form field *@
    @if(form.hasGlobalErrors) {
        @form.globalErrors.map { error: FormError =>
            <div>
                Error: @error.key: @error.message
            </div>
        }
    }

    @helper.form(postUrl, 'id -> "blog-edit-form") {
        @helper.CSRF.formField

        @helper.inputText(
            form("title"),
            '_label -> "Title",
            'placeholder -> "the title of your blog post",
            'id -> "title",
            'size -> 60
        )
        @helper.textarea(
            form("blogContent"),
            '_label -> "Content",
            'id -> "blog-content-textarea",
            'rows -> 10,
            'cols -> 60
        )
        @helper.inputText(
            form("tags"),
            '_label -> "Tags",
            'placeholder -> "comma-separated list (foo, bar baz, bazzle)",
            'size -> 60
        )
        @helper.textarea(
            form("description"),
            '_label -> "Description",
            'id -> "blog-description-textarea",
            'rows -> 2,
            'cols -> 60
        )
        @helper.inputText(
            form("uri"),
            '_label -> "URI",
            'placeholder -> "must start with a /",
            'size -> 60
        )

        <button>Save</button>
    }

    </div>

</div>

</body>
</html>

Setting the user login cookie

One other thing that’s related is to see how the user authentication cookie is set during the login process. That part is actually simple, you just set it like this in your user login controller method:

Redirect(routes.BlogPostController.list)
    .flashing("info" -> "You are logged in.")
    .withSession("USERNAME" -> user.username)

Of course there’s a bunch of business logic code before you reach that point, but once you know the user’s login credentials are authentic, you can just set a session cookie like that.

Discussion

I think those are the only pieces of source code you need to see to get an idea of how to handle user authentication in a Play Framework application. Other things like the routes, models, and DAOs (data access objects) aren’t important to this discussion.

The AuthenticatedUserAction class can use some more work, but I just got this running earlier today, so for now it is what it is. I just wanted to share a Play Framework user authentication example out here before I got too far away from it. The keys to the process are digging through these Play docs, especially the first one, to see how to implement your own user authentication action:

I hope I’ve included enough source code out here to make this example helpful. If you read that first document and follow my example code, I hope to save you some time and frustration in getting Play Framework user authentication working in your application.