Skip to content

Commit 29dc8c8

Browse files
Christopher ToomeyChristopher Toomey
authored andcommitted
Make client credentials optional for password grant.
1 parent 46e5ff1 commit 29dc8c8

File tree

10 files changed

+90
-27
lines changed

10 files changed

+90
-27
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,25 @@ object MyController extends Controller with OAuth2Provider {
126126

127127
If you'd like to change the OAuth workflow, modify handleRequest methods of TokenEndPoint and ```ProtectedResource``` traits.
128128

129+
### Customizing Grant Handlers
130+
131+
If you want to change which grant types are supported or to use a customized handler for a grant type, you can
132+
override the ```handlers``` map in a customized ```TokenEndpoint``` trait. Here's an example of a customized
133+
```TokenEndpoint``` that 1) only supports the ```password``` grant type, and 2) customizes the ``password``` grant
134+
type handler to not require client credentials:
135+
136+
```scala
137+
class MyTokenEndpoint extends TokenEndpoint {
138+
val passwordNoCred = new Password(ClientCredentialFetcher) {
139+
override def clientCredentialRequired = false
140+
}
141+
142+
override val handlers = Map(
143+
"password" -> passwordNoCred
144+
)
145+
}
146+
```
147+
129148
## Examples
130149

131150
- [Playframework 2.2](https://github.com/oyediyildiz/scala-oauth2-provider-example)

play2-oauth2-provider/src/main/scala/scalaoauth2/provider/OAuth2Provider.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,4 @@ trait OAuth2AsyncProvider extends OAuth2BaseProvider {
225225
}
226226
}
227227

228-
}
228+
}

scala-oauth2-core/src/main/scala/scalaoauth2/provider/DataHandler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ case class AccessToken(token: String, refreshToken: Option[String], scope: Optio
2424
* @param scope Inform the client of the scope of the access token issued.
2525
* @param redirectUri This value is used by Authorization Code Grant.
2626
*/
27-
case class AuthInfo[+U](user: U, clientId: String, scope: Option[String], redirectUri: Option[String])
27+
case class AuthInfo[+U](user: U, clientId: Option[String], scope: Option[String], redirectUri: Option[String])
2828

2929
/**
3030
* Provide accessing to data storage for using OAuth 2.0.

scala-oauth2-core/src/main/scala/scalaoauth2/provider/GrantHandler.scala

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import scala.concurrent.ExecutionContext.Implicits.global
66
case class GrantHandlerResult(tokenType: String, accessToken: String, expiresIn: Option[Long], refreshToken: Option[String], scope: Option[String])
77

88
trait GrantHandler {
9+
/**
10+
* Controls whether client credentials are required. Defaults to true but can be overridden to be false when needed.
11+
* Per the OAuth2 specification, client credentials are required for all grant types except password, where it is up
12+
* to the authorization provider whether to make them required or not.
13+
*/
14+
def clientCredentialRequired = true
915

1016
def handleRequest[U](request: AuthorizationRequest, dataHandler: DataHandler[U]): Future[GrantHandlerResult]
1117

@@ -45,7 +51,7 @@ class RefreshToken(clientCredentialFetcher: ClientCredentialFetcher) extends Gra
4551

4652
dataHandler.findAuthInfoByRefreshToken(refreshToken).flatMap { authInfoOption =>
4753
val authInfo = authInfoOption.getOrElse(throw new InvalidGrant("Authorized information is not found by the refresh token"))
48-
if (authInfo.clientId != clientCredential.clientId) {
54+
if (authInfo.clientId != Some(clientCredential.clientId)) {
4955
throw new InvalidClient
5056
}
5157

@@ -65,14 +71,16 @@ class RefreshToken(clientCredentialFetcher: ClientCredentialFetcher) extends Gra
6571
class Password(clientCredentialFetcher: ClientCredentialFetcher) extends GrantHandler {
6672

6773
override def handleRequest[U](request: AuthorizationRequest, dataHandler: DataHandler[U]): Future[GrantHandlerResult] = {
68-
val clientCredential = clientCredentialFetcher.fetch(request).getOrElse(throw new InvalidRequest("Authorization header is invalid"))
74+
val clientCredential = clientCredentialFetcher.fetch(request)
75+
if (clientCredentialRequired && clientCredential.isEmpty)
76+
throw new InvalidRequest("Authorization header is invalid")
6977
val username = request.requireUsername
7078
val password = request.requirePassword
7179

7280
dataHandler.findUser(username, password).flatMap { userOption =>
7381
val user = userOption.getOrElse(throw new InvalidGrant("username or password is incorrect"))
7482
val scope = request.scope
75-
val clientId = clientCredential.clientId
83+
val clientId = clientCredential.map { _.clientId }
7684
val authInfo = AuthInfo(user, clientId, scope, None)
7785

7886
issueAccessToken(dataHandler, authInfo)
@@ -90,7 +98,7 @@ class ClientCredentials(clientCredentialFetcher: ClientCredentialFetcher) extend
9098

9199
dataHandler.findClientUser(clientId, clientSecret, scope).flatMap { userOption =>
92100
val user = userOption.getOrElse(throw new InvalidGrant("client_id or client_secret or scope is incorrect"))
93-
val authInfo = AuthInfo(user, clientId, scope, None)
101+
val authInfo = AuthInfo(user, Some(clientId), scope, None)
94102

95103
issueAccessToken(dataHandler, authInfo)
96104
}
@@ -108,7 +116,7 @@ class AuthorizationCode(clientCredentialFetcher: ClientCredentialFetcher) extend
108116

109117
dataHandler.findAuthInfoByCode(code).flatMap { authInfoOption =>
110118
val authInfo = authInfoOption.getOrElse(throw new InvalidGrant("Authorized information is not found by the code"))
111-
if (authInfo.clientId != clientId) {
119+
if (authInfo.clientId != Some(clientId)) {
112120
throw new InvalidClient
113121
}
114122

scala-oauth2-core/src/main/scala/scalaoauth2/provider/TokenEndpoint.scala

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import scala.concurrent.Future
44
import scala.concurrent.ExecutionContext.Implicits.global
55

66
trait TokenEndpoint {
7-
87
val fetcher = ClientCredentialFetcher
98

109
val handlers = Map(
@@ -17,16 +16,25 @@ trait TokenEndpoint {
1716
def handleRequest[U](request: AuthorizationRequest, dataHandler: DataHandler[U]): Future[Either[OAuthError, GrantHandlerResult]] = try {
1817
val grantType = request.grantType.getOrElse(throw new InvalidRequest("grant_type is not found"))
1918
val handler = handlers.get(grantType).getOrElse(throw new UnsupportedGrantType("The grant_type is not supported"))
20-
val clientCredential = fetcher.fetch(request).getOrElse(throw new InvalidRequest("Client credential is not found"))
2119

22-
dataHandler.validateClient(clientCredential.clientId, clientCredential.clientSecret, grantType).flatMap { validClient =>
23-
if (!validClient) {
24-
Future.successful(Left(throw new InvalidClient()))
20+
fetcher.fetch(request).map { clientCredential =>
21+
dataHandler.validateClient(clientCredential.clientId, clientCredential.clientSecret, grantType).flatMap { validClient =>
22+
if (!validClient) {
23+
Future.successful(Left(throw new InvalidClient()))
24+
} else {
25+
handler.handleRequest(request, dataHandler).map(Right(_))
26+
}
27+
}.recover {
28+
case e: OAuthError => Left(e)
29+
}
30+
}.getOrElse {
31+
if (handler.clientCredentialRequired) {
32+
throw new InvalidRequest("Client credential is not found")
2533
} else {
26-
handler.handleRequest(request, dataHandler).map(Right(_))
34+
handler.handleRequest(request, dataHandler).map(Right(_)).recover {
35+
case e: OAuthError => Left(e)
36+
}
2737
}
28-
}.recover {
29-
case e: OAuthError => Left(e)
3038
}
3139
} catch {
3240
case e: OAuthError => Future.successful(Left(e))

scala-oauth2-core/src/test/scala/scalaoauth2/provider/AuthorizationCodeSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class AuthorizationCodeSpec extends FlatSpec with ScalaFutures {
1616
val f = authorizationCode.handleRequest(request, new MockDataHandler() {
1717

1818
override def findAuthInfoByCode(code: String): Future[Option[AuthInfo[User]]] = Future.successful(Some(
19-
AuthInfo(user = MockUser(10000, "username"), clientId = "clientId1", scope = Some("all"), redirectUri = Some("http://example.com/"))
19+
AuthInfo(user = MockUser(10000, "username"), clientId = Some("clientId1"), scope = Some("all"), redirectUri = Some("http://example.com/"))
2020
))
2121

2222
override def createAccessToken(authInfo: AuthInfo[User]): Future[AccessToken] = Future.successful(AccessToken("token1", Some("refreshToken1"), Some("all"), Some(3600), new java.util.Date()))
@@ -37,7 +37,7 @@ class AuthorizationCodeSpec extends FlatSpec with ScalaFutures {
3737
val f = authorizationCode.handleRequest(request, new MockDataHandler() {
3838

3939
override def findAuthInfoByCode(code: String): Future[Option[AuthInfo[MockUser]]] = Future.successful(Some(
40-
AuthInfo(user = MockUser(10000, "username"), clientId = "clientId1", scope = Some("all"), redirectUri = None)
40+
AuthInfo(user = MockUser(10000, "username"), clientId = Some("clientId1"), scope = Some("all"), redirectUri = None)
4141
))
4242

4343
override def createAccessToken(authInfo: AuthInfo[User]): Future[AccessToken] = Future.successful(AccessToken("token1", Some("refreshToken1"), Some("all"), Some(3600), new java.util.Date()))

scala-oauth2-core/src/test/scala/scalaoauth2/provider/PasswordSpec.scala

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ import scala.concurrent.Future
88

99
class PasswordSpec extends FlatSpec with ScalaFutures {
1010

11-
it should "handle request" in {
12-
val password = new Password(new MockClientCredentialFetcher())
11+
val passwordClientCredReq = new Password(new MockClientCredentialFetcher())
12+
val passwordNoClientCredReq = new Password(new MockClientCredentialFetcher()) {
13+
override def clientCredentialRequired = false
14+
}
15+
16+
"Password when client credential required" should "handle request" in handlesRequest(passwordClientCredReq)
17+
"Password when client credential not required" should "handle request" in handlesRequest(passwordNoClientCredReq)
18+
19+
def handlesRequest(password: Password) = {
1320
val request = AuthorizationRequest(Map(), Map("username" -> Seq("user"), "password" -> Seq("pass"), "scope" -> Seq("all")))
1421
val f = password.handleRequest(request, new MockDataHandler() {
1522

@@ -20,11 +27,11 @@ class PasswordSpec extends FlatSpec with ScalaFutures {
2027
})
2128

2229
whenReady(f) { result =>
23-
result.tokenType should be ("Bearer")
24-
result.accessToken should be ("token1")
25-
result.expiresIn should be (Some(3600))
26-
result.refreshToken should be (Some("refreshToken1"))
27-
result.scope should be (Some("all"))
30+
result.tokenType should be("Bearer")
31+
result.accessToken should be("token1")
32+
result.expiresIn should be(Some(3600))
33+
result.refreshToken should be(Some("refreshToken1"))
34+
result.scope should be(Some("all"))
2835
}
2936
}
3037

scala-oauth2-core/src/test/scala/scalaoauth2/provider/ProtectedResourceSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class ProtectedResourceSpec extends FlatSpec with ScalaFutures {
1616
override def findAccessToken(token: String): Future[Option[AccessToken]] = Future.successful(Some(AccessToken("token1", Some("refreshToken1"), Some("all"), Some(3600), new Date())))
1717

1818
override def findAuthInfoByAccessToken(accessToken: AccessToken): Future[Option[AuthInfo[User]]] = Future.successful(Some(
19-
AuthInfo(user = MockUser(10000, "username"), clientId = "clientId1", scope = Some("all"), redirectUri = None)
19+
AuthInfo(user = MockUser(10000, "username"), clientId = Some("clientId1"), scope = Some("all"), redirectUri = None)
2020
))
2121

2222
}
@@ -52,7 +52,7 @@ class ProtectedResourceSpec extends FlatSpec with ScalaFutures {
5252
override def findAccessToken(token: String): Future[Option[AccessToken]] = Future.successful(Some(AccessToken("token1", Some("refreshToken1"), Some("all"), Some(3600), new Date(new Date().getTime() - 4000 * 1000))))
5353

5454
override def findAuthInfoByAccessToken(accessToken: AccessToken): Future[Option[AuthInfo[MockUser]]] = Future.successful(Some(
55-
AuthInfo(user = MockUser(10000, "username"), clientId = "clientId1", scope = Some("all"), redirectUri = None)
55+
AuthInfo(user = MockUser(10000, "username"), clientId = Some("clientId1"), scope = Some("all"), redirectUri = None)
5656
))
5757

5858
}

scala-oauth2-core/src/test/scala/scalaoauth2/provider/RefreshTokenSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class RefreshTokenSpec extends FlatSpec with ScalaFutures {
1616
val f = refreshToken.handleRequest(request, new MockDataHandler() {
1717

1818
override def findAuthInfoByRefreshToken(refreshToken: String): Future[Option[AuthInfo[User]]] =
19-
Future.successful(Some(AuthInfo(user = MockUser(10000, "username"), clientId = "clientId1", scope = None, redirectUri = None)))
19+
Future.successful(Some(AuthInfo(user = MockUser(10000, "username"), clientId = Some("clientId1"), scope = None, redirectUri = None)))
2020

2121
override def refreshAccessToken(authInfo: AuthInfo[User], refreshToken: String): Future[AccessToken] = Future.successful(AccessToken("token1", Some(refreshToken), None, Some(3600), new java.util.Date()))
2222

scala-oauth2-core/src/test/scala/scalaoauth2/provider/TokenEndPointSpec.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,27 @@ class TokenEndPointSpec extends FlatSpec with ScalaFutures {
9393
}
9494
}
9595

96+
it should "not be invalid request without client credential when not required" in {
97+
val request = AuthorizationRequest(
98+
Map(),
99+
Map("grant_type" -> Seq("password"), "username" -> Seq("user"), "password" -> Seq("pass"), "scope" -> Seq("all"))
100+
)
101+
102+
val dataHandler = successfulDataHandler()
103+
val passwordNoCred = new Password(ClientCredentialFetcher) {
104+
override def clientCredentialRequired = false
105+
}
106+
class MyTokenEndpoint extends TokenEndpoint {
107+
override val handlers = Map(
108+
"password" -> passwordNoCred
109+
)
110+
}
111+
112+
val f = (new MyTokenEndpoint().handleRequest(request, dataHandler))
113+
114+
whenReady(f) { result => result should be ('right)}
115+
}
116+
96117
it should "be invalid client if client information is wrong" in {
97118
val request = AuthorizationRequest(
98119
Map("Authorization" -> Seq("Basic Y2xpZW50X2lkX3ZhbHVlOmNsaWVudF9zZWNyZXRfdmFsdWU=")),

0 commit comments

Comments
 (0)