Skip to content

Commit 1848a48

Browse files
cdiniztsuyoshizawa
authored andcommitted
Add akka-http-oauth2-provider sub project. (#94)
* Add akka-http-oauth2-provider sub project. * Add tests for akka-http oauth2 provider route helper. Remove default tokenEndpoint from akka-http-provider. * rename OAuth2provider.scala
1 parent 34d7e6a commit 1848a48

File tree

4 files changed

+202
-2
lines changed

4 files changed

+202
-2
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package scalaoauth2.provider
2+
3+
import akka.http.scaladsl.model.StatusCodes._
4+
import akka.http.scaladsl.server.Directives
5+
import akka.http.scaladsl.server.directives.Credentials
6+
import scalaoauth2.provider.OAuth2Provider.TokenResponse
7+
import spray.json.DefaultJsonProtocol
8+
9+
import scala.concurrent.ExecutionContext.Implicits.global
10+
import scala.concurrent.Future
11+
import scala.util.{ Failure, Success }
12+
13+
trait OAuth2Provider[U] extends Directives with DefaultJsonProtocol {
14+
import OAuth2Provider.tokenResponseFormat
15+
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
16+
17+
val oauth2DataHandler: DataHandler[U]
18+
19+
val tokenEndpoint: TokenEndpoint
20+
21+
def grantResultToTokenResponse(grantResult: GrantHandlerResult[U]): TokenResponse =
22+
TokenResponse(grantResult.tokenType, grantResult.accessToken, grantResult.expiresIn.getOrElse(1L), grantResult.refreshToken.getOrElse(""))
23+
24+
def oauth2Authenticator(credentials: Credentials): Future[Option[AuthInfo[U]]] =
25+
credentials match {
26+
case Credentials.Provided(token) =>
27+
oauth2DataHandler.findAccessToken(token).flatMap {
28+
case Some(token) => oauth2DataHandler.findAuthInfoByAccessToken(token)
29+
case None => Future.successful(None)
30+
}
31+
case _ => Future.successful(None)
32+
}
33+
34+
def accessTokenRoute = pathPrefix("oauth") {
35+
path("access_token") {
36+
post {
37+
formFieldMap { fields =>
38+
onComplete(tokenEndpoint.handleRequest(new AuthorizationRequest(Map(), fields.map(m => m._1 -> Seq(m._2))), oauth2DataHandler)) {
39+
case Success(maybeGrantResponse) =>
40+
maybeGrantResponse.fold(
41+
oauthError => complete(Unauthorized),
42+
grantResult => complete(tokenResponseFormat.write(grantResultToTokenResponse(grantResult)))
43+
)
44+
case Failure(ex) => complete(InternalServerError, s"An error occurred: ${ex.getMessage}")
45+
}
46+
}
47+
}
48+
}
49+
}
50+
51+
}
52+
53+
object OAuth2Provider extends DefaultJsonProtocol {
54+
case class TokenResponse(token_type: String, access_token: String, expires_in: Long, refresh_token: String)
55+
implicit val tokenResponseFormat = jsonFormat4(TokenResponse)
56+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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(request: AuthorizationRequest): Future[Boolean] = Future.successful(false)
10+
11+
override def findUser(request: AuthorizationRequest): Future[Option[User]] = Future.successful(None)
12+
13+
override def createAccessToken(authInfo: AuthInfo[User]): Future[AccessToken] = Future.successful(AccessToken("", Some(""), Some(""), Some(0L), new Date()))
14+
15+
override def findAuthInfoByCode(code: String): Future[Option[AuthInfo[User]]] = Future.successful(None)
16+
17+
override def findAuthInfoByRefreshToken(refreshToken: String): Future[Option[AuthInfo[User]]] = Future.successful(None)
18+
19+
override def findAccessToken(token: String): Future[Option[AccessToken]] = Future.successful(None)
20+
21+
override def findAuthInfoByAccessToken(accessToken: AccessToken): Future[Option[AuthInfo[User]]] = Future.successful(None)
22+
23+
override def getStoredAccessToken(authInfo: AuthInfo[User]): Future[Option[AccessToken]] = Future.successful(None)
24+
25+
override def refreshAccessToken(authInfo: AuthInfo[User], refreshToken: String): Future[AccessToken] = Future.successful(AccessToken("", Some(""), Some(""), Some(0L), new Date()))
26+
27+
override def deleteAuthCode(code: String): Future[Unit] = Future.successful(Unit)
28+
}
29+
30+
trait User {
31+
def id: Long
32+
def name: String
33+
}
34+
35+
case class MockUser(id: Long, name: String) extends User
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package scalaoauth2.provider
2+
3+
import java.util.Date
4+
5+
import akka.http.scaladsl.model.headers.OAuth2BearerToken
6+
import akka.http.scaladsl.server.directives.Credentials
7+
import akka.http.scaladsl.testkit.ScalatestRouteTest
8+
import org.scalatest.concurrent.ScalaFutures
9+
import org.scalatest.{ Matchers, WordSpec }
10+
import akka.http.scaladsl.model.StatusCodes._
11+
import akka.http.scaladsl.model.FormData
12+
import scala.concurrent.Future
13+
14+
class OAuth2ProviderSpec extends WordSpec with Matchers with ScalatestRouteTest with ScalaFutures {
15+
16+
val tokenEndpointCredentials = new TokenEndpoint {
17+
override val handlers = Map(
18+
OAuthGrantType.CLIENT_CREDENTIALS -> new ClientCredentials
19+
)
20+
}
21+
22+
val oauth2ProviderFail = new OAuth2Provider[User] {
23+
override val oauth2DataHandler = new MockDataHandler()
24+
override val tokenEndpoint = tokenEndpointCredentials
25+
}
26+
27+
val user = MockUser(1, "user")
28+
val someAuthInfo = Some(AuthInfo(user, Some("clientId"), None, None))
29+
val accessToken = AccessToken("token", Some("refresh token"), None, Some(3600), new Date)
30+
31+
val oauth2ProviderSuccess = new OAuth2Provider[User] {
32+
override val tokenEndpoint = tokenEndpointCredentials
33+
override val oauth2DataHandler = new MockDataHandler() {
34+
override def findAccessToken(token: String): Future[Option[AccessToken]] =
35+
Future.successful(Some(accessToken))
36+
override def findAuthInfoByAccessToken(accessToken: AccessToken): Future[Option[AuthInfo[User]]] =
37+
Future.successful(someAuthInfo)
38+
override def findUser(request: AuthorizationRequest): Future[Option[User]] =
39+
Future.successful(Some(user))
40+
override def validateClient(request: AuthorizationRequest): Future[Boolean] =
41+
Future.successful(true)
42+
override def getStoredAccessToken(authInfo: AuthInfo[User]): Future[Option[AccessToken]] =
43+
Future.successful(Some(accessToken))
44+
override def createAccessToken(authInfo: AuthInfo[User]): Future[AccessToken] =
45+
Future.successful(accessToken)
46+
}
47+
}
48+
49+
"oauth2Authenticator" should {
50+
51+
"return none when data handler cannot find access token" in {
52+
val r = oauth2ProviderFail.oauth2Authenticator(Credentials(Some(OAuth2BearerToken("token"))))
53+
whenReady(r) { result => result should be(None) }
54+
}
55+
56+
"return none when there is not a bearer token in request" in {
57+
val r = oauth2ProviderSuccess.oauth2Authenticator(Credentials(None))
58+
whenReady(r) { result => result should be(None) }
59+
}
60+
61+
"return some authinfo when there is a token match" in {
62+
val r = oauth2ProviderSuccess.oauth2Authenticator(Credentials(Some(OAuth2BearerToken("token"))))
63+
whenReady(r) { result => result should be(someAuthInfo) }
64+
}
65+
66+
}
67+
68+
"access token route" should {
69+
70+
"return Unauthorized when there is an error on authorization" in {
71+
Post("/oauth/access_token", FormData(
72+
"client_id" -> "bob_client_id",
73+
"client_secret" -> "bob_client_secret", "grant_type" -> "client_credentials"
74+
)) ~> oauth2ProviderFail.accessTokenRoute ~> check {
75+
handled shouldEqual true
76+
status shouldEqual Unauthorized
77+
}
78+
}
79+
80+
"return Ok with token respons when there is a valid authorization" in {
81+
Post("/oauth/access_token", FormData(
82+
"client_id" -> "bob_client_id",
83+
"client_secret" -> "bob_client_secret", "grant_type" -> "client_credentials"
84+
)) ~> oauth2ProviderSuccess.accessTokenRoute ~> check {
85+
handled shouldEqual true
86+
status shouldEqual OK
87+
}
88+
}
89+
90+
}
91+
92+
}

project/Build.scala

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ object ScalaOAuth2Build extends Build {
77
lazy val _organization = "com.nulab-inc"
88
lazy val _version = "0.17.3-SNAPSHOT"
99
lazy val _playVersion = "2.5.0"
10-
10+
lazy val akkaV = "2.4.2"
1111
val _scalaVersion = "2.11.7"
1212
val _crossScalaVersions = Seq("2.11.7")
1313

@@ -38,7 +38,7 @@ object ScalaOAuth2Build extends Build {
3838
name := "scala-oauth2-provider",
3939
description := "OAuth 2.0 server-side implementation written in Scala"
4040
)
41-
).aggregate(scalaOAuth2Core, play2OAuth2Provider)
41+
).aggregate(scalaOAuth2Core, play2OAuth2Provider, akkahttpOAuth2Provider)
4242

