|
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) |
|
} |
|
} |
|
} |
|
} |
Could you also provide sample code for Session/SessionOption? Thanks a lot.