How do you solve in Silouette the sudo Actions use-case?

Hi,

Sudo actions are an extra level of protection when the user attempts to access sensitive information such as billing or account security information. When the user is authenticated with a Cookie the actions marked with Sudo auth will nevertheless request the user to provide the password.

Is this supported in Silhouette and if so how?

TIA,
Best regards,
Giovanni

Hi,

good question! Silhouette hasn’t a direct built-in functionality to support such scenario. Nonetheless, I think it can be handled with authorization and a local error handler.

Write an authorization that checks for a specific value in the session. Plugin this authorization into your secured action. In the controller that contains this action, write a local error handler that handles the authorization error. In this error handler you can ask with the help of the CredentialsProvider for the password. If the password is correct, set the session value and redirect to your action. The authorization action should now pass.

The session value could be a time based value. So that the sudo action has validity of a specific time.

Hope that helps.

Best regards,
Christian

Hi Christian,

Thank you for the fast response and wow amazing project! the code quality, documentation and forum wow! :slight_smile:

I will follow your advice but I think it is a very common use-case to consider having it covered within the core of Silhouette. You’ll find the “Sudo action” (btw how is this called officially? other than Sudo actions), for example, in GitHub when you try to access account information; you also find it in Upwork when trying to modify billing information. I think it is a very common use-case and most professional Web Apps I have used they all have it.

What’d be the recommended approach to integrate it within the Silhouette core?

TIA,
Best regards,
Giovanni

PS: this is a sample implementation of the Sudo action for PA https://github.com/bravegag/play-authenticate-usage-scala/blob/master/app/actions/SudoForbidCookieAuthAction.scala

Hi,

you’r welcome! The philosophy of Silhouette is it to be an easy to use loosely coupled library. It gives you the building blocks, but it doesn’t give you a complete solution. It’s hard to find a balance between extensibility and easy to use components. So the idea was it to have a core (Silhouette) and extensions that can be implemented in Seeds. You will not find forms or templates in Silhouette, but you will find the components with which you can easily handle the backend logic of a login form.

If you implement a custom SudoAction than you must allow a developer to easily extend it with own forms, templates, styles and so on. An action in Silhouette is also authenticator agnostic. That means that every action works with every authenticator implementation. A JWT authenticator or a bearer token authenticator wouldn’t make sense with a SudoAction in a common web page. For a single page app or a mobile app it would make sense, but in this case, such functionality must be handled in a complete different way.

Anyway, if you find a good solution. Then I’m open for a pull request.

Best regards,
Christian

Hi Christian,

I implemented this “Sudo Action” use-case using your suggested approach, however, I think it would be cleaner to have it built-in Silhouette and I will describe some ideas on how to do that at the end.

This is a step by step description of the solution:

  1. We need at least two Session keys: "has.sudo.access" and "redirect.to.url". The first one is a simplified version of is-the-user-logged-in-explicitly-with-password.
object SessionKeys extends Enumeration {
  val HAS_SUDO_ACCESS = "has.sudo.access"
  val REDIRECT_TO_URI = "redirect.to.url"
}
  1. We need custom Authorization that checks the Session for the first key above:
case class SudoAccessAuthorization[A <: Authenticator]() extends Authorization[User, A] {
  def isAuthorized[B](user: User, authenticator: A)(implicit request: Request[B]) = {
    Future.successful {
      request.session.get(SessionKeys.HAS_SUDO_ACCESS) match {
        case Some(value) => value.toBoolean
        case _ => false
      }
    }
  }
}
  1. It is best to have a dedicated Controller class that encapsulates all the actions that require Sudo access because then they can all reuse the same local error handler:
class SudoAccessController @Inject() (
  silhouette: Silhouette[DefaultEnv]
)(
  implicit
  webJarsUtil: WebJarsUtil,
  assets: AssetsFinder,
  ex: ExecutionContext
) extends InjectedController with I18nSupport {
  /**
   * A local error handler.
   */
  val errorHandler = new SecuredErrorHandler {
    override def onNotAuthenticated(implicit request: RequestHeader) = {
      Future.successful(Redirect(controllers.routes.SignInController.view()))
    }

    override def onNotAuthorized(implicit request: RequestHeader) = {
      Future.successful(Redirect(controllers.routes.SignInController.view()).
        flashing("error" -> Messages("reenter.password")).
        withSession(request.session + (SessionKeys.REDIRECT_TO_URI -> request.uri)))
    }
  }

  /**
   * Handles example restricted sudo access action.
   * @return The result to display.
   */
  def restrictedSudoAccess = silhouette.SecuredAction(errorHandler)(SudoAccessAuthorization[DefaultEnv#A]()).async { implicit request =>
    Future.successful(Ok(views.html.restrictedSudoAccess(request.identity)))
  }
}
  1. Now we need bookkeeping the Session key "has.sudo.access" across all relevant controllers: signIn, signOut, activate, resetPassword, changePassword, etc. Basically it is set to "true" once on authentication success and the key is removed everywhere else:

As part of the SignInController#submit:

val result = request.session.get(SessionKeys.REDIRECT_TO_URI).map { targetUri =>
  Redirect(targetUri)
}.getOrElse {
  Redirect(routes.ApplicationController.index())
}.withSession(request.session + (SessionKeys.HAS_SUDO_ACCESS -> "true"))

The complete working solution can be found in my fork of the play-silhouette-seed. Please take a look at the TODO.md for a list of what I’m trying to accomplish on top of the original play-silhouette-seed.

The changes are too scattered and application-specific to generalize into Silhouette core … I don’t know. We have basically reduced this use-case to a new Authorization. A possibility for generalization could be using the Event bus and do the Session bookkeeping within some built-in listeners … what do you think?

TIA,
Best regards,
Giovanni