diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 197323285..f744d8f65 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,6 @@ * [ ] Have you read [How to write the perfect pull request](https://github.com/blog/1943-how-to-write-the-perfect-pull-request)? * [ ] Have you read through the [contributor guidelines](https://github.com/mohiva/play-silhouette/blob/master/CONTRIBUTING.md)? -* [ ] Have you [squashed your commits](https://www.playframework.com/documentation/2.5.x/WorkingWithGit#Squashing-commits)? * [ ] Have you added copyright headers to new files? * [ ] Have you suggest documentation edits? * [ ] Have you added tests for any changed functionality? diff --git a/silhouette-cas/src/test/scala/com/mohiva/play/silhouette/impl/providers/CasProviderSpec.scala b/silhouette-cas/src/test/scala/com/mohiva/play/silhouette/impl/providers/CasProviderSpec.scala index 33af5647c..0345cc674 100644 --- a/silhouette-cas/src/test/scala/com/mohiva/play/silhouette/impl/providers/CasProviderSpec.scala +++ b/silhouette-cas/src/test/scala/com/mohiva/play/silhouette/impl/providers/CasProviderSpec.scala @@ -23,6 +23,7 @@ import org.specs2.mock.Mockito import org.specs2.specification.Scope import play.api.libs.concurrent.Execution.Implicits._ import play.api.test.FakeRequest +import test.SocialProviderSpec import scala.concurrent.Future import scala.concurrent.duration._ @@ -70,29 +71,25 @@ class CasProviderSpec extends SocialProviderSpec[CasInfo] with Mockito with Logg "redirect to CAS server if service ticket is not present in request" in new Context { implicit val req = FakeRequest(GET, "/") - result(provider.authenticate()) { - case result => - status(result) must equalTo(SEE_OTHER) - redirectLocation(result) must beSome("https://cas-url/?service=https%3A%2F%2Fcas-redirect%2F") + result(provider.authenticate()) { result => + status(result) must equalTo(SEE_OTHER) + redirectLocation(result) must beSome("https://cas-url/?service=https%3A%2F%2Fcas-redirect%2F") } } "redirect to CAS server with the original requested URL if service ticket is not present in the request" in new Context { implicit val req = FakeRequest(GET, redirectURLWithOrigin) - result(provider.authenticate()) { - case result => - status(result) must equalTo(SEE_OTHER) - redirectLocation(result) must beSome("https://cas-url/?service=https%3A%2F%2Fcas-redirect%2F") + result(provider.authenticate()) { result => + status(result) must equalTo(SEE_OTHER) + redirectLocation(result) must beSome("https://cas-url/?service=https%3A%2F%2Fcas-redirect%2F") } } "return a valid CASAuthInfo object if service ticket is present in request" in new Context { implicit val req = FakeRequest(GET, "/?ticket=%s".format(ticket)) - authInfo(provider.authenticate()) { - case authInfo => authInfo must be equalTo CasInfo(ticket) - } + authInfo(provider.authenticate())(authInfo => authInfo must be equalTo CasInfo(ticket)) } } @@ -111,7 +108,7 @@ class CasProviderSpec extends SocialProviderSpec[CasInfo] with Mockito with Logg await(futureProfile) must beLike[CommonSocialProfile] { case profile => - profile must be equalTo new CommonSocialProfile( + profile must be equalTo CommonSocialProfile( loginInfo = new LoginInfo(CasProvider.ID, userName), firstName = Some(firstName), lastName = Some(lastName), @@ -145,7 +142,7 @@ class CasProviderSpec extends SocialProviderSpec[CasInfo] with Mockito with Logg lazy val ticket = "ST-12345678" - lazy val casAuthInfo = new CasInfo(ticket) + lazy val casAuthInfo = CasInfo(ticket) lazy val principal = mock[AttributePrincipal].smart diff --git a/silhouette/app/com/mohiva/play/silhouette/impl/providers/OAuth2Provider.scala b/silhouette/app/com/mohiva/play/silhouette/impl/providers/OAuth2Provider.scala index 1ab291849..a26948617 100644 --- a/silhouette/app/com/mohiva/play/silhouette/impl/providers/OAuth2Provider.scala +++ b/silhouette/app/com/mohiva/play/silhouette/impl/providers/OAuth2Provider.scala @@ -32,12 +32,12 @@ import play.api.libs.json._ import play.api.libs.ws.WSResponse import play.api.mvc._ -import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.Future import scala.reflect.ClassTag import scala.util.{ Failure, Success, Try } /** - * The Oauth2 info. + * The OAuth2 info. * * @param accessToken The access token. * @param tokenType The token type. @@ -145,7 +145,7 @@ trait OAuth2Provider extends SocialStateProvider with OAuth2Constants with Logge } /** - * Handles the Oauth2 flow. + * Handles the OAuth2 flow. * * The left flow is the authorization flow, which will be processed, if no `code` parameter exists * in the request. The right flow is the access token flow, which will be executed after a successful @@ -289,74 +289,6 @@ trait OAuth2Constants { val AccessDenied = "access_denied" } -/** - * The OAuth2 state. - * - * This is to prevent the client for CSRF attacks as described in the OAuth2 RFC. - * - * @see https://tools.ietf.org/html/rfc6749#section-10.12 - */ -trait OAuth2State { - - /** - * Checks if the state is expired. This is an absolute timeout since the creation of - * the state. - * - * @return True if the state is expired, false otherwise. - */ - def isExpired: Boolean -} - -/** - * Provides state for authentication providers. - */ -trait OAuth2StateProvider { - - /** - * The type of the state implementation. - */ - type State <: OAuth2State - - /** - * Builds the state. - * - * @param request The current request. - * @param ec The execution context to handle the asynchronous operations. - * @tparam B The type of the request body. - * @return The build state. - */ - def build[B](implicit request: ExtractableRequest[B], ec: ExecutionContext): Future[State] - - /** - * Validates the provider and the client state. - * - * @param request The current request. - * @param ec The execution context to handle the asynchronous operations. - * @tparam B The type of the request body. - * @return The state on success, otherwise an failure. - */ - def validate[B](implicit request: ExtractableRequest[B], ec: ExecutionContext): Future[State] - - /** - * Publishes the state to the client. - * - * @param result The result to send to the client. - * @param state The state to publish. - * @param request The current request. - * @tparam B The type of the request body. - * @return The result to send to the client. - */ - def publish[B](result: Result, state: State)(implicit request: ExtractableRequest[B]): Result - - /** - * Returns a serialized value of the state. - * - * @param state The state to serialize. - * @return A serialized value of the state. - */ - def serialize(state: State): String -} - /** * The OAuth2 settings. * @@ -383,4 +315,5 @@ case class OAuth2Settings( scope: Option[String] = None, authorizationParams: Map[String, String] = Map.empty, accessTokenParams: Map[String, String] = Map.empty, - customProperties: Map[String, String] = Map.empty) + customProperties: Map[String, String] = Map.empty +) diff --git a/silhouette/app/com/mohiva/play/silhouette/impl/providers/SocialStateProvider.scala b/silhouette/app/com/mohiva/play/silhouette/impl/providers/SocialStateProvider.scala index d6bf7ce10..dc2efd7f4 100644 --- a/silhouette/app/com/mohiva/play/silhouette/impl/providers/SocialStateProvider.scala +++ b/silhouette/app/com/mohiva/play/silhouette/impl/providers/SocialStateProvider.scala @@ -1,5 +1,5 @@ /** - * Copyright 2016 Mohiva Organisation (license at mohiva dot com) + * Copyright 2017 Mohiva Organisation (license at mohiva dot com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,9 @@ package com.mohiva.play.silhouette.impl.providers import com.mohiva.play.silhouette.api.AuthInfo import com.mohiva.play.silhouette.api.crypto.{ Base64, CookieSigner } +import com.mohiva.play.silhouette.api.exceptions.ProviderException import com.mohiva.play.silhouette.api.util.ExtractableRequest +import com.mohiva.play.silhouette.impl.providers.DefaultSocialStateHandler._ import com.mohiva.play.silhouette.impl.providers.SocialStateItem._ import play.api.libs.json.{ Format, JsValue, Json } import play.api.mvc.Result @@ -71,7 +73,7 @@ object SocialStateItem { * * @return The serialized representation of the item. */ - override def toString = s"${Base64.encode(id)}-${Base64.encode(data)}" + def asString = s"${Base64.encode(id)}-${Base64.encode(data)}" } /** @@ -93,7 +95,6 @@ object SocialStateItem { } } } - } /** @@ -108,7 +109,7 @@ trait SocialStateProvider extends SocialProvider { * sends to the browser (e.g.: in the case of OAuth where the user needs to be redirected to the service * provider). * - * @param format The JSON format to the transform the user state into JSON. + * @param format The JSON format to transform the user state into JSON. * @param request The request. * @param classTag The class tag for the user state item. * @tparam S The type of the user state item. @@ -205,7 +206,8 @@ trait SocialStateHandler { * * @param handlers The item handlers configured for this handler. */ -class DefaultSocialStateHandler(val handlers: Set[SocialStateItemHandler], cookieSigner: CookieSigner) extends SocialStateHandler { +class DefaultSocialStateHandler(val handlers: Set[SocialStateItemHandler], cookieSigner: CookieSigner) + extends SocialStateHandler { /** * The concrete instance of the state provider. @@ -259,20 +261,24 @@ class DefaultSocialStateHandler(val handlers: Set[SocialStateItemHandler], cooki override def unserialize[B](state: String)( implicit request: ExtractableRequest[B], - ec: ExecutionContext): Future[SocialState] = { - - Future.fromTry(cookieSigner.extract(state)).flatMap(state => state.split('.').toList match { - case Nil => Future.successful(SocialState(Set())) - case items => - Future.sequence(items.map { - case ItemStructure(item) => handlers.find(_.canHandle(item)) match { - case Some(handler) => handler.unserialize(item) - case None => - throw new RuntimeException("None of the registered handlers can handle the given state item:" + item) - } - case s => throw new RuntimeException("Cannot extract social state item from string: " + s) - }).map(items => SocialState(items.toSet)) - }) + ec: ExecutionContext + ): Future[SocialState] = { + Future.fromTry(cookieSigner.extract(state)).flatMap { state => + state.split('.').toList match { + case Nil | List("") => + Future.successful(SocialState(Set())) + case items => + Future.sequence { + items.map { + case ItemStructure(item) => handlers.find(_.canHandle(item)) match { + case Some(handler) => handler.unserialize(item) + case None => throw new ProviderException(MissingItemHandlerError.format(item)) + } + case item => throw new ProviderException(ItemExtractionError.format(item)) + } + }.map(items => SocialState(items.toSet)) + } + } } /** @@ -294,6 +300,19 @@ class DefaultSocialStateHandler(val handlers: Set[SocialStateItemHandler], cooki } } +/** + * The companion object for the [[DefaultSocialStateHandler]] class. + */ +object DefaultSocialStateHandler { + + /** + * Some errors. + */ + val MissingItemHandlerError = "None of the registered handlers can handle the given state item: %s" + val ItemExtractionError = "Cannot extract social state item from string: %s" + +} + /** * Handles state for different purposes. */ diff --git a/silhouette/app/com/mohiva/play/silhouette/impl/providers/oauth2/state/CookieState.scala b/silhouette/app/com/mohiva/play/silhouette/impl/providers/oauth2/state/CookieState.scala deleted file mode 100644 index ec49dc6a3..000000000 --- a/silhouette/app/com/mohiva/play/silhouette/impl/providers/oauth2/state/CookieState.scala +++ /dev/null @@ -1,248 +0,0 @@ -/** - * Copyright 2015 Mohiva Organisation (license at mohiva dot com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.mohiva.play.silhouette.impl.providers.oauth2.state - -import javax.inject.Inject - -import com.mohiva.play.silhouette.api.crypto.{ Base64, CookieSigner } -import com.mohiva.play.silhouette.api.util.{ Clock, ExtractableRequest, IDGenerator } -import com.mohiva.play.silhouette.impl.exceptions.OAuth2StateException -import com.mohiva.play.silhouette.impl.providers.OAuth2Provider._ -import com.mohiva.play.silhouette.impl.providers.oauth2.state.CookieStateProvider._ -import com.mohiva.play.silhouette.impl.providers.{ OAuth2State, OAuth2StateProvider } -import org.joda.time.DateTime -import play.api.libs.json.Json -import play.api.mvc.{ Cookie, RequestHeader, Result } - -import scala.concurrent.duration._ -import scala.concurrent.{ ExecutionContext, Future } -import scala.language.postfixOps -import scala.util.{ Failure, Success, Try } - -/** - * The cookie state companion object. - */ -object CookieState { - - /** - * Converts the [[CookieState]]] to Json and vice versa. - */ - implicit val jsonFormat = Json.format[CookieState] - - /** - * Returns a serialized value of the state. - * - * @param state The state to serialize. - * @param cookieSigner The cookie signer implementation. - * @return A serialized value of the state. - */ - def serialize(state: CookieState, cookieSigner: CookieSigner) = { - cookieSigner.sign(Base64.encode(Json.toJson(state))) - } - - /** - * Unserializes the state. - * - * @param str The string representation of the state. - * @param cookieSigner The cookie signer implementation. - * @return Some state on success, otherwise None. - */ - def unserialize(str: String, cookieSigner: CookieSigner): Try[CookieState] = { - cookieSigner.extract(str) match { - case Success(data) => buildState(Base64.decode(data)) - case Failure(e) => Failure(new OAuth2StateException(InvalidCookieSignature, e)) - } - } - - /** - * Builds the state from Json. - * - * @param str The string representation of the state. - * @return A state on success, otherwise a failure. - */ - private def buildState(str: String): Try[CookieState] = { - Try(Json.parse(str)) match { - case Success(json) => json.validate[CookieState].asEither match { - case Left(error) => Failure(new OAuth2StateException(InvalidStateFormat.format(error))) - case Right(authenticator) => Success(authenticator) - } - case Failure(error) => Failure(new OAuth2StateException(InvalidJson.format(str), error)) - } - } -} - -/** - * A state which gets persisted in a cookie. - * - * This is to prevent the client for CSRF attacks as described in the OAuth2 RFC. - * - * @see https://tools.ietf.org/html/rfc6749#section-10.12 - * @param expirationDate The expiration time. - * @param value A value that binds the request to the user-agent's authenticated state. - */ -case class CookieState(expirationDate: DateTime, value: String) extends OAuth2State { - - /** - * Checks if the state is expired. This is an absolute timeout since the creation of - * the state. - * - * @return True if the state is expired, false otherwise. - */ - override def isExpired = expirationDate.isBeforeNow -} - -/** - * Saves the state in a cookie. - * - * @param settings The state settings. - * @param idGenerator The ID generator used to create the state value. - * @param cookieSigner The cookie signer implementation. - * @param clock The clock implementation. - */ -class CookieStateProvider @Inject() ( - settings: CookieStateSettings, - idGenerator: IDGenerator, - cookieSigner: CookieSigner, - clock: Clock) - extends OAuth2StateProvider { - - /** - * The type of the state implementation. - */ - override type State = CookieState - - /** - * Builds the state. - * - * @param request The request. - * @param ec The execution context to handle the asynchronous operations. - * @tparam B The type of the request body. - * @return The build state. - */ - override def build[B](implicit request: ExtractableRequest[B], ec: ExecutionContext): Future[CookieState] = { - idGenerator.generate.map { id => - CookieState(clock.now.plusSeconds(settings.expirationTime.toSeconds.toInt), id) - } - } - - /** - * Validates the provider and the client state. - * - * @param request The request. - * @param ec The execution context to handle the asynchronous operations. - * @tparam B The type of the request body. - * @return The state on success, otherwise an failure. - */ - override def validate[B](implicit request: ExtractableRequest[B], ec: ExecutionContext): Future[CookieState] = { - Future.fromTry(clientState.flatMap(clientState => providerState.flatMap(providerState => - if (clientState != providerState) Failure(new OAuth2StateException(StateIsNotEqual)) - else if (clientState.isExpired) Failure(new OAuth2StateException(StateIsExpired)) - else Success(clientState) - ))) - } - - /** - * Sends a cookie to the client containing the serialized state. - * - * @param result The result to send to the client. - * @param state The state to publish. - * @param request The request. - * @tparam B The type of the request body. - * @return The result to send to the client. - */ - override def publish[B](result: Result, state: State)(implicit request: ExtractableRequest[B]): Result = { - result.withCookies(Cookie( - name = settings.cookieName, - value = serialize(state), - maxAge = Some(settings.expirationTime.toSeconds.toInt), - path = settings.cookiePath, - domain = settings.cookieDomain, - secure = settings.secureCookie, - httpOnly = settings.httpOnlyCookie)) - } - - /** - * Returns a serialized value of the state. - * - * @param state The state to serialize. - * @return A serialized value of the state. - */ - override def serialize(state: State): String = CookieState.serialize(state, cookieSigner) - - /** - * Gets the state from cookie. - * - * @param request The request header. - * @return The OAuth2 state on success, otherwise a failure. - */ - private def clientState(implicit request: RequestHeader): Try[CookieState] = { - request.cookies.get(settings.cookieName) match { - case Some(cookie) => CookieState.unserialize(cookie.value, cookieSigner) - case None => Failure(new OAuth2StateException(ClientStateDoesNotExists.format(settings.cookieName))) - } - } - - /** - * Gets the state from request the after the provider has redirected back from the authorization server - * with the access code. - * - * @param request The request. - * @tparam B The type of the request body. - * @return The OAuth2 state on success, otherwise a failure. - */ - private def providerState[B](implicit request: ExtractableRequest[B]): Try[CookieState] = { - request.extractString(State) match { - case Some(state) => CookieState.unserialize(state, cookieSigner) - case _ => Failure(new OAuth2StateException(ProviderStateDoesNotExists.format(State))) - } - } -} - -/** - * The CookieStateProvider companion object. - */ -object CookieStateProvider { - - /** - * The error messages. - */ - val ClientStateDoesNotExists = "[Silhouette][CookieState] State cookie doesn't exists for name: %s" - val ProviderStateDoesNotExists = "[Silhouette][CookieState] Couldn't find state in request for param: %s" - val StateIsNotEqual = "[Silhouette][CookieState] State isn't equal" - val StateIsExpired = "[Silhouette][CookieState] State is expired" - val InvalidJson = "[Silhouette][CookieState] Cannot parse invalid Json: %s" - val InvalidStateFormat = "[Silhouette][CookieState] Cannot build OAuth2State because of invalid Json format: %s" - val InvalidCookieSignature = "[Silhouette][CookieState] Invalid cookie signature" -} - -/** - * The settings for the cookie state. - * - * @param cookieName The cookie name. - * @param cookiePath The cookie path. - * @param cookieDomain The cookie domain. - * @param secureCookie Whether this cookie is secured, sent only for HTTPS requests. - * @param httpOnlyCookie Whether this cookie is HTTP only, i.e. not accessible from client-side JavaScript code. - * @param expirationTime State expiration. Defaults to 5 minutes which provides sufficient time to log in, but - * not too much. This is a balance between convenience and security. - */ -case class CookieStateSettings( - cookieName: String = "OAuth2State", - cookiePath: String = "/", - cookieDomain: Option[String] = None, - secureCookie: Boolean = true, - httpOnlyCookie: Boolean = true, - expirationTime: FiniteDuration = 5 minutes) diff --git a/silhouette/app/com/mohiva/play/silhouette/impl/providers/oauth2/state/DummyState.scala b/silhouette/app/com/mohiva/play/silhouette/impl/providers/oauth2/state/DummyState.scala deleted file mode 100644 index 3d35675d7..000000000 --- a/silhouette/app/com/mohiva/play/silhouette/impl/providers/oauth2/state/DummyState.scala +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright 2015 Mohiva Organisation (license at mohiva dot com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.mohiva.play.silhouette.impl.providers.oauth2.state - -import com.mohiva.play.silhouette.api.util.ExtractableRequest -import com.mohiva.play.silhouette.impl.providers.{ OAuth2State, OAuth2StateProvider } -import play.api.mvc.Result - -import scala.concurrent.{ ExecutionContext, Future } - -/** - * A dummy state which can be used to avoid state validation. This can be useful if the state - * should be validated on client side. - */ -case class DummyState() extends OAuth2State { - override def isExpired = false -} - -/** - * Handles the dummy state. - */ -class DummyStateProvider extends OAuth2StateProvider { - - /** - * The type of the state implementation. - */ - override type State = DummyState - - /** - * Builds the state. - * - * @param request The request. - * @param ec The execution context to handle the asynchronous operations. - * @tparam B The type of the request body. - * @return The build state. - */ - override def build[B](implicit request: ExtractableRequest[B], ec: ExecutionContext): Future[DummyState] = - Future.successful(DummyState()) - - /** - * Returns always a valid state avoid authentication errors. - * - * @param request The request. - * @param ec The execution context to handle the asynchronous operations. - * @tparam B The type of the request body. - * @return Always a valid state avoid authentication errors. - */ - override def validate[B](implicit request: ExtractableRequest[B], ec: ExecutionContext) = - Future.successful(DummyState()) - - /** - * Returns the original result. - * - * @param result The result to send to the client. - * @param state The state to publish. - * @param request The request. - * @tparam B The type of the request body. - * @return The result to send to the client. - */ - override def publish[B](result: Result, state: State)(implicit request: ExtractableRequest[B]) = result - - /** - * Returns a serialized value of the state. - * - * @param state The state to serialize. - * @return A serialized value of the state. - */ - override def serialize(state: State) = "" -} diff --git a/silhouette/app/com/mohiva/play/silhouette/impl/providers/state/CsrfStateItemHandler.scala b/silhouette/app/com/mohiva/play/silhouette/impl/providers/state/CsrfStateItemHandler.scala index 2703b6f2c..769078792 100644 --- a/silhouette/app/com/mohiva/play/silhouette/impl/providers/state/CsrfStateItemHandler.scala +++ b/silhouette/app/com/mohiva/play/silhouette/impl/providers/state/CsrfStateItemHandler.scala @@ -1,5 +1,5 @@ /** - * Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * Copyright 2017 Mohiva Organisation (license at mohiva dot com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,17 +28,33 @@ import play.api.mvc.{ Cookie, RequestHeader, Result } import scala.concurrent.duration._ import scala.concurrent.{ ExecutionContext, Future } +import scala.language.postfixOps import scala.util.{ Failure, Success, Try } /** - * Csrf State is a sub type of SocialStateItem + * The item the handler can handle. * - * @param value wrapper around the csrf state value + * @param token A unique token used to protect the application against CSRF attacks. */ -case class CsrfState(value: String) extends SocialStateItem +case class CsrfStateItem(token: String) extends SocialStateItem /** - * Handles csrf state. + * The companion object of the [[CsrfStateItem]]. + */ +object CsrfStateItem { + + /** + * Converts the [[CsrfStateItem]] to JSON and vice versa. + */ + implicit val csrfFormat: Format[CsrfStateItem] = Json.format[CsrfStateItem] +} + +/** + * Protects the application against CSRF attacks. + * + * The handler stores a unique token in provider state and the same token in a signed client side cookie. After the + * provider redirects back to the application both tokens will be compared. If both tokens are the same than the + * application can trust the redirect source. * * @param settings The state settings. * @param idGenerator The ID generator used to create the state value. @@ -47,12 +63,14 @@ case class CsrfState(value: String) extends SocialStateItem class CsrfStateItemHandler @Inject() ( settings: CsrfStateSettings, idGenerator: IDGenerator, - cookieSigner: CookieSigner) extends SocialStateItemHandler with PublishableSocialStateItemHandler { + cookieSigner: CookieSigner +) extends SocialStateItemHandler + with PublishableSocialStateItemHandler { /** * The item the handler can handle. */ - override type Item = CsrfState + override type Item = CsrfStateItem /** * Gets the state item the handler can handle. @@ -60,11 +78,7 @@ class CsrfStateItemHandler @Inject() ( * @param ec The execution context to handle the asynchronous operations. * @return The state params the handler can handle. */ - override def item(implicit ec: ExecutionContext): Future[Item] = { - idGenerator.generate.map { - CsrfState(_) - } - } + override def item(implicit ec: ExecutionContext): Future[Item] = idGenerator.generate.map(CsrfStateItem.apply) /** * Indicates if a handler can handle the given [[SocialStateItem]]. @@ -94,8 +108,8 @@ class CsrfStateItemHandler @Inject() ( override def canHandle[B](item: ItemStructure)(implicit request: ExtractableRequest[B]): Boolean = { item.id == ID && { clientState match { - case Success(token) => token == item.data.as[Item] - case Failure(_) => false + case Success(i) => i == item.data.as[Item] + case Failure(_) => false } } } @@ -117,12 +131,16 @@ class CsrfStateItemHandler @Inject() ( * @tparam B The type of the request body. * @return The unserialized state item. */ - override def unserialize[B](item: ItemStructure)(implicit request: ExtractableRequest[B], ec: ExecutionContext): Future[Item] = { + override def unserialize[B](item: ItemStructure)( + implicit + request: ExtractableRequest[B], + ec: ExecutionContext + ): Future[Item] = { Future.fromTry(Try(item.data.as[Item])) } /** - * Publishes the Csrf State to the client. + * Publishes the CSRF token to the client. * * @param item The item to publish. * @param result The result to send to the client. @@ -133,23 +151,24 @@ class CsrfStateItemHandler @Inject() ( override def publish[B](item: Item, result: Result)(implicit request: ExtractableRequest[B]): Result = { result.withCookies(Cookie( name = settings.cookieName, - value = cookieSigner.sign(item.value), + value = cookieSigner.sign(item.token), maxAge = Some(settings.expirationTime.toSeconds.toInt), path = settings.cookiePath, domain = settings.cookieDomain, secure = settings.secureCookie, - httpOnly = settings.httpOnlyCookie)) + httpOnly = settings.httpOnlyCookie + )) } /** - * Gets the Csrf State from the cookie. + * Gets the CSRF token from the cookie. * * @param request The request header. - * @return The OAuth2 state on success, otherwise a failure. + * @return The CSRF token on success, otherwise a failure. */ private def clientState(implicit request: RequestHeader): Try[Item] = { request.cookies.get(settings.cookieName) match { - case Some(cookie) => cookieSigner.extract(cookie.value).map(token => CsrfState(token)) + case Some(cookie) => cookieSigner.extract(cookie.value).map(token => CsrfStateItem(token)) case None => Failure(new OAuth2StateException(ClientStateDoesNotExists.format(settings.cookieName))) } } @@ -159,23 +178,16 @@ class CsrfStateItemHandler @Inject() ( * The companion object. */ object CsrfStateItemHandler { - val ID = "csrf-state" /** - * The error messages. + * The ID of the handler. */ - val ClientStateDoesNotExists = "[Silhouette][CookieState] State cookie doesn't exists for name: %s" - val ProviderStateDoesNotExists = "[Silhouette][CookieState] Couldn't find state in request for param: %s" - val StateIsNotEqual = "[Silhouette][CookieState] State isn't equal" - val StateIsExpired = "[Silhouette][CookieState] State is expired" - val InvalidJson = "[Silhouette][CookieState] Cannot parse invalid Json: %s" - val InvalidStateFormat = "[Silhouette][CookieState] Cannot build OAuth2State because of invalid Json format: %s" - val InvalidCookieSignature = "[Silhouette][CookieState] Invalid cookie signature" + val ID = "csrf-state" /** - * Json Format for the Csrf State + * The error messages. */ - implicit val csrfFormat: Format[CsrfState] = Json.format[CsrfState] + val ClientStateDoesNotExists = "[Silhouette][CsrfStateItemHandler] State cookie doesn't exists for name: %s" } /** @@ -190,7 +202,7 @@ object CsrfStateItemHandler { * not too much. This is a balance between convenience and security. */ case class CsrfStateSettings( - cookieName: String = "OAuth2CsrfState", + cookieName: String = "CsrfState", cookiePath: String = "/", cookieDomain: Option[String] = None, secureCookie: Boolean = true, diff --git a/silhouette/app/com/mohiva/play/silhouette/impl/providers/state/UserStateItemHandler.scala b/silhouette/app/com/mohiva/play/silhouette/impl/providers/state/UserStateItemHandler.scala index ade396c6d..0027aba47 100644 --- a/silhouette/app/com/mohiva/play/silhouette/impl/providers/state/UserStateItemHandler.scala +++ b/silhouette/app/com/mohiva/play/silhouette/impl/providers/state/UserStateItemHandler.scala @@ -1,5 +1,5 @@ /** - * Copyright 2016 Mohiva Organisation (license at mohiva dot com) + * Copyright 2017 Mohiva Organisation (license at mohiva dot com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,19 +25,35 @@ import scala.concurrent.{ ExecutionContext, Future } import scala.reflect.ClassTag import scala.util.Try +/** + * A default user state item where state is of type Map[String, String]. + */ +case class UserStateItem(state: Map[String, String]) extends SocialStateItem + +/** + * The companion object of the [[UserStateItem]]. + */ +object UserStateItem { + + /** + * Converts the [[UserStateItem]] to JSON and vice versa. + */ + implicit val csrfFormat: Format[UserStateItem] = Json.format[UserStateItem] +} + /** * Handles user defined state. * - * @param userState The user state. - * @param format The JSON format to the transform the user state into JSON. - * @param classTag The class tag for the user state item. + * @param item The user state item. + * @param format The JSON format to the transform the user state into JSON and vice versa. + * @param classTag The class tag for the user state item. * @tparam S The type of the user state. */ -class UserStateItemHandler[S <: SocialStateItem](userState: S)( +class UserStateItemHandler[S <: SocialStateItem](item: S)( implicit format: Format[S], - classTag: ClassTag[S]) - extends SocialStateItemHandler { + classTag: ClassTag[S] +) extends SocialStateItemHandler { /** * The item the handler can handle. @@ -50,7 +66,7 @@ class UserStateItemHandler[S <: SocialStateItem](userState: S)( * @param ec The execution context to handle the asynchronous operations. * @return The state params the handler can handle. */ - override def item(implicit ec: ExecutionContext): Future[Item] = Future.successful(userState) + override def item(implicit ec: ExecutionContext): Future[Item] = Future.successful(item) /** * Indicates if a handler can handle the given `SocialStateItem`. @@ -109,5 +125,9 @@ class UserStateItemHandler[S <: SocialStateItem](userState: S)( * The companion object. */ object UserStateItemHandler { + + /** + * The ID of the state handler. + */ val ID = "user-state" } diff --git a/silhouette/test/Helpers.scala b/silhouette/test/Helpers.scala index a68e90136..f6b34a355 100644 --- a/silhouette/test/Helpers.scala +++ b/silhouette/test/Helpers.scala @@ -15,18 +15,26 @@ */ package test -import org.specs2.execute.{ AsResult, Result } +import com.mohiva.play.silhouette.api.AuthInfo +import com.mohiva.play.silhouette.impl.providers.{ SocialProfile, SocialStateItem, StatefulAuthInfo } +import org.specs2.execute.{ AsResult, Result => Specs2Result } +import org.specs2.matcher.{ JsonMatchers, MatchResult } +import org.specs2.mock.Mockito import org.specs2.mutable.Around import play.api.libs.json.{ JsValue, Json } +import play.api.mvc.{ Result => PlayResult } +import play.api.test.PlaySpecification +import scala.concurrent.Future import scala.io.{ Codec, Source } +import scala.reflect.ClassTag /** * Executes a before method in the context of the around method. */ trait BeforeWithinAround extends Around { def before: Any - abstract override def around[T: AsResult](t: => T): Result = super.around { + abstract override def around[T: AsResult](t: => T): Specs2Result = super.around { before; t } } @@ -36,7 +44,7 @@ trait BeforeWithinAround extends Around { */ trait AfterWithinAround extends Around { def after: Any - abstract override def around[T: AsResult](t: => T): Result = super.around { + abstract override def around[T: AsResult](t: => T): Specs2Result = super.around { try { t } finally { after } } } @@ -47,11 +55,113 @@ trait AfterWithinAround extends Around { trait BeforeAfterWithinAround extends Around { def before: Any def after: Any - abstract override def around[T: AsResult](t: => T): Result = super.around { + abstract override def around[T: AsResult](t: => T): Specs2Result = super.around { try { before; t } finally { after } } } +/** + * Base test case for the social providers. + */ +trait SocialProviderSpec[A <: AuthInfo] extends PlaySpecification with Mockito with JsonMatchers { + + /** + * Applies a matcher on a simple result. + * + * @param providerResult The result from the provider. + * @param b The matcher block to apply. + * @return A specs2 match result. + */ + def result(providerResult: Future[Either[PlayResult, A]])(b: Future[PlayResult] => MatchResult[_]) = { + await(providerResult) must beLeft[PlayResult].like { + case result => b(Future.successful(result)) + } + } + + /** + * Applies a matcher on a auth info. + * + * @param providerResult The result from the provider. + * @param b The matcher block to apply. + * @return A specs2 match result. + */ + def authInfo(providerResult: Future[Either[PlayResult, A]])(b: A => MatchResult[_]) = { + await(providerResult) must beRight[A].like { + case authInfo => b(authInfo) + } + } + + /** + * Applies a matcher on a social profile. + * + * @param providerResult The result from the provider. + * @param b The matcher block to apply. + * @return A specs2 match result. + */ + def profile(providerResult: Future[SocialProfile])(b: SocialProfile => MatchResult[_]) = { + await(providerResult) must beLike[SocialProfile] { + case socialProfile => b(socialProfile) + } + } + + /** + * Matches a partial function against a failure message. + * + * This method checks if an exception was thrown in a future. + * @see https://groups.google.com/d/msg/specs2-users/MhJxnvyS1_Q/FgAK-5IIIhUJ + * + * @param providerResult The result from the provider. + * @param f A matcher function. + * @return A specs2 match result. + */ + def failed[E <: Throwable: ClassTag](providerResult: Future[_])(f: => PartialFunction[Throwable, MatchResult[_]]) = { + implicit class Rethrow(t: Throwable) { + def rethrow = { throw t; t } + } + + lazy val result = await(providerResult.failed) + + result must not(throwAn[E]) + result.rethrow must throwAn[E].like(f) + } +} + +/** + * Base test case for the social state providers. + */ +trait SocialStateProviderSpec[A <: AuthInfo, S <: SocialStateItem] extends SocialProviderSpec[A] { + + /** + * Applies a matcher on a simple result. + * + * @param providerResult The result from the provider. + * @param b The matcher block to apply. + * @return A specs2 match result. + */ + def statefulResult(providerResult: Future[Either[PlayResult, StatefulAuthInfo[A, S]]])( + b: Future[PlayResult] => MatchResult[_] + ) = { + await(providerResult) must beLeft[PlayResult].like { + case result => b(Future.successful(result)) + } + } + + /** + * Applies a matcher on a stateful auth info. + * + * @param providerResult The result from the provider. + * @param b The matcher block to apply. + * @return A specs2 match result. + */ + def statefulAuthInfo(providerResult: Future[Either[PlayResult, StatefulAuthInfo[A, S]]])( + b: StatefulAuthInfo[A, S] => MatchResult[_] + ) = { + await(providerResult) must beRight[StatefulAuthInfo[A, S]].like { + case info => b(info) + } + } +} + /** * Some test-related helper methods. */ diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/DefaultSocialStateHandlerSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/DefaultSocialStateHandlerSpec.scala new file mode 100644 index 000000000..4566999cc --- /dev/null +++ b/silhouette/test/com/mohiva/play/silhouette/impl/providers/DefaultSocialStateHandlerSpec.scala @@ -0,0 +1,195 @@ +/** + * Copyright 2015 Mohiva Organisation (license at mohiva dot com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mohiva.play.silhouette.impl.providers + +import com.mohiva.play.silhouette.api.crypto.CookieSigner +import com.mohiva.play.silhouette.api.exceptions.ProviderException +import com.mohiva.play.silhouette.api.util.ExtractableRequest +import com.mohiva.play.silhouette.impl.providers.DefaultSocialStateHandler._ +import com.mohiva.play.silhouette.impl.providers.SocialStateItem.ItemStructure +import org.specs2.matcher.JsonMatchers +import org.specs2.mock.Mockito +import org.specs2.specification.Scope +import play.api.libs.concurrent.Execution.Implicits._ +import play.api.libs.json.Json +import play.api.mvc.Results +import play.api.test.{ FakeRequest, PlaySpecification } + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.util.Success + +/** + * Test case for the [[DefaultSocialStateHandler]] class. + */ +class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with JsonMatchers { + + "The `withHandler` method" should { + "return a new state handler with the given item handler added" in new Context { + val newHandler = mock[SocialStateItemHandler].smart + + stateHandler.handlers.size must be equalTo 2 + stateHandler.withHandler(newHandler).handlers.size must be equalTo 3 + } + } + + "The `state` method" should { + "return the social state" in new Context { + Default.itemHandler.item returns Future.successful(Default.item) + Publishable.itemHandler.item returns Future.successful(Publishable.item) + + await(stateHandler.state) must be equalTo state + } + } + + "The `serialize` method" should { + "return the serialized social state" in new Context { + Default.itemHandler.canHandle(Publishable.item) returns None + Default.itemHandler.canHandle(Default.item) returns Some(Default.item) + Default.itemHandler.serialize(Default.item) returns Default.structure + + Publishable.itemHandler.canHandle(Default.item) returns None + Publishable.itemHandler.canHandle(Publishable.item) returns Some(Publishable.item) + Publishable.itemHandler.serialize(Publishable.item) returns Publishable.structure + + stateHandler.serialize(state) must be equalTo s"${Default.structure}.${Publishable.structure}" + } + } + + "The `unserialize` method" should { + "return an empty social state for an empty string" in new Context { + implicit val request = new ExtractableRequest(FakeRequest()) + + await(stateHandler.unserialize("")) must be equalTo SocialState(Set()) + } + + "throw an ProviderException if the serialized item structure cannot be extracted" in new Context { + implicit val request = new ExtractableRequest(FakeRequest()) + val serialized = s"some-wired-content" + + await(stateHandler.unserialize(serialized)) must throwA[ProviderException].like { + case e => + e.getMessage must startWith(ItemExtractionError.format(serialized)) + } + } + + "throw an ProviderException if none of the item handlers can handle the given state" in new Context { + implicit val request = new ExtractableRequest(FakeRequest()) + val serialized = s"${Default.structure.asString}" + + Default.itemHandler.canHandle(any[ItemStructure])(any) returns false + Publishable.itemHandler.canHandle(any[ItemStructure])(any) returns false + + await(stateHandler.unserialize(serialized)) must throwA[ProviderException].like { + case e => + e.getMessage must startWith(MissingItemHandlerError.format(Default.structure)) + } + } + + "return the unserialized social state" in new Context { + implicit val request = new ExtractableRequest(FakeRequest()) + val serialized = s"${Default.structure.asString}.${Publishable.structure.asString}" + + Default.itemHandler.canHandle(Publishable.structure) returns false + Default.itemHandler.canHandle(Default.structure) returns true + Default.itemHandler.unserialize(Default.structure) returns Future.successful(Default.item) + + Publishable.itemHandler.canHandle(Default.structure) returns false + Publishable.itemHandler.canHandle(Publishable.structure) returns true + Publishable.itemHandler.unserialize(Publishable.structure) returns Future.successful(Publishable.item) + + await(stateHandler.unserialize(serialized)) must be equalTo SocialState(Set(Default.item, Publishable.item)) + } + } + + "The `publish` method" should { + "should publish the state with the publishable handler that is responsible for the item" in new Context { + implicit val request = new ExtractableRequest(FakeRequest()) + val result = Results.Ok + val publishedResult = Results.Ok.withHeaders("X-PUBLISHED" -> "true") + + Publishable.itemHandler.publish(Publishable.item, result) returns publishedResult + Publishable.itemHandler.canHandle(Default.item) returns None + Publishable.itemHandler.canHandle(Publishable.item) returns Some(Publishable.item) + + stateHandler.publish(result, state) must be equalTo publishedResult + } + + "should not publish the state if no publishable handler is responsible" in new Context { + implicit val request = FakeRequest() + val result = Results.Ok + + Publishable.itemHandler.canHandle(Default.item) returns None + Publishable.itemHandler.canHandle(Publishable.item) returns None + + stateHandler.publish(result, state) must be equalTo result + } + } + + /** + * The context. + */ + trait Context extends Scope { + + /** + * A default handler implementation. + */ + case class DefaultItem() extends SocialStateItem + trait DefaultItemHandler extends SocialStateItemHandler { + type Item = DefaultItem + } + object Default { + val itemHandler = mock[DefaultItemHandler].smart + val item = DefaultItem() + val structure = ItemStructure("default", Json.obj()) + } + + /** + * A publishable handler implementation. + */ + case class PublishableItem() extends SocialStateItem + trait PublishableItemHandler extends SocialStateItemHandler with PublishableSocialStateItemHandler { + type Item = PublishableItem + } + object Publishable { + val itemHandler = mock[PublishableItemHandler].smart + val item = PublishableItem() + val structure = ItemStructure("publishable", Json.obj()) + } + + /** + * The cookie signer implementation. + * + * The cookie signer returns the same value as passed to the methods. This is enough for testing. + */ + val cookieSigner = { + val c = mock[CookieSigner].smart + c.sign(any) answers { p => p.asInstanceOf[String] } + c.extract(any) answers { p => Success(p.asInstanceOf[String]) } + c + } + + /** + * The state. + */ + val state = SocialState(Set(Default.item, Publishable.item)) + + /** + * The state handler to test. + */ + val stateHandler = new DefaultSocialStateHandler(Set(Default.itemHandler, Publishable.itemHandler), cookieSigner) + } +} diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/OAuth1ProviderSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/OAuth1ProviderSpec.scala index a1dd65f18..d12613795 100644 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/OAuth1ProviderSpec.scala +++ b/silhouette/test/com/mohiva/play/silhouette/impl/providers/OAuth1ProviderSpec.scala @@ -26,6 +26,7 @@ import play.api.libs.concurrent.Execution.Implicits._ import play.api.mvc.{ Result, Results } import play.api.test.{ FakeRequest, WithApplication } import play.mvc.Http.HeaderNames +import test.SocialProviderSpec import scala.concurrent.Future diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/OAuth2ProviderSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/OAuth2ProviderSpec.scala index e371f9133..40450a388 100644 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/OAuth2ProviderSpec.scala +++ b/silhouette/test/com/mohiva/play/silhouette/impl/providers/OAuth2ProviderSpec.scala @@ -30,6 +30,7 @@ import play.api.libs.ws.{ WSRequest, WSResponse } import play.api.mvc.Result import play.api.test.{ FakeRequest, WithApplication } import play.mvc.Http.HeaderNames +import test.SocialStateProviderSpec import scala.concurrent.{ ExecutionContext, Future } diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/OpenIDProviderSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/OpenIDProviderSpec.scala index 444795c37..09fcd9956 100644 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/OpenIDProviderSpec.scala +++ b/silhouette/test/com/mohiva/play/silhouette/impl/providers/OpenIDProviderSpec.scala @@ -24,6 +24,7 @@ import org.specs2.specification.Scope import play.api.libs.concurrent.Execution.Implicits._ import play.api.test.{ FakeRequest, WithApplication } import play.mvc.Http.HeaderNames +import test.SocialProviderSpec import scala.concurrent.Future diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/SocialProviderSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/SocialProviderSpec.scala deleted file mode 100644 index 64998dde6..000000000 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/SocialProviderSpec.scala +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright 2015 Mohiva Organisation (license at mohiva dot com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.mohiva.play.silhouette.impl.providers - -import com.mohiva.play.silhouette.api.AuthInfo -import org.specs2.matcher.{ JsonMatchers, MatchResult } -import org.specs2.mock.Mockito -import play.api.mvc.Result -import play.api.test.PlaySpecification - -import scala.concurrent.Future -import scala.reflect.ClassTag - -/** - * Abstract test case for the social providers. - */ -abstract class SocialProviderSpec[A <: AuthInfo] extends PlaySpecification with Mockito with JsonMatchers { - - /** - * Applies a matcher on a simple result. - * - * @param providerResult The result from the provider. - * @param b The matcher block to apply. - * @return A specs2 match result. - */ - def result(providerResult: Future[Either[Result, A]])(b: Future[Result] => MatchResult[_]) = { - await(providerResult) must beLeft[Result].like { - case result => b(Future.successful(result)) - } - } - - /** - * Applies a matcher on a auth info. - * - * @param providerResult The result from the provider. - * @param b The matcher block to apply. - * @return A specs2 match result. - */ - def authInfo(providerResult: Future[Either[Result, A]])(b: A => MatchResult[_]) = { - await(providerResult) must beRight[A].like { - case authInfo => b(authInfo) - } - } - - /** - * Applies a matcher on a social profile. - * - * @param providerResult The result from the provider. - * @param b The matcher block to apply. - * @return A specs2 match result. - */ - def profile(providerResult: Future[SocialProfile])(b: SocialProfile => MatchResult[_]) = { - await(providerResult) must beLike[SocialProfile] { - case socialProfile => b(socialProfile) - } - } - - /** - * Matches a partial function against a failure message. - * - * This method checks if an exception was thrown in a future. - * @see https://groups.google.com/d/msg/specs2-users/MhJxnvyS1_Q/FgAK-5IIIhUJ - * - * @param providerResult The result from the provider. - * @param f A matcher function. - * @return A specs2 match result. - */ - def failed[E <: Throwable: ClassTag](providerResult: Future[_])(f: => PartialFunction[Throwable, MatchResult[_]]) = { - implicit class Rethrow(t: Throwable) { - def rethrow = { throw t; t } - } - - lazy val result = await(providerResult.failed) - - result must not(throwAn[E]) - result.rethrow must throwAn[E].like(f) - } -} diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/SocialStateProviderSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/SocialStateProviderSpec.scala deleted file mode 100644 index 1bd9845a4..000000000 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/SocialStateProviderSpec.scala +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright 2015 Mohiva Organisation (license at mohiva dot com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.mohiva.play.silhouette.impl.providers - -import com.mohiva.play.silhouette.api.AuthInfo -import org.specs2.matcher.MatchResult -import play.api.mvc.Result - -import scala.concurrent.Future - -/** - * Abstract test case for the social state providers. - */ -abstract class SocialStateProviderSpec[A <: AuthInfo, S <: SocialStateItem] extends SocialProviderSpec[A] { - - /** - * Applies a matcher on a simple result. - * - * @param providerResult The result from the provider. - * @param b The matcher block to apply. - * @return A specs2 match result. - */ - def statefulResult(providerResult: Future[Either[Result, StatefulAuthInfo[A, S]]])(b: Future[Result] => MatchResult[_]) = { - await(providerResult) must beLeft[Result].like { - case result => b(Future.successful(result)) - } - } - - /** - * Applies a matcher on a stateful auth info. - * - * @param providerResult The result from the provider. - * @param b The matcher block to apply. - * @return A specs2 match result. - */ - def statefulAuthInfo(providerResult: Future[Either[Result, StatefulAuthInfo[A, S]]])(b: StatefulAuthInfo[A, S] => MatchResult[_]) = { - await(providerResult) must beRight[StatefulAuthInfo[A, S]].like { - case info => b(info) - } - } -} diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/oauth2/state/CookieStateSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/oauth2/state/CookieStateSpec.scala deleted file mode 100644 index 22d03ce13..000000000 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/oauth2/state/CookieStateSpec.scala +++ /dev/null @@ -1,276 +0,0 @@ -/** - * Copyright 2015 Mohiva Organisation (license at mohiva dot com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.mohiva.play.silhouette.impl.providers.oauth2.state - -import java.util.regex.Pattern - -import com.mohiva.play.silhouette.api.crypto.{ Base64, CookieSigner } -import com.mohiva.play.silhouette.api.util.{ Clock, IDGenerator } -import com.mohiva.play.silhouette.impl.exceptions.OAuth2StateException -import com.mohiva.play.silhouette.impl.providers.OAuth2Provider._ -import com.mohiva.play.silhouette.impl.providers.oauth2.state.CookieState._ -import com.mohiva.play.silhouette.impl.providers.oauth2.state.CookieStateProvider._ -import org.joda.time.DateTime -import org.specs2.control.NoLanguageFeatures -import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito -import org.specs2.specification.Scope -import play.api.libs.concurrent.Execution.Implicits._ -import play.api.mvc.{ Cookie, Results } -import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } - -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.language.postfixOps -import scala.util.{ Failure, Success } - -/** - * Test case for the [[com.mohiva.play.silhouette.impl.providers.oauth2.state.CookieState]] class. - */ -class CookieStateSpec extends PlaySpecification with Mockito with JsonMatchers with NoLanguageFeatures { - - "The `isExpired` method of the state" should { - "return true if the state is expired" in new Context { - state.copy(expirationDate = DateTime.now.minusHours(1)).isExpired must beTrue - } - - "return false if the state isn't expired" in new Context { - state.copy(expirationDate = DateTime.now.plusHours(1)).isExpired must beFalse - } - } - - "The `serialize` method of the state" should { - "sign the cookie" in new WithApplication with Context { - serialize(state, cookieSigner) - - there was one(cookieSigner).sign(any) - } - } - - "The `unserialize` method of the state" should { - "throw a OAuth2StateException if a state contains invalid json" in new WithApplication with Context { - val value = "invalid" - val msg = Pattern.quote(InvalidJson.format(value)) - - unserialize(Base64.encode(value), cookieSigner) must beFailedTry.withThrowable[OAuth2StateException](msg) - } - - "throw an OAuth2StateException if a state contains valid json but invalid state" in new WithApplication with Context { - val value = "{ \"test\": \"test\" }" - val msg = "^" + Pattern.quote(InvalidStateFormat.format("")) + ".*" - - unserialize(Base64.encode(value), cookieSigner) must beFailedTry.withThrowable[OAuth2StateException](msg) - } - - "throw an OAuth2StateException if a state is badly signed" in new WithApplication with Context { - cookieSigner.extract(any) returns Failure(new Exception("Bad signature")) - - val value = serialize(state, cookieSigner) - val msg = Pattern.quote(InvalidCookieSignature) - - unserialize(Base64.encode(value), cookieSigner) must beFailedTry.withThrowable[OAuth2StateException](msg) - } - } - - "The `serialize/unserialize` method of the state" should { - "serialize/unserialize a state" in new WithApplication with Context { - val serialized = serialize(state, cookieSigner) - - unserialize(serialized, cookieSigner) must beSuccessfulTry.withValue(state) - } - } - - "The `build` method of the provider" should { - "return a new state" in new Context { - implicit val req = FakeRequest() - val dateTime = new DateTime(2014, 8, 8, 0, 0, 0) - val value = "value" - - clock.now returns dateTime - idGenerator.generate returns Future.successful(value) - - val s = await(provider.build) - - s.expirationDate must be equalTo dateTime.plusSeconds(settings.expirationTime.toSeconds.toInt) - s.value must be equalTo value - } - } - - "The `validate` method of the provider" should { - "throw an OAuth2StateException if client state doesn't exists" in new Context { - implicit val req = FakeRequest(GET, s"?$State=${provider.serialize(state)}") - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(ClientStateDoesNotExists.format("")) - } - } - - "throw an OAuth2StateException if provider state doesn't exists" in new WithApplication with Context { - implicit val req = FakeRequest(GET, "/").withCookies(Cookie(settings.cookieName, provider.serialize(state))) - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(ProviderStateDoesNotExists.format("")) - } - } - - "throw an OAuth2StateException if client state contains invalid json" in new WithApplication with Context { - val invalidState = Base64.encode("{") - - implicit val req = FakeRequest(GET, s"?$State=${provider.serialize(state)}").withCookies(Cookie(settings.cookieName, invalidState)) - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(InvalidJson.format("")) - } - } - - "throw an OAuth2StateException if client state contains valid json but invalid state" in new WithApplication with Context { - val invalidState = Base64.encode("{ \"test\": \"test\" }") - - implicit val req = FakeRequest(GET, s"?$State=${provider.serialize(state)}").withCookies(Cookie(settings.cookieName, invalidState)) - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(InvalidStateFormat.format("")) - } - } - - "throw an OAuth2StateException if client state is badly signed" in new WithApplication with Context { - cookieSigner.extract(any) returns Failure(new Exception("Bad signature")) - - implicit val req = FakeRequest(GET, s"?$State=${provider.serialize(state)}").withCookies(Cookie(settings.cookieName, provider.serialize(state))) - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(InvalidCookieSignature) - } - } - - "throw an OAuth2StateException if provider state contains invalid json" in new WithApplication with Context { - val invalidState = Base64.encode("{") - - implicit val req = FakeRequest(GET, s"?$State=$invalidState").withCookies(Cookie(settings.cookieName, provider.serialize(state))) - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(InvalidJson.format("")) - } - } - - "throw an OAuth2StateException if provider state contains valid json but invalid state" in new WithApplication with Context { - val invalidState = Base64.encode("{ \"test\": \"test\" }") - - implicit val req = FakeRequest(GET, s"?$State=$invalidState").withCookies(Cookie(settings.cookieName, provider.serialize(state))) - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(InvalidStateFormat.format("")) - } - } - - "throw an OAuth2StateException if client and provider state are not equal" in new WithApplication with Context { - val clientState = state.copy(value = "clientState") - val providerState = state.copy(value = "providerState") - - implicit val req = FakeRequest(GET, s"?$State=${provider.serialize(providerState)}").withCookies(Cookie(settings.cookieName, provider.serialize(clientState))) - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(StateIsNotEqual.format()) - } - } - - "throw an OAuth2StateException if state is expired" in new WithApplication with Context { - val expiredState = state.copy(expirationDate = DateTime.now.minusHours(1)) - - implicit val req = FakeRequest(GET, s"?$State=${provider.serialize(expiredState)}").withCookies(Cookie(settings.cookieName, provider.serialize(expiredState))) - - await(provider.validate) must throwA[OAuth2StateException].like { - case e => e.getMessage must startWith(StateIsExpired.format()) - } - } - - "return the state if it's valid" in new WithApplication with Context { - implicit val req = FakeRequest(GET, s"?$State=${provider.serialize(state)}").withCookies(Cookie(settings.cookieName, provider.serialize(state))) - - await(provider.validate) must be equalTo state - } - } - - "The `publish` method of the provider" should { - "add the state to the cookie" in new Context { - implicit val req = FakeRequest(GET, "/") - val result = Future.successful(provider.publish(Results.Ok, state)) - - cookies(result).get(settings.cookieName) should beSome[Cookie].which { c => - c.name must be equalTo settings.cookieName - c.value must be equalTo provider.serialize(state) - // https://github.com/mohiva/play-silhouette/issues/273 - c.maxAge must beSome[Int].which(_ <= settings.expirationTime.toSeconds.toInt) - c.path must be equalTo settings.cookiePath - c.domain must be equalTo settings.cookieDomain - c.secure must be equalTo settings.secureCookie - } - } - } - - /** - * The context. - */ - trait Context extends Scope { - - /** - * The ID generator implementation. - */ - lazy val idGenerator = mock[IDGenerator].smart - - /** - * The clock implementation. - */ - lazy val clock = mock[Clock].smart - - /** - * The settings. - */ - lazy val settings = CookieStateSettings( - cookieName = "OAuth2State", - cookiePath = "/", - cookieDomain = None, - secureCookie = true, - httpOnlyCookie = true, - expirationTime = 5 minutes - ) - - /** - * The cookie signer implementation. - * - * The cookie signer returns the same value as passed to the methods. This is enough for testing. - */ - lazy val cookieSigner = { - val c = mock[CookieSigner].smart - c.sign(any) answers { p => p.asInstanceOf[String] } - c.extract(any) answers { p => Success(p.asInstanceOf[String]) } - c - } - - /** - * The provider implementation to test. - */ - lazy val provider = new CookieStateProvider(settings, idGenerator, cookieSigner, clock) - - /** - * A state to test. - */ - lazy val state = spy(new CookieState( - expirationDate = DateTime.now.plusSeconds(settings.expirationTime.toSeconds.toInt), - value = "value" - )) - } -} diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/oauth2/state/DummyStateSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/oauth2/state/DummyStateSpec.scala deleted file mode 100644 index 5874c7b4c..000000000 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/oauth2/state/DummyStateSpec.scala +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright 2015 Mohiva Organisation (license at mohiva dot com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.mohiva.play.silhouette.impl.providers.oauth2.state - -import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito -import org.specs2.specification.Scope -import play.api.libs.concurrent.Execution.Implicits._ -import play.api.mvc.Results -import play.api.test.{ FakeRequest, PlaySpecification, WithApplication } - -/** - * Test case for the [[com.mohiva.play.silhouette.impl.providers.oauth2.state.DummyState]] class. - */ -class DummyStateSpec extends PlaySpecification with Mockito with JsonMatchers { - - "The `isExpired` method of the state" should { - "return false" in new Context { - state.isExpired must beFalse - } - } - - "The `build` method of the provider" should { - "return a new state" in new Context { - implicit val req = FakeRequest() - - await(provider.build) must be equalTo state - } - } - - "The `validate` method of the provider" should { - "return the state if it's valid" in new WithApplication with Context { - implicit val req = FakeRequest() - - await(provider.validate) must be equalTo state - } - } - - "The `publish` method of the provider" should { - "return the original result" in new Context { - implicit val req = FakeRequest(GET, "/") - val result = Results.Ok - - provider.publish(result, state) must be equalTo result - } - } - - "The `serialize` method of the provider" should { - "return an empty string" in new Context { - provider.serialize(state) must be equalTo "" - } - } - - /** - * The context. - */ - trait Context extends Scope { - - /** - * The provider implementation to test. - */ - lazy val provider = new DummyStateProvider() - - /** - * A state to test. - */ - lazy val state = DummyState() - } -} diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/CsrfStateItemHandlerSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/CsrfStateItemHandlerSpec.scala index c8cab2ab6..cb9fdfe94 100644 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/CsrfStateItemHandlerSpec.scala +++ b/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/CsrfStateItemHandlerSpec.scala @@ -19,18 +19,17 @@ import com.mohiva.play.silhouette.api.crypto.CookieSigner import com.mohiva.play.silhouette.api.util.IDGenerator import com.mohiva.play.silhouette.impl.providers.SocialStateItem import com.mohiva.play.silhouette.impl.providers.SocialStateItem.ItemStructure +import com.mohiva.play.silhouette.impl.providers.state.CsrfStateItemHandler._ import org.specs2.matcher.JsonMatchers import org.specs2.mock.Mockito import org.specs2.specification.Scope -import play.api.libs.json.{ Json } +import play.api.libs.json.Json +import play.api.mvc.{ Cookie, Results } import play.api.test.{ FakeRequest, PlaySpecification } -import scala.util.Success -import scala.concurrent.duration._ -import play.api.libs.concurrent.Execution.Implicits._ -import play.api.mvc.Cookie - +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future +import scala.util.Success /** * Test case for the [[CsrfStateItemHandler]] class. @@ -38,80 +37,89 @@ import scala.concurrent.Future class CsrfStateItemHandlerSpec extends PlaySpecification with Mockito with JsonMatchers { "The `item` method" should { - "return csrfState" in new Context { + "return the CSRF state item" in new Context { idGenerator.generate returns Future.successful(csrfToken) - await(csrfStateHandler.item) must beAnInstanceOf[CsrfState] + + await(csrfStateItemHandler.item) must be equalTo csrfStateItem } } "The `canHandle` method" should { - "return `Some[SocialStateItem]` if it can handle the given `SocialStateItem`" in new Context { - csrfStateHandler.canHandle(csrfState) must beSome[SocialStateItem] + "return the same item if it can handle the given item" in new Context { + csrfStateItemHandler.canHandle(csrfStateItem) must beSome(csrfStateItem) } - "should return `None` if it can't handle the given `SocialStateItem`" in new Context { - csrfStateHandler.canHandle(userState) must beNone + "should return `None` if it can't handle the given item" in new Context { + val nonCsrfState = mock[SocialStateItem].smart + + csrfStateItemHandler.canHandle(nonCsrfState) must beNone } } "The `canHandle` method" should { - "return true if it can handle the given `ItemStructure`" in new Context { - implicit val request = FakeRequest().withCookies(Cookie( - name = settings.cookieName, - value = cookieSigner.sign(csrfState.value), - maxAge = Some(settings.expirationTime.toSeconds.toInt), - path = settings.cookiePath, - domain = settings.cookieDomain, - secure = settings.secureCookie, - httpOnly = settings.httpOnlyCookie)) - csrfStateHandler.canHandle(itemStructure) must beTrue - } + "return false if the give item is for another handler" in new Context { + val nonCsrfItemStructure = mock[ItemStructure].smart + nonCsrfItemStructure.id returns "non-csrf-item" - "return false if it can't handle the given `ItemStructure`" in new Context { implicit val request = FakeRequest() - csrfStateHandler.canHandle(itemStructure.copy(id = "non-csrf-state")) must beFalse + csrfStateItemHandler.canHandle(nonCsrfItemStructure) must beFalse + } + + "return false if client state doesn't match the item state" in new Context { + implicit val request = FakeRequest().withCookies(cookie("invalid-token")) + csrfStateItemHandler.canHandle(csrfItemStructure) must beFalse + } + + "return true if it can handle the given `ItemStructure`" in new Context { + implicit val request = FakeRequest().withCookies(cookie(csrfStateItem.token)) + csrfStateItemHandler.canHandle(csrfItemStructure) must beTrue } } "The `serialize` method" should { - "serialize `CsrfState` to `ItemStructure`" in new Context { - csrfStateHandler.serialize(csrfState) must beAnInstanceOf[ItemStructure] + "return a serialized value of the state item" in new Context { + csrfStateItemHandler.serialize(csrfStateItem).asString must be equalTo csrfItemStructure.asString } } "The `unserialize` method" should { - "unserialize `ItemStructure` to `CsrfState`" in new Context { + "unserialize the state item" in new Context { implicit val request = FakeRequest() - await(csrfStateHandler.unserialize(itemStructure)) must beAnInstanceOf[CsrfState] + + await(csrfStateItemHandler.unserialize(csrfItemStructure)) must be equalTo csrfStateItem } } + "The `publish` method" should { + "publish the state item to the client" in new Context { + implicit val request = FakeRequest() + val result = csrfStateItemHandler.publish(csrfStateItem, Results.Ok) + + cookies(Future.successful(result)).get(settings.cookieName) must beSome(cookie(csrfToken)) + } + } + + /** + * The context. + */ trait Context extends Scope { - import CsrfStateItemHandler._ /** * The ID generator implementation. */ - lazy val idGenerator = mock[IDGenerator].smart + val idGenerator = mock[IDGenerator].smart /** * The settings. */ - lazy val settings = CsrfStateSettings( - cookieName = "OAuth2CsrfState", - cookiePath = "/", - cookieDomain = None, - secureCookie = true, - httpOnlyCookie = true, - expirationTime = 5 minutes - ) + val settings = CsrfStateSettings() /** * The cookie signer implementation. * * The cookie signer returns the same value as passed to the methods. This is enough for testing. */ - lazy val cookieSigner = { + val cookieSigner = { val c = mock[CookieSigner].smart c.sign(any) answers { p => p.asInstanceOf[String] } c.extract(any) answers { p => Success(p.asInstanceOf[String]) } @@ -119,35 +127,38 @@ class CsrfStateItemHandlerSpec extends PlaySpecification with Mockito with JsonM } /** - * An example usage of UserState where state is of type Map[String, String] - * @param state - */ - case class UserState(state: Map[String, String]) extends SocialStateItem - - /** - * An instance of UserState + * A CSRF token. */ - val userState = UserState(Map("path" -> "/login")) + val csrfToken = "csrfToken" /** - * Csrf State value + * A CSRF state item. */ - val csrfToken = "csrfToken" + val csrfStateItem = CsrfStateItem(csrfToken) /** - * An instance of CsrfState + * The serialized type of the CSRF state item. */ - val csrfState = CsrfState(csrfToken) + val csrfItemStructure = ItemStructure(ID, Json.toJson(csrfStateItem)) /** - * Serialized type of CsrfState + * An instance of the CSRF state item handler. */ - val itemStructure = ItemStructure("csrf-state", Json.toJson(csrfState)) + val csrfStateItemHandler = new CsrfStateItemHandler(settings, idGenerator, cookieSigner) /** - * An instance of Csrf State Handler + * A helper method to create a cookie. + * + * @param value The cookie value. + * @return A cookie instance with the given value. */ - val csrfStateHandler = new CsrfStateItemHandler(settings, idGenerator, cookieSigner) + def cookie(value: String): Cookie = Cookie( + name = settings.cookieName, + value = cookieSigner.sign(value), + maxAge = Some(settings.expirationTime.toSeconds.toInt), + path = settings.cookiePath, + domain = settings.cookieDomain, + secure = settings.secureCookie, + httpOnly = settings.httpOnlyCookie) } } - diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/DefaultSocialStateHandlerSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/DefaultSocialStateHandlerSpec.scala deleted file mode 100644 index 87af0d424..000000000 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/DefaultSocialStateHandlerSpec.scala +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright 2015 Mohiva Organisation (license at mohiva dot com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.mohiva.play.silhouette.impl.providers.state - -import com.mohiva.play.silhouette.api.crypto.CookieSigner -import com.mohiva.play.silhouette.api.util.IDGenerator -import com.mohiva.play.silhouette.impl.providers.SocialStateItem.ItemStructure -import com.mohiva.play.silhouette.impl.providers.{ DefaultSocialStateHandler, SocialState, SocialStateItem } -import org.specs2.matcher.JsonMatchers -import org.specs2.mock.Mockito -import org.specs2.specification.Scope -import play.api.libs.json.{ Format, Json } -import play.api.mvc.Cookie -import play.api.test.{ FakeRequest, PlaySpecification } - -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.util.Success -import play.api.libs.concurrent.Execution.Implicits._ - -/** - * Test case for the [[DefaultSocialStateHandler]] class. - */ -class DefaultSocialStateHandlerSpec extends PlaySpecification with Mockito with JsonMatchers { - - "The `state` method" should { - "return `SocialState` which wraps set of states" in new Context { - idGenerator.generate returns Future.successful(csrfToken) - val socialState = await(stateHandler.state) - socialState.items must contain(CsrfState(csrfToken)) - socialState.items must contain(userState) - } - } - - "The `withHandler` method" should { - "return a new instance with updated set of handlers" in new Context { - val updatedProvider = stateHandlerWithoutUserState.withHandler(userStateHandler) - updatedProvider.handlers must contain(userStateHandler) - updatedProvider.handlers must haveLength(2) - } - } - - "The `serialize` method" should { - "create a state String from `SocialState`" in new Context { - idGenerator.generate returns Future.successful(csrfToken) - stateHandler.serialize(SocialState(Set(userState, CsrfState(csrfToken)))) must beAnInstanceOf[String] - } - } - - "The `unserialize` method" should { - "create `SocialState` from a state String" in new Context { - idGenerator.generate returns Future.successful(csrfToken) - val stateParam = stateHandler.serialize(SocialState(Set(userState, csrfState))) - - implicit val request = FakeRequest().withCookies(Cookie( - name = settings.cookieName, - value = cookieSigner.sign(csrfState.value), - maxAge = Some(settings.expirationTime.toSeconds.toInt), - path = settings.cookiePath, - domain = settings.cookieDomain, - secure = settings.secureCookie, - httpOnly = settings.httpOnlyCookie)) - val socialState = await(stateHandler.unserialize(stateParam)) - socialState.items must contain(userState) - socialState.items must contain(csrfState) - } - } - - trait Context extends Scope { - - /** - * The ID generator implementation. - */ - lazy val idGenerator = mock[IDGenerator].smart - - /** - * The settings. - */ - lazy val settings = CsrfStateSettings( - cookieName = "OAuth2CsrfState", - cookiePath = "/", - cookieDomain = None, - secureCookie = true, - httpOnlyCookie = true, - expirationTime = 5 minutes - ) - - /** - * The cookie signer implementation. - * - * The cookie signer returns the same value as passed to the methods. This is enough for testing. - */ - lazy val cookieSigner = { - val c = mock[CookieSigner].smart - c.sign(any) answers { p => p.asInstanceOf[String] } - c.extract(any) answers { p => Success(p.asInstanceOf[String]) } - c - } - - /** - * An example usage of UserState where state is of type Map[String, String] - * @param state - */ - case class UserState(state: Map[String, String]) extends SocialStateItem - - /** - * Format to serialize the UserState - */ - implicit val userStateFormat: Format[UserState] = Json.format[UserState] - - /** - * An instance of UserState - */ - val userState = UserState(Map("path" -> "/login")) - - /** - * Serialized type of UserState - */ - val itemStructure = ItemStructure("user-state", Json.toJson(userState)) - - /** - * Csrf State value - */ - val csrfToken = "csrfToken" - - /** - * An instance of CsrfState - */ - val csrfState = CsrfState(csrfToken) - - /** - * An instance of Csrf State Handler - */ - val csrfStateHandler = new CsrfStateItemHandler(settings, idGenerator, cookieSigner) - - /** - * An instance of User State Handler - */ - val userStateHandler = new UserStateItemHandler(userState) - - /** - * The default state provider with User and Csrf State Handlers to test - */ - lazy val stateHandler = new DefaultSocialStateHandler(Set(csrfStateHandler, userStateHandler), cookieSigner) - - /** - * The default state provider without User State Handler to test - */ - lazy val stateHandlerWithoutUserState = new DefaultSocialStateHandler(Set(csrfStateHandler), cookieSigner) - } -} diff --git a/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/UserStateItemHandlerSpec.scala b/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/UserStateItemHandlerSpec.scala index e26a49261..ed493018e 100644 --- a/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/UserStateItemHandlerSpec.scala +++ b/silhouette/test/com/mohiva/play/silhouette/impl/providers/state/UserStateItemHandlerSpec.scala @@ -17,12 +17,14 @@ package com.mohiva.play.silhouette.impl.providers.state import com.mohiva.play.silhouette.impl.providers.SocialStateItem import com.mohiva.play.silhouette.impl.providers.SocialStateItem.ItemStructure +import com.mohiva.play.silhouette.impl.providers.state.UserStateItemHandler._ import org.specs2.matcher.JsonMatchers import org.specs2.mock.Mockito import org.specs2.specification.Scope -import play.api.libs.json.{ Format, Json } +import play.api.libs.json.Json import play.api.test.{ FakeRequest, PlaySpecification } -import play.api.libs.concurrent.Execution.Implicits._ + +import scala.concurrent.ExecutionContext.Implicits.global /** * Test case for the [[UserStateItemHandler]] class. @@ -30,82 +32,70 @@ import play.api.libs.concurrent.Execution.Implicits._ class UserStateItemHandlerSpec extends PlaySpecification with Mockito with JsonMatchers { "The `item` method" should { - "return userState" in new Context { - await(userStateHandler.item) must be(userState) + "return the user state item" in new Context { + await(userStateItemHandler.item) must be equalTo userStateItem } } "The `canHandle` method" should { - "return `Some[SocialStateItem]` if it can handle the given `SocialStateItem`" in new Context { - userStateHandler.canHandle(userState) must beSome[SocialStateItem] + "return the same item if it can handle the given item" in new Context { + userStateItemHandler.canHandle(userStateItem) must beSome(userStateItem) } - "return `None` if it can't handle the given `SocialStateItem`" in new Context { - userStateHandler.canHandle(csrfState) must beNone + "should return `None` if it can't handle the given item" in new Context { + val nonUserState = mock[SocialStateItem].smart + + userStateItemHandler.canHandle(nonUserState) must beNone } } "The `canHandle` method" should { - "return true if it can handle the given `ItemStructure`" in new Context { + "return false if the give item is for another handler" in new Context { + val nonUserItemStructure = mock[ItemStructure].smart + nonUserItemStructure.id returns "non-user-item" + implicit val request = FakeRequest() - userStateHandler.canHandle(itemStructure) must beTrue + userStateItemHandler.canHandle(nonUserItemStructure) must beFalse } - "return false if it can't handle the given `ItemStructure`" in new Context { + "return true if it can handle the given `ItemStructure`" in new Context { implicit val request = FakeRequest() - userStateHandler.canHandle(itemStructure.copy(id = "non-user-state")) must beFalse + userStateItemHandler.canHandle(userItemStructure) must beTrue } } "The `serialize` method" should { - "serialize `UserState` to `ItemStructure`" in new Context { - userStateHandler.serialize(userState) must beAnInstanceOf[ItemStructure] + "return a serialized value of the state item" in new Context { + userStateItemHandler.serialize(userStateItem).asString must be equalTo userItemStructure.asString } } "The `unserialize` method" should { - "unserialize `ItemStructure` to `UserState`" in new Context { + "unserialize the state item" in new Context { implicit val request = FakeRequest() - await(userStateHandler.unserialize(itemStructure)) must beAnInstanceOf[UserState] + + await(userStateItemHandler.unserialize(userItemStructure)) must be equalTo userStateItem } } + /** + * The context. + */ trait Context extends Scope { /** - * An example usage of UserState where state is of type Map[String, String] - * @param state - */ - case class UserState(state: Map[String, String]) extends SocialStateItem - - /** - * Format to serialize the UserState - */ - implicit val userStateFormat: Format[UserState] = Json.format[UserState] - - /** - * An instance of UserState - */ - val userState = UserState(Map("path" -> "/login")) - - /** - * Serialized type of UserState - */ - val itemStructure = ItemStructure("user-state", Json.toJson(userState)) - - /** - * Csrf State value + * A user state item. */ - val csrfToken = "csrfToken" + val userStateItem = UserStateItem(Map("path" -> "/login")) /** - * An instance of CsrfState + * The serialized type of the user state item. */ - val csrfState = CsrfState(csrfToken) + val userItemStructure = ItemStructure(ID, Json.toJson(userStateItem)) /** - * An instance of User State Handler + * An instance of the user state item handler. */ - val userStateHandler = new UserStateItemHandler(userState) + val userStateItemHandler = new UserStateItemHandler[UserStateItem](userStateItem) } }