4343
lazy val scalaOAuth2Core = Project(
4444
id = "scala-oauth2-core",
@@ -66,6 +66,23 @@ object ScalaOAuth2Build extends Build {
6666
)
6767
) dependsOn(scalaOAuth2Core % "compile->compile;test->test")
6868

69+
lazy val akkahttpOAuth2Provider = Project(
70+
id = "akka-http-oauth2-provider",
71+
base = file("akka-http-oauth2-provider"),
72+
settings = scalaOAuth2ProviderSettings ++ Seq(
73+
name := "akka-http-oauth2-provider",
74+
description := "Support scala-oauth2-core library on akka-http",
75+
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/maven-releases/",
76+
libraryDependencies ++= Seq(
77+
"com.typesafe.akka" %% "akka-http-experimental" % akkaV,
78+
"com.typesafe.akka" %% "akka-http-core" % akkaV,
79+
"com.typesafe.akka" %% "akka-http-testkit" % akkaV,
80+
"com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaV,
81+
"com.typesafe.akka" %% "akka-actor" % akkaV
82+
) ++ commonDependenciesInTestScope
83+
)
84+
) dependsOn(scalaOAuth2Core % "compile->compile;test->test")
85+
6986
def _publishTo(v: String) = {
7087
val nexus = "https://oss.sonatype.org/"
7188
if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")

0 commit comments

Comments
 (0)