Skip to content

Instantly share code, notes, and snippets.

@agemooij
Last active September 26, 2018 13:19
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save agemooij/11115834 to your computer and use it in GitHub Desktop.
Save agemooij/11115834 to your computer and use it in GitHub Desktop.
Quick spray-routing authentication example

Extremely bare example of Spray routing authentication using session cookies and XSRF tokens

This code was quickly ripped out of an active project to serve as an example. It will not compile in any way!

Any questions? Add them to the comments!

Notes

  • the application uses a version of the cake pattern to compose the Spray routing application together from various traits
  • a lot of those traits should be pretty self-explaining based on their name but if requested I can paste them into this Gist
import concurrent._
import concurrent.Future._
import spray.routing._
import spray.routing.authentication._
/**
* Simple wrapper around the Spray routing ContextAuthenticator type to make it
* specific for our sessions and to add some util methods to it.
*
* Spray types used by Authenticators (defined in spray.routing.authentication):
*
* type Authentication[T] = Either[Rejection, T]
* type ContextAuthenticator[T] = RequestContext ⇒ Future[Authentication[T]]
*
*/
abstract class Authenticator extends ContextAuthenticator[Session] {
/**
* Function to make Authenticators composable, i.e. to create a new Authenticator
* that wraps two others and that will try the second one if the first one fails
* to authenticate the request.
*/
def orElse(other: Authenticator)(implicit ec: ExecutionContext): Authenticator = {
new Authenticator {
def apply(requestContext: RequestContext): Future[Authentication[Session]] = {
// We need to explicitly specify the 'super' apply method from the surrounding
// class so we can call it without calling ourselves recursively by accident
Authenticator.this.apply(requestContext).flatMap {
case success @ Right(_) ⇒ successful(success)
case Left(rejection) ⇒ other.apply(requestContext)
}
}
}
}
}
/** If anything goes wrong during authentication, this is the rejection to use. */
case object AuthenticatorRejection extends Rejection
/** Custom RejectionHandler for dealing with AuthenticatorRejections. */
object AuthenticatorRejectionHandler {
import spray.http.StatusCodes._
import AuthDirectives._
def apply(settings: Settings): RejectionHandler = RejectionHandler {
case AuthenticatorRejection :: _ ⇒ {
completeWithoutSessionCookies(settings.Auth.CookieDomain, settings.Auth.EnforceTLS)(Unauthorized, "Missing or invalid authentication")
}
}
}
/**
* This trait provides two (!) authenticators based on session cookies used by the
* web application.
*
* Authentication succeeds when:
* - the request has a sessionId cookie
* - the value of that cookie is a valid session
*
* Optional for XSRF-protected resources (at least PUT/POST/DELETE)
* - the request has the CSRF token header (X-XSRF-TOKEN, AngularJs-style)
* - the value of that header matches the csrf token in the session
*
* Returns:
* - Successful authentication -> Future(Right(Session))
* - Unsuccessful authentication -> Future(Left(AuthenticatorRejection))
*/
trait SessionCookieAuthenticatorProvider
extends SessionStoreProvider // provides a SessionStore implementation as "sessionStore"
with ExecutionContextProvider // provides an implicit ExecutionContext
with LoggingProvider { // provides a logger
import AuthCookies._ // import our cookie definitions
val SessionCookieXsrfAuthenticator: Authenticator = new SessionCookieXsrfAuthenticatorImpl(sessionStore)
val SessionCookieAuthenticator: Authenticator = new SessionCookieAuthenticatorImpl(sessionStore)
/**
* Authenticator that checks for the standard SessionId cookie and
* validates its value against the SessionStore.
*
* TODO: Move this class into a standalone object by also specifying the ExecutionContext and Logger as arguments
*/
private class SessionCookieAuthenticatorImpl(sessionStore: SessionStore) extends Authenticator {
def apply(ctx: RequestContext): Future[Authentication[Session]] = {
log.debug("Authenticating request for uri '{}'...", ctx.request.uri)
findSessionIdCookieValue(ctx)
.map { sessionId ⇒
log.debug("Authenticating session id cookie with value '{}'", sessionId)
validateSessionId(ctx, sessionId)
}
.getOrElse {
log.warning("No session id cookie found in request for uri {}.", ctx.request.uri)
Future.successful(Left(AuthenticatorRejection))
}
}
private def findSessionIdCookieValue(ctx: RequestContext): Option[String] =
ctx.request.cookies.find(_.name == SessionIdCookieName).map(_.content)
private def validateSessionId(ctx: RequestContext, sessionId: String): Future[Authentication[Session]] = {
sessionStore.findSession(sessionId).map { sessionOption ⇒
sessionOption.map { session ⇒
log.debug("Session id cookie is valid. Authentication succeeded.")
Right(session)
}
.getOrElse {
log.warning("Invalid session id '{}' in request for uri {}.", sessionId, ctx.request.uri)
Left(AuthenticatorRejection)
}
}
}
}
/**
* Subclass of SessioncookieAuthenticatorImpl that adds an extra XSRF check for valid sessions.
*
* TODO: Move this class into a standalone object by also specifying the ExecutionContext and Logger as arguments
*/
private class SessionCookieXsrfAuthenticatorImpl(sessionStore: SessionStore) extends SessionCookieAuthenticatorImpl(sessionStore) {
override def apply(ctx: RequestContext): Future[Authentication[Session]] = {
super.apply(ctx).map { authentication ⇒
authentication.right.flatMap(session ⇒ validateCsrfToken(ctx, session))
}
}
private def findCsrfTokenHeaderValue(ctx: RequestContext): Option[String] =
ctx.request.headers.find(_.lowercaseName == CsrfTokenHeaderName.toLowerCase).map(_.value)
private def validateCsrfToken(ctx: RequestContext, session: Session): Authentication[Session] = {
findCsrfTokenHeaderValue(ctx).map { csrfToken ⇒
if (csrfToken == session.csrfToken) {
log.debug("XSRF token is valid. Authentication succeeded.")
Right(session)
} else {
log.warning("XSRF token doesn't match in request for uri {} and user {} with session id {}.", ctx.request.uri, session.user.email, session.id)
Left(AuthenticatorRejection)
}
}.getOrElse {
log.warning("XSRF token not found in request for uri {} and user {} with session id {}.", ctx.request.uri, session.user.email, session.id)
Left(AuthenticatorRejection)
}
}
}
}
/** The complete API. */
trait ApiRoutes
extends XRoutes
with YRoutes
with ZRoutes
with SessionCookieAuthenticatorProvider
with SettingsProvider {
// format: OFF
def apiRoutes = {
pathPrefix("api") {
handleRejections(ApiRejectionHandler) {
xRoutes ~
authenticate(SessionCookieAuthenticator) { session ⇒
yRoutes(session)
} ~
authenticate(SessionCookieXsrfAuthenticator) { session ⇒
zRoutes(session)
}
}
}
}
// format: ON
private val ApiRejectionHandler = AuthenticatorRejectionHandler(settings) orElse RejectionHandler.Default
}
import spray.http._
import spray.http.{ DateTime ⇒ SprayDateTime }
import spray.json._
import base64.Encode.{ urlSafe ⇒ toBase64 }
import util.DateTimeSupport._
object AuthCookies {
val SessionIdCookieName = "session-id"
val CsrfTokenCookieName = "XSRF-TOKEN"
val CsrfTokenHeaderName = "X-XSRF-TOKEN"
private val path = Some("/")
def sessionCookies(session: Session, domain: String, persistent: Boolean = false, secure: Boolean = true): Set[HttpCookie] = {
val expiresOn = if (persistent) Some(toSprayDateTime(session.expiresOn)) else None
Set(
cookie(SessionIdCookieName, session.id, domain, expiresOn, secure, httpOnly = true),
cookie(CsrfTokenCookieName, session.csrfToken, domain, expiresOn, secure, httpOnly = false),
)
}
def sessionCookiesForDeletion(domain: String, secure: Boolean = true): Set[HttpCookie] = {
Set(
cookie(SessionIdCookieName, "", domain, None, secure, httpOnly = true),
cookie(CsrfTokenCookieName, "", domain, None, secure, httpOnly = false)
)
}
private def cookie(name: String, value: String, domain: String, expiresOn: Option[SprayDateTime], secure: Boolean, httpOnly: Boolean) = HttpCookie(
name = name,
content = value,
expires = expiresOn,
domain = Some(domain),
path = path,
httpOnly = httpOnly,
secure = secure
)
}
@hekailiang
Copy link

Could you also provide sample code for Session/SessionOption? Thanks a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment