├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── scalaoauth2 │ └── provider │ ├── AuthInfoRequest.scala │ ├── AuthorizedActionFunction.scala │ ├── OAuth2Provider.scala │ └── OAuth2ProviderActionBuilders.scala └── test └── scala └── scalaoauth2 └── provider ├── MockDataHandler.scala ├── OAuth2ProviderActionBuildersSpec.scala └── OAuth2ProviderSpec.scala /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | include: 11 | - os: ubuntu-latest 12 | java: 11 13 | - os: ubuntu-latest 14 | java: 17 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Setup 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: adopt 23 | java-version: ${{ matrix.java }} 24 | - name: Coursier cache 25 | uses: coursier/cache-action@v6 26 | - name: Build and test 27 | run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck +test 28 | - run: rm -rf "$HOME/.ivy2/local" || true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bsp/ 2 | .idea/ 3 | .idea_modules/ 4 | target/ 5 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.7.14 2 | project.git = true 3 | runner.dialect = scala3 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Nulab Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # play2-oauth2-provider [![CI](https://github.com/nulab/play2-oauth2-provider/actions/workflows/ci.yml/badge.svg)](https://github.com/nulab/play2-oauth2-provider/actions/workflows/ci.yml) 2 | 3 | This library is enabled using [scala-oauth2-provider](https://github.com/nulab/scala-oauth2-provider) in Play Framework. 4 | 5 | ## Setup 6 | 7 | Add "play2-oauth2-provider" to library dependencies of your project. 8 | 9 | ```scala 10 | libraryDependencies ++= Seq( 11 | "com.nulab-inc" %% "scala-oauth2-core" % "1.6.0", 12 | "com.nulab-inc" %% "play2-oauth2-provider" % "2.0.0" 13 | ) 14 | ``` 15 | 16 | | Library version | Play version | 17 | | --------------- | ------------ | 18 | | 2.0.0 | 3.0.x | 19 | | 1.6.0 | 2.9.x | 20 | | 1.5.0 | 2.8.x | 21 | | 1.4.2 | 2.7.x | 22 | | 1.3.0 | 2.6.x | 23 | | 1.2.0 | 2.5.x | 24 | | 0.16.1 | 2.4.x | 25 | | 0.14.0 | 2.3.x | 26 | | 0.7.4 | 2.2.x | 27 | 28 | ## How to use 29 | 30 | You should follow four steps below to work with Play Framework. 31 | 32 | - Customizing Grant Handlers 33 | - Define a controller to issue access token 34 | - Assign a route to the controller 35 | - Access to an authorized resource 36 | 37 | You want to use which grant types are supported or to use a customized handler for a grant type, you should override the `handlers` map in a customized `TokenEndpoint` trait. 38 | 39 | ```scala 40 | class MyTokenEndpoint extends TokenEndpoint { 41 | override val handlers = Map( 42 | OAuthGrantType.AUTHORIZATION_CODE -> new AuthorizationCode(), 43 | OAuthGrantType.REFRESH_TOKEN -> new RefreshToken(), 44 | OAuthGrantType.CLIENT_CREDENTIALS -> new ClientCredentials(), 45 | OAuthGrantType.PASSWORD -> new Password(), 46 | OAuthGrantType.IMPLICIT -> new Implicit() 47 | ) 48 | } 49 | ``` 50 | 51 | Here's an example of a customized `TokenEndpoint` that 1) only supports the `password` grant type, and 2) customizes the `password` grant type handler to not require client credentials: 52 | 53 | ```scala 54 | class MyTokenEndpoint extends TokenEndpoint { 55 | val passwordNoCred = new Password() { 56 | override def clientCredentialRequired = false 57 | } 58 | 59 | override val handlers = Map( 60 | OAuthGrantType.PASSWORD -> passwordNoCred 61 | ) 62 | } 63 | ``` 64 | 65 | Define your own controller with mixining `OAuth2Provider` trait provided by this library to issue access token with customized `TokenEndpoint`. 66 | 67 | ```scala 68 | class MyController @Inject() (components: ControllerComponents) 69 | extends AbstractController(components) with OAuth2Provider { 70 | override val tokenEndpoint = new MyTokenEndpoint() 71 | 72 | def accessToken = Action.async { implicit request => 73 | issueAccessToken(new MyDataHandler()) 74 | } 75 | } 76 | ``` 77 | 78 | Then, assign a route to the controller that OAuth clients will access to. 79 | 80 | ``` 81 | POST /oauth2/access_token controllers.OAuth2Controller.accessToken 82 | ``` 83 | 84 | Finally, you can access to an authorized resource like this: 85 | 86 | ```scala 87 | class MyController @Inject() (components: ControllerComponents) 88 | extends AbstractController(components) with OAuth2Provider { 89 | 90 | val action = Action.async { request => 91 | authorize(new MockDataHandler()) { authInfo => 92 | val user = authInfo.user // User is defined on your system 93 | // access resource for the user 94 | ??? 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | If you'd like to change the OAuth workflow, modify handleRequest methods of `TokenEndPoint` and `ProtectedResource` traits. 101 | 102 | ### Using Action composition 103 | 104 | You can write more easily authorize action by using Action composition. 105 | 106 | Play Framework's documentation is [here](https://www.playframework.com/documentation/2.7.x/ScalaActionsComposition). 107 | 108 | ```scala 109 | class MyController @Inject() (components: ControllerComponents) 110 | extends AbstractController(components) with OAuth2ProviderActionBuilders { 111 | 112 | def list = AuthorizedAction(new MyDataHandler()) { request => 113 | val user = request.authInfo.user // User is defined on your system 114 | // access resource for the user 115 | } 116 | } 117 | ``` 118 | 119 | ## Examples 120 | 121 | ### Play Framework 2.5 122 | 123 | - https://github.com/lglossman/scala-oauth2-deadbolt-redis 124 | - https://github.com/tsuyoshizawa/scala-oauth2-provider-example-skinny-orm 125 | 126 | ### Play Framework 2.3 127 | 128 | - https://github.com/davidseth/scala-oauth2-provider-slick 129 | 130 | ### Play Framework 2.2 131 | 132 | - https://github.com/oyediyildiz/scala-oauth2-provider-example 133 | - https://github.com/tuxdna/play-oauth2-server 134 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | val playVersion = "3.0.0" 2 | val commonDependenciesInTestScope = Seq( 3 | "org.scalatest" %% "scalatest" % "3.2.17" % "test", 4 | "ch.qos.logback" % "logback-classic" % "1.4.11" % "test" 5 | ) 6 | 7 | def unusedWarnings(scalaVersion: String) = 8 | Seq("-Wunused:imports") 9 | 10 | lazy val scalaOAuth2ProviderSettings = 11 | Defaults.coreDefaultSettings ++ 12 | Seq( 13 | organization := "com.nulab-inc", 14 | scalaVersion := "3.3.1", 15 | crossScalaVersions ++= Seq("2.13.12"), 16 | scalacOptions ++= Seq("-deprecation", "-unchecked", "-feature"), 17 | scalacOptions ++= unusedWarnings(scalaVersion.value), 18 | publishTo := { 19 | val v = version.value 20 | val nexus = "https://oss.sonatype.org/" 21 | if (v.trim.endsWith("SNAPSHOT")) 22 | Some("snapshots" at nexus + "content/repositories/snapshots") 23 | else Some("releases" at nexus + "service/local/staging/deploy/maven2") 24 | }, 25 | publishMavenStyle := true, 26 | Test / publishArtifact := false, 27 | pomIncludeRepository := { _ => 28 | false 29 | }, 30 | pomExtra := https://github.com/nulab/play2-oauth2-provider 31 | 32 | 33 | MIT License 34 | http://www.opensource.org/licenses/mit-license.php 35 | repo 36 | 37 | 38 | 39 | https://github.com/nulab/play2-oauth2-provider 40 | scm:git:git@github.com:nulab/play2-oauth2-provider.git 41 | 42 | 43 | 44 | tsuyoshizawa 45 | Tsuyoshi Yoshizawa 46 | https://github.com/tsuyoshizawa 47 | 48 | 49 | ) ++ Seq(Compile, Test).flatMap(c => 50 | c / console / scalacOptions --= unusedWarnings(scalaVersion.value) 51 | ) 52 | 53 | lazy val root = (project in file(".")) 54 | .settings( 55 | scalaOAuth2ProviderSettings, 56 | name := "play2-oauth2-provider", 57 | description := "Support scala-oauth2-core library on Play Framework Scala", 58 | version := "2.0.0", 59 | libraryDependencies ++= Seq( 60 | "com.nulab-inc" %% "scala-oauth2-core" % "1.6.0" % "provided", 61 | "org.playframework" %% "play" % playVersion % "provided", 62 | "org.playframework" %% "play-test" % playVersion % "test" 63 | ) ++ commonDependenciesInTestScope 64 | ) 65 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.6 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") 2 | -------------------------------------------------------------------------------- /src/main/scala/scalaoauth2/provider/AuthInfoRequest.scala: -------------------------------------------------------------------------------- 1 | package scalaoauth2.provider 2 | 3 | import play.api.mvc.{Request, WrappedRequest} 4 | 5 | case class AuthInfoRequest[A, U]( 6 | authInfo: AuthInfo[U], 7 | private val request: Request[A] 8 | ) extends WrappedRequest[A](request) 9 | -------------------------------------------------------------------------------- /src/main/scala/scalaoauth2/provider/AuthorizedActionFunction.scala: -------------------------------------------------------------------------------- 1 | package scalaoauth2.provider 2 | 3 | import play.api.mvc._ 4 | 5 | import scala.concurrent.{Future, ExecutionContext} 6 | 7 | case class AuthorizedActionFunction[U](handler: ProtectedResourceHandler[U])( 8 | implicit ctx: ExecutionContext 9 | ) extends ActionFunction[Request, ({ type L[A] = AuthInfoRequest[A, U] })#L] 10 | with OAuth2Provider { 11 | 12 | override protected def executionContext: ExecutionContext = ctx 13 | 14 | override def invokeBlock[A]( 15 | request: Request[A], 16 | block: AuthInfoRequest[A, U] => Future[Result] 17 | ): Future[Result] = { 18 | authorize(handler) { authInfo => 19 | block(AuthInfoRequest(authInfo, request)) 20 | }(request, ctx) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/scalaoauth2/provider/OAuth2Provider.scala: -------------------------------------------------------------------------------- 1 | package scalaoauth2.provider 2 | 3 | import play.api.libs.json._ 4 | import play.api.mvc._ 5 | 6 | import scala.concurrent.{ExecutionContext, Future} 7 | import scala.language.implicitConversions 8 | 9 | /** Basic OAuth2 provider trait. 10 | */ 11 | trait OAuth2BaseProvider extends Results { 12 | 13 | private[provider] def getParam[A]( 14 | request: Request[A] 15 | ): Map[String, Seq[String]] = { 16 | val unwrap = request.body match { 17 | case body: play.api.mvc.AnyContent => 18 | body.asFormUrlEncoded 19 | .orElse(body.asMultipartFormData) 20 | .orElse(body.asJson) 21 | .getOrElse(body) 22 | case body => body 23 | } 24 | ((unwrap match { 25 | case body: Map[_, _] => body.asInstanceOf[Map[String, Seq[String]]] 26 | case body: MultipartFormData[_] => body.asFormUrlEncoded 27 | case Right(body: MultipartFormData[_]) => body.asFormUrlEncoded 28 | case body: play.api.libs.json.JsValue => 29 | FormUtils.fromJson(js = body).view.mapValues(Seq(_)) 30 | case _ => Map.empty 31 | }) ++ request.queryString).toMap 32 | } 33 | 34 | private[provider] object FormUtils { 35 | 36 | import play.api.libs.json._ 37 | 38 | def fromJson(prefix: String = "", js: JsValue): Map[String, String] = 39 | js match { 40 | case JsObject(fields) => 41 | fields 42 | .map { case (key, value) => 43 | fromJson( 44 | Option(prefix) 45 | .filterNot(_.isEmpty) 46 | .map(_ + ".") 47 | .getOrElse("") + key, 48 | value 49 | ) 50 | } 51 | .foldLeft(Map.empty[String, String])(_ ++ _) 52 | case JsArray(values) => 53 | values.zipWithIndex 54 | .map { case (value, i) => fromJson(prefix + "[" + i + "]", value) } 55 | .foldLeft(Map.empty[String, String])(_ ++ _) 56 | case JsNull => Map.empty 57 | case JsUndefined() => Map.empty 58 | case JsBoolean(value) => Map(prefix -> value.toString) 59 | case JsNumber(value) => Map(prefix -> value.toString) 60 | case JsString(value) => Map(prefix -> value.toString) 61 | } 62 | 63 | } 64 | 65 | protected[scalaoauth2] def responseOAuthErrorHeader( 66 | e: OAuthError 67 | ): (String, String) = 68 | "WWW-Authenticate" -> ("Bearer " + toOAuthErrorString(e)) 69 | 70 | protected def toOAuthErrorString(e: OAuthError): String = { 71 | val params = Seq("error=\"" + e.errorType + "\"") ++ 72 | (if (e.description.nonEmpty) { 73 | Seq("error_description=\"" + e.description + "\"") 74 | } else { 75 | Nil 76 | }) 77 | params.mkString(", ") 78 | } 79 | 80 | } 81 | 82 | trait OAuth2ProtectedResourceProvider extends OAuth2BaseProvider { 83 | 84 | val protectedResource: ProtectedResource = ProtectedResource 85 | 86 | implicit def play2protectedResourceRequest( 87 | request: RequestHeader 88 | ): ProtectedResourceRequest = { 89 | new ProtectedResourceRequest(request.headers.toMap, request.queryString) 90 | } 91 | 92 | implicit def play2protectedResourceRequest[A]( 93 | request: Request[A] 94 | ): ProtectedResourceRequest = { 95 | val param: Map[String, Seq[String]] = getParam(request) 96 | new ProtectedResourceRequest(request.headers.toMap, param) 97 | } 98 | 99 | /** Authorize to already created access token in ProtectedResourceHandler 100 | * process and return the response to client. 101 | * 102 | * @param handler 103 | * Implemented ProtectedResourceHandler for authenticate to your system. 104 | * @param callback 105 | * Callback is called when authentication is successful. 106 | * @param request 107 | * Play Framework is provided HTTP request interface. 108 | * @param ctx 109 | * This contxt is used by ProtectedResource. 110 | * @tparam A 111 | * play.api.mvc.Request has type. 112 | * @tparam U 113 | * set the type in AuthorizationHandler. 114 | * @return 115 | * Authentication is successful then the response use your API result. 116 | * Authentication is failed then return BadRequest or Unauthorized status 117 | * to client with cause into the JSON. 118 | */ 119 | def authorize[A, U](handler: ProtectedResourceHandler[U])( 120 | callback: AuthInfo[U] => Future[Result] 121 | )(implicit request: Request[A], ctx: ExecutionContext): Future[Result] = { 122 | protectedResource.handleRequest(request, handler).flatMap { 123 | case Left(e) => 124 | Future.successful( 125 | new Status(e.statusCode).withHeaders(responseOAuthErrorHeader(e)) 126 | ) 127 | case Right(authInfo) => callback(authInfo) 128 | } 129 | } 130 | 131 | } 132 | 133 | trait OAuth2TokenEndpointProvider extends OAuth2BaseProvider { 134 | 135 | val tokenEndpoint: TokenEndpoint = TokenEndpoint 136 | 137 | implicit def play2oauthRequest( 138 | request: RequestHeader 139 | ): AuthorizationRequest = { 140 | new AuthorizationRequest(request.headers.toMap, request.queryString) 141 | } 142 | 143 | implicit def play2oauthRequest[A]( 144 | request: Request[A] 145 | ): AuthorizationRequest = { 146 | val param: Map[String, Seq[String]] = getParam(request) 147 | new AuthorizationRequest(request.headers.toMap, param) 148 | } 149 | 150 | /** Issue access token in AuthorizationHandler process and return the response 151 | * to client. 152 | * 153 | * @param handler 154 | * Implemented AuthorizationHandler for register access token to your 155 | * system. 156 | * @param request 157 | * Play Framework is provided HTTP request interface. 158 | * @param ctx 159 | * This context is used by TokenEndPoint. 160 | * @tparam A 161 | * play.api.mvc.Request has type. 162 | * @tparam U 163 | * set the type in AuthorizationHandler. 164 | * @return 165 | * Request is successful then return JSON to client in OAuth 2.0 format. 166 | * Request is failed then return BadRequest or Unauthorized status to 167 | * client with cause into the JSON. 168 | */ 169 | def issueAccessToken[A, U]( 170 | handler: AuthorizationHandler[U] 171 | )(implicit request: Request[A], ctx: ExecutionContext): Future[Result] = { 172 | tokenEndpoint.handleRequest(request, handler).map { 173 | case Left(e) => 174 | new Status(e.statusCode)(responseOAuthErrorJson(e)) 175 | .withHeaders(responseOAuthErrorHeader(e)) 176 | case Right(r) => 177 | Ok(Json.toJson(responseAccessToken(r))) 178 | .withHeaders("Cache-Control" -> "no-store", "Pragma" -> "no-cache") 179 | } 180 | } 181 | 182 | protected[scalaoauth2] def responseOAuthErrorJson(e: OAuthError): JsValue = 183 | Json.obj("error" -> e.errorType, "error_description" -> e.description) 184 | 185 | protected[scalaoauth2] def responseAccessToken[U]( 186 | r: GrantHandlerResult[U] 187 | ) = { 188 | Map[String, JsValue]( 189 | "token_type" -> JsString(r.tokenType), 190 | "access_token" -> JsString(r.accessToken) 191 | ) ++ r.expiresIn.map { 192 | "expires_in" -> JsNumber(_) 193 | } ++ r.refreshToken.map { 194 | "refresh_token" -> JsString(_) 195 | } ++ r.scope.map { 196 | "scope" -> JsString(_) 197 | } ++ r.params.map(e => (e._1, JsString(e._2))) 198 | } 199 | 200 | } 201 | 202 | /** OAuth2Provider supports issue access token and authorize. 203 | * 204 | *

