import play.api.test.Helpers._ import play.api.test.{FakeRequest,WithApplication} import play.api.mvc.{Results,Headers} import play.api.libs.json.{JsObject,Json} import org.scalatest.BeforeAndAfterAll import org.scalatest.tagobjects.Slow import org.scalatestplus.play._ import org.scalatestplus.play.guice.GuiceOneServerPerSuite import com.weEat.shared.models.{User,UserAuthorization} import scala.concurrent.duration._ import java.util.Base64 class OAuthSpec extends PlaySpec with BeforeAndAfterAll with Results with GuiceOneServerPerSuite { val email = "tuser@sample.org" val password = "password" val fname = "test" val lname = "user" val users = Seq( (User("test", "user", "tuser@sample.org"), "password"), (User("another", "user", "usert@sample.org"), "password") ) implicit class CSRFWrapper[T](requ: FakeRequest[T]) { def withCSRFToken() = requ.withHeaders( requ.headers.add(("Csrf-Token", "nocheck")) ) } def makeAuthHeader(username: String, password: String) = Headers(("Authorization" -> s"Basic ${new String(Base64.getEncoder.encode(s"$username:$password".getBytes))}")) def makeAuthHeader(auth: UserAuthorization) = Headers(("Authorization" -> s"${auth.tokenType} ${auth.accessToken}")) override def beforeAll() = { for ((User(fname, lname, email), password) <- users) { val Some(resp) = route(app, FakeRequest(PUT, "/user/").withJsonBody( Json.parse(s"""|{ | "fname": "$fname", | "lname": "$lname", | "email": "$email", | "password": "$password" |}""".stripMargin) ).withCSRFToken()) status(resp) mustEqual OK } } "the token endpoint" should { "reject an empty request" in { val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ) status(resp) mustEqual BAD_REQUEST headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_request\", "+ "error_description=\"required parameter: grant_type\"") ) } "reject an improperly formatted request" in { val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withBody("""{"grant_type" "password"}""") .withHeaders( makeAuthHeader(email, password).add(("Content-Type", "application/json")) ).withCSRFToken() ) status(resp) mustEqual BAD_REQUEST // Won't get an OAuth response header. Will fail before processing. } "reject a missing grant_type" in { val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("{}")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ) status(resp) mustEqual BAD_REQUEST headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_request\", "+ "error_description=\"required parameter: grant_type\"") ) } } "the token endpoint for password grants" should { "accept a valid login" in { val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ) status(resp) mustEqual OK contentAsJson(resp).as[UserAuthorization] must matchPattern { case UserAuthorization(_, _, _, _) => } } "issue unique access tokens for successive logins" in { val requ = FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() val Some(resp1) = route(app, requ) val Some(resp2) = route(app, requ) status(resp2) mustEqual OK val resp1Auth = contentAsJson(resp1).as[UserAuthorization] val resp2Auth = contentAsJson(resp2).as[UserAuthorization] resp1Auth must matchPattern { case UserAuthorization(_, _, _, _) => } resp2Auth must matchPattern { case UserAuthorization(_, _, _, _) => } resp1Auth.accessToken mustNot equal(resp2Auth.accessToken) resp1Auth.refreshToken mustNot equal(resp2Auth.refreshToken) } "reject an unregistered user" in { val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader( "_"+users(0)._1.email, "_"+users(0)._2 )) .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_client\", "+ "error_description=\"Invalid client or client is not authorized\"")) } "reject an incorrect password" in { val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, "_"+users(0)._2)) .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_client\", "+ "error_description=\"Invalid client or client is not authorized\"") ) } "reject an incorrect username" in { val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader("_"+users(0)._1.email, users(0)._2)) .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_client\", "+ "error_description=\"Invalid client or client is not authorized\"") ) } "reject an requests without Authorization headers" in { val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse(s"""{"grant_type": "password"}""") ).withCSRFToken()) status(resp) mustEqual BAD_REQUEST headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_request\", "+ "error_description=\"Client credential is not found\"") ) } } "the token endpoint for refresh grants" should { "accept a given refresh token" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withCSRFToken() ) status(resp) mustEqual OK val newAuth = contentAsJson(resp).as[UserAuthorization] newAuth must matchPattern { case UserAuthorization(_, _, _, _) => } newAuth.accessToken mustNot equal(auth.accessToken) newAuth.refreshToken mustNot equal(auth.refreshToken) } "reject a given refresh token on the second attempt" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp1) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withCSRFToken() ) status(resp1) mustEqual OK contentAsJson(resp1).as[UserAuthorization] must matchPattern { case UserAuthorization(_, _, _, _) => } val Some(resp2) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withCSRFToken() ) status(resp2) mustEqual UNAUTHORIZED headers(resp2) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_client\", "+ "error_description=\"Invalid client or client is not authorized\"") ) } "reject a revoked refresh token" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp1) = route(app, FakeRequest(DELETE, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withHeaders(makeAuthHeader(auth)) .withCSRFToken() ) status(resp1) mustEqual OK val Some(resp2) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withCSRFToken() ) status(resp2) mustEqual UNAUTHORIZED headers(resp2) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_client\", "+ "error_description=\"Invalid client or client is not authorized\"") ) } "reject a given refresh token with an incorrect username" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "_${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_client\", "+ "error_description=\"Invalid client or client is not authorized\"") ) } "reject an unprovided refresh token" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val fakeToken = (auth.refreshToken.charAt(0)+1) + auth.refreshToken.substring(1); val Some(resp) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "$fakeToken" |}""".stripMargin)) .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_client\", "+ "error_description=\"Invalid client or client is not authorized\"") ) } } "the secured endpoint" should { "accept an authorized request" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(email, password)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp) = route(app, FakeRequest(GET, "/user/self/name/") .withHeaders(makeAuthHeader(auth)) .withCSRFToken() ) status(resp) mustEqual OK contentAsString(resp) must equal(s"${users(0)._1.fname} ${users(0)._1.lname}") } "reject an unauthorized request" in { val Some(resp) = route(app, FakeRequest(GET, "/user/self/name/") .withCSRFToken() ) status(resp) mustEqual BAD_REQUEST headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_request\", "+ "error_description=\"Access token is not found\"") ) } "reject an forged authorized request" in { val fakeToken = "7pKNy790TV5lKVjQw3k/pwJmMS8XBhHaLTVaI6ftd5M=" val Some(resp) = route(app, FakeRequest(GET, "/user/self/name/") .withHeaders(("Authorization" -> s"Bearer $fakeToken")) .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_token\", "+ "error_description=\"The access token is not found\"") ) } "reject a refreshed access token" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp1) = route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withCSRFToken() ) status(resp1) mustEqual OK val Some(resp2) = route(app, FakeRequest(GET, "/user/self/name/") .withHeaders(makeAuthHeader(auth)) .withCSRFToken() ) status(resp2) mustEqual UNAUTHORIZED headers(resp2) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_token\", "+ "error_description=\"The access token is invalid\"") ) } "reject a revoked access token" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp1) = route(app, FakeRequest(DELETE, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withHeaders(makeAuthHeader(auth)) .withCSRFToken() ) status(resp1) mustEqual OK val Some(resp2) = route(app, FakeRequest(GET, "/user/self/name/") .withHeaders(makeAuthHeader(auth)) .withCSRFToken() ) status(resp2) mustEqual UNAUTHORIZED headers(resp2) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_token\", "+ "error_description=\"The access token is invalid\"") ) } /* "reject an expired access token" taggedAs(Slow) in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(email, password)) ).get).as[UserAuthorization] Thread.sleep((1 hour).toMillis) val Some(resp) = route(app, FakeRequest(GET, "/user/self/name/") .withHeaders(makeAuthHeader(auth))) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_token\", "+ "error_description=\"The access token is invalid\"") ) } */ } "the token endpoint for revokation requests" should { "accept a valid request" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp) = route(app, FakeRequest(DELETE, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withHeaders(makeAuthHeader(auth)) .withCSRFToken() ) status(resp) mustEqual OK contentAsString(resp) mustEqual("") } "reject a request with an invalid authorization header" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val fakeToken = (auth.accessToken.charAt(0)+1) + auth.accessToken.substring(1); val Some(resp) = route(app, FakeRequest(DELETE, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withHeaders("Authorization" -> s"Bearer $fakeToken") .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_token\", "+ "error_description=\"The access token is not found\"") ) } "reject a request without an authorization header" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp) = route(app, FakeRequest(DELETE, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withCSRFToken() ) status(resp) mustEqual BAD_REQUEST headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_request\", "+ "error_description=\"Access token is not found\"") ) } "reject a request with an unissued refresh token" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val fakeToken = (auth.refreshToken.charAt(0)+1) + auth.refreshToken.substring(1); val Some(resp) = route(app, FakeRequest(DELETE, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "${users(0)._1.email}", | "refresh_token": "${fakeToken}" |}""".stripMargin)) .withHeaders(makeAuthHeader(auth)) .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_token\", "+ "error_description=\"The access token is invalid\"") ) } "reject a request with an incorrect username" in { val auth = contentAsJson(route(app, FakeRequest(POST, "/authorize/") .withJsonBody(Json.parse("""{"grant_type": "password"}""")) .withHeaders(makeAuthHeader(users(0)._1.email, users(0)._2)) .withCSRFToken() ).get).as[UserAuthorization] val Some(resp) = route(app, FakeRequest(DELETE, "/authorize/") .withJsonBody(Json.parse(s"""|{ | "grant_type": "refresh_token", | "client_id": "_${users(0)._1.email}", | "refresh_token": "${auth.refreshToken}" |}""".stripMargin)) .withHeaders(makeAuthHeader(auth)) .withCSRFToken() ) status(resp) mustEqual UNAUTHORIZED headers(resp) must contain ("WWW-Authenticate" -> ("Bearer error=\"invalid_client\", "+ "error_description=\"Invalid client or client is not authorized\"") ) } } }