├── .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 [](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 |
--------------------------------------------------------------------------------