Create controller for issue access token

205 | * @example 206 | * {{{ object OAuth2Controller extends Controller with OAuth2Provider { def 207 | * accessToken = Action.async { implicit request => issueAccessToken(new 208 | * MyDataHandler()) } } }}} 209 | * 210 | *

Register routes

211 | * @example 212 | * {{{POST /oauth2/access_token controllers.OAuth2Controller.accessToken}}} 213 | * 214 | *

Authorized

215 | * @example 216 | * {{{ import scalaoauth2.provider._ object BookController extends Controller 217 | * with OAuth2Provider { def list = Action.async { implicit request => 218 | * authorize(new MyDataHandler()) { authInfo => val user = authInfo.user // 219 | * User is defined on your system // access resource for the user } } } }}} 220 | */ 221 | trait OAuth2Provider 222 | extends OAuth2ProtectedResourceProvider 223 | with OAuth2TokenEndpointProvider 224 | -------------------------------------------------------------------------------- /src/main/scala/scalaoauth2/provider/OAuth2ProviderActionBuilders.scala: -------------------------------------------------------------------------------- 1 | package scalaoauth2.provider 2 | 3 | import play.api.mvc.{ActionBuilder, AnyContent, BaseController} 4 | 5 | trait OAuth2ProviderActionBuilders { 6 | 7 | self: BaseController => 8 | 9 | def AuthorizedAction[U]( 10 | handler: ProtectedResourceHandler[U] 11 | ): ActionBuilder[({ type L[A] = AuthInfoRequest[A, U] })#L, AnyContent] = { 12 | AuthorizedActionFunction(handler)( 13 | self.defaultExecutionContext 14 | ) compose Action 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/scala/scalaoauth2/provider/MockDataHandler.scala: -------------------------------------------------------------------------------- 1 | package scalaoauth2.provider 2 | 3 | import java.util.Date 4 | 5 | import scala.concurrent.Future 6 | 7 | class MockDataHandler extends DataHandler[User] { 8 | 9 | override def validateClient( 10 | maybeClientCredential: Option[ClientCredential], 11 | request: AuthorizationRequest 12 | ): Future[Boolean] = Future.successful(false) 13 | 14 | override def findUser( 15 | maybeClientCredential: Option[ClientCredential], 16 | request: AuthorizationRequest 17 | ): Future[Option[User]] = Future.successful(None) 18 | 19 | override def createAccessToken( 20 | authInfo: AuthInfo[User] 21 | ): Future[AccessToken] = 22 | Future.successful(AccessToken("", Some(""), Some(""), Some(0L), new Date())) 23 | 24 | override def findAuthInfoByCode( 25 | code: String 26 | ): Future[Option[AuthInfo[User]]] = Future.successful(None) 27 | 28 | override def findAuthInfoByRefreshToken( 29 | refreshToken: String 30 | ): Future[Option[AuthInfo[User]]] = Future.successful(None) 31 | 32 | override def findAccessToken(token: String): Future[Option[AccessToken]] = 33 | Future.successful(None) 34 | 35 | override def findAuthInfoByAccessToken( 36 | accessToken: AccessToken 37 | ): Future[Option[AuthInfo[User]]] = Future.successful(None) 38 | 39 | override def getStoredAccessToken( 40 | authInfo: AuthInfo[User] 41 | ): Future[Option[AccessToken]] = Future.successful(None) 42 | 43 | override def refreshAccessToken( 44 | authInfo: AuthInfo[User], 45 | refreshToken: String 46 | ): Future[AccessToken] = 47 | Future.successful(AccessToken("", Some(""), Some(""), Some(0L), new Date())) 48 | 49 | override def deleteAuthCode(code: String): Future[Unit] = 50 | Future.successful(()) 51 | } 52 | 53 | trait User { 54 | def id: Long 55 | def name: String 56 | } 57 | 58 | case class MockUser(id: Long, name: String) extends User 59 | -------------------------------------------------------------------------------- /src/test/scala/scalaoauth2/provider/OAuth2ProviderActionBuildersSpec.scala: -------------------------------------------------------------------------------- 1 | package scalaoauth2.provider 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers._ 5 | import play.api.mvc.{AbstractController, ControllerComponents} 6 | import play.api.test.Helpers._ 7 | import play.api.test.{FakeRequest, _} 8 | import scala.concurrent.Future 9 | 10 | import javax.inject.Inject 11 | 12 | class OAuth2ProviderActionBuildersSpec extends AnyFlatSpec { 13 | 14 | class MyController @Inject() (components: ControllerComponents) 15 | extends AbstractController(components) 16 | with OAuth2ProviderActionBuilders { 17 | 18 | val action = AuthorizedAction(new MockDataHandler).async { request => 19 | Future.successful(Ok(request.authInfo.user.name)) 20 | } 21 | 22 | } 23 | 24 | it should "return BadRequest" in { 25 | val controller = new MyController(Helpers.stubControllerComponents()) 26 | val result = controller.action(FakeRequest()) 27 | status(result) should be(400) 28 | contentAsString(result) should be("") 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/scala/scalaoauth2/provider/OAuth2ProviderSpec.scala: -------------------------------------------------------------------------------- 1 | package scalaoauth2.provider 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers._ 5 | import play.api.libs.json._ 6 | import play.api.mvc.{AnyContentAsFormUrlEncoded, AnyContentAsJson} 7 | import play.api.test.{FakeHeaders, FakeRequest} 8 | 9 | class OAuth2ProviderSpec extends AnyFlatSpec { 10 | 11 | case class User(id: Long, name: String) 12 | 13 | object TestOAuthProvider extends OAuth2Provider { 14 | override def responseAccessToken[U](r: GrantHandlerResult[U]) = 15 | super.responseAccessToken(r) ++ Map( 16 | "custom_key" -> JsString("custom_value") 17 | ) 18 | } 19 | 20 | it should "return including access token" in { 21 | val map = TestOAuthProvider.responseAccessToken( 22 | GrantHandlerResult( 23 | authInfo = AuthInfo[User]( 24 | user = User(0L, "name"), 25 | Some("client_id"), 26 | None, 27 | None 28 | ), 29 | tokenType = "Bearer", 30 | accessToken = "access_token", 31 | expiresIn = Some(3600), 32 | refreshToken = None, 33 | scope = None, 34 | params = Map.empty 35 | ) 36 | ) 37 | map.get("token_type") should contain(JsString("Bearer")) 38 | map.get("access_token") should contain(JsString("access_token")) 39 | map.get("expires_in") should contain(JsNumber(3600)) 40 | map.get("refresh_token") should be(None) 41 | map.get("scope") should be(None) 42 | map.get("custom_key") should contain(JsString("custom_value")) 43 | } 44 | 45 | it should "return error message as JSON" in { 46 | val json = TestOAuthProvider.responseOAuthErrorJson( 47 | new InvalidRequest("request is invalid") 48 | ) 49 | (json \ "error").as[String] should be("invalid_request") 50 | (json \ "error_description").as[String] should be("request is invalid") 51 | } 52 | 53 | it should "return error message to header" in { 54 | val (name, value) = TestOAuthProvider.responseOAuthErrorHeader( 55 | new InvalidRequest("request is invalid") 56 | ) 57 | name should be("WWW-Authenticate") 58 | value should be( 59 | """Bearer error="invalid_request", error_description="request is invalid"""" 60 | ) 61 | } 62 | 63 | it should "get parameters from form url encoded body" in { 64 | val values = Map("id" -> List("1000"), "language" -> List("Scala")) 65 | val request = FakeRequest( 66 | method = "GET", 67 | uri = "/", 68 | headers = FakeHeaders(), 69 | body = AnyContentAsFormUrlEncoded(values) 70 | ) 71 | val params = TestOAuthProvider.getParam(request) 72 | params.get("id") should contain(List("1000")) 73 | params.get("language") should contain(List("Scala")) 74 | } 75 | 76 | it should "get parameters from query string" in { 77 | val values = Map("id" -> List("1000"), "language" -> List("Scala")) 78 | val request = FakeRequest( 79 | method = "GET", 80 | uri = "/?version=2.11", 81 | headers = FakeHeaders(), 82 | body = AnyContentAsFormUrlEncoded(values) 83 | ) 84 | val params = TestOAuthProvider.getParam(request) 85 | params.get("id") should contain(List("1000")) 86 | params.get("language") should contain(List("Scala")) 87 | params.get("version") should contain(List("2.11")) 88 | } 89 | 90 | it should "get parameters from JSON body" in { 91 | val json = Json.obj("id" -> 1000, "language" -> "Scala") 92 | val request = FakeRequest( 93 | method = "GET", 94 | uri = "/", 95 | headers = FakeHeaders(), 96 | body = AnyContentAsJson(json) 97 | ) 98 | val params = TestOAuthProvider.getParam(request) 99 | params.get("id") should contain(List("1000")) 100 | params.get("language") should contain(List("Scala")) 101 | } 102 | } 103 | --------------------------------------------------------------------------------