Skip to content

Commit 4cc5c7a

Browse files
rmmeanstsuyoshizawa
authored andcommitted
Authorization Header Specification fixes (#110)
* Authorization Header Specification fixes * The root of this PR stems from this part of the specification: https://tools.ietf.org/html/rfc6749#section-5.2 This section states that in the event a client is authenticating via the “Authorization” header field, then the authorization server MUST respond with a 401 Unauthorized in the event the client does not have access. This validation MUST occur before any other validations occur since this is an authentication state via a standard header. Only after the authorization has validated - assuming via the Authorization header - can the rest of the processing and validation continue. * Added some documentation comments on the AuthorizationHandler trait further explaining what a client validation request must do. * Removed some unnecessary overrides for fetching the clientCrential in the request type case classes as they are unnecessary since they don’t override anything. * Cleaned up tests to reflect changes for Authorization Header changes, added a few new tests to cover the new case. * changing to `parseClientCredential` and `maybeValidatedClientCred` per convo on PR
1 parent 7f32072 commit 4cc5c7a

File tree

11 files changed

+170
-87
lines changed

11 files changed

+170
-87
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ trait AuthorizationHandler[U] {
5555

5656
/**
5757
* Verify proper client with parameters for issue an access token.
58+
* Note that per the OAuth Specification, a Client may be valid if it only contains a client ID but no client
59+
* secret (common with Public Clients). However, if the registered client has a client secret value the specification
60+
* requires that a client secret must always be provided and verified for that client ID.
5861
*
5962
* @param request Request sent by client.
6063
* @return true if request is a regular client, false if request is a illegal client.

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

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,32 @@ class AuthorizationRequest(headers: Map[String, Seq[String]], params: Map[String
1212

1313
def grantType: String = requireParam("grant_type")
1414

15-
lazy val clientCredential: Option[ClientCredential] =
15+
def parseClientCredential: Option[Either[InvalidClient, ClientCredential]] =
1616
findAuthorization
17-
.flatMap(clientCredentialByAuthorization)
18-
.orElse(clientCredentialByParam)
19-
20-
private def findAuthorization = for {
21-
authorization <- header("Authorization")
22-
matcher <- """^\s*Basic\s+(.+?)\s*$""".r.findFirstMatchIn(authorization)
23-
} yield matcher.group(1)
24-
25-
private def clientCredentialByAuthorization(s: String) =
17+
.flatMap(x => Some(x.fold(
18+
left => Left(left),
19+
header => clientCredentialByAuthorization(header)
20+
)))
21+
.orElse(clientCredentialByParam.map(Right(_)))
22+
23+
private def findAuthorization: Option[Either[InvalidClient, String]] = {
24+
header("Authorization").map { auth =>
25+
val basicAuthCred = for {
26+
matcher <- """^\s*Basic\s+(.+?)\s*$""".r.findFirstMatchIn(auth)
27+
} yield matcher.group(1)
28+
29+
basicAuthCred.fold[Either[InvalidClient, String]](Left(new InvalidClient("Authorization header could not be parsed")))(x => Right(x))
30+
}
31+
}
32+
33+
private def clientCredentialByAuthorization(s: String): Either[InvalidClient, ClientCredential] =
2634
Try(new String(Base64.getDecoder.decode(s), "UTF-8"))
2735
.map(_.split(":", 2))
2836
.getOrElse(Array.empty) match {
2937
case Array(clientId, clientSecret) =>
30-
Some(ClientCredential(clientId, if (clientSecret.isEmpty) None else Some(clientSecret)))
38+
Right(ClientCredential(clientId, if (clientSecret.isEmpty) None else Some(clientSecret)))
3139
case _ =>
32-
None
40+
Left(new InvalidClient())
3341
}
3442

3543
private def clientCredentialByParam = param("client_id").map(ClientCredential(_, param("client_secret")))
@@ -44,8 +52,6 @@ case class RefreshTokenRequest(request: AuthorizationRequest) extends Authorizat
4452
* @throws InvalidRequest if the parameter is not found
4553
*/
4654
def refreshToken: String = requireParam("refresh_token")
47-
48-
override lazy val clientCredential = request.clientCredential
4955
}
5056

5157
case class PasswordRequest(request: AuthorizationRequest) extends AuthorizationRequest(request.headers, request.params) {
@@ -64,13 +70,9 @@ case class PasswordRequest(request: AuthorizationRequest) extends AuthorizationR
6470
* @throws InvalidRequest if the parameter is not found
6571
*/
6672
def password = requireParam("password")
67-
68-
override lazy val clientCredential = request.clientCredential
6973
}
7074

71-
case class ClientCredentialsRequest(request: AuthorizationRequest) extends AuthorizationRequest(request.headers, request.params) {
72-
override lazy val clientCredential = request.clientCredential
73-
}
75+
case class ClientCredentialsRequest(request: AuthorizationRequest) extends AuthorizationRequest(request.headers, request.params)
7476

7577
case class AuthorizationCodeRequest(request: AuthorizationRequest) extends AuthorizationRequest(request.headers, request.params) {
7678
/**
@@ -87,11 +89,6 @@ case class AuthorizationCodeRequest(request: AuthorizationRequest) extends Autho
8789
* @return redirect_uri
8890
*/
8991
def redirectUri: Option[String] = param("redirect_uri")
90-
91-
override lazy val clientCredential = request.clientCredential
92-
9392
}
9493

95-
case class ImplicitRequest(request: AuthorizationRequest) extends AuthorizationRequest(request.headers, request.params) {
96-
override lazy val clientCredential = request.clientCredential
97-
}
94+
case class ImplicitRequest(request: AuthorizationRequest) extends AuthorizationRequest(request.headers, request.params)

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

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ trait GrantHandler {
2020
*/
2121
def clientCredentialRequired = true
2222

23-
def handleRequest[U](request: AuthorizationRequest, authorizationHandler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]]
23+
def handleRequest[U](maybeValidatedClientCred: Option[ClientCredential], request: AuthorizationRequest, authorizationHandler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]]
2424

2525
/**
2626
* Returns valid access token.
@@ -53,51 +53,51 @@ trait GrantHandler {
5353

5454
class RefreshToken extends GrantHandler {
5555

56-
override def handleRequest[U](request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
56+
override def handleRequest[U](maybeValidatedClientCred: Option[ClientCredential], request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
57+
val clientId = maybeValidatedClientCred.getOrElse(throw new InvalidRequest("Client credential is required")).clientId
5758
val refreshTokenRequest = RefreshTokenRequest(request)
58-
val clientCredential = refreshTokenRequest.clientCredential.getOrElse(throw new InvalidRequest("Client credential is required"))
5959
val refreshToken = refreshTokenRequest.refreshToken
6060

6161
handler.findAuthInfoByRefreshToken(refreshToken).flatMap { authInfoOption =>
6262
val authInfo = authInfoOption.getOrElse(throw new InvalidGrant("Authorized information is not found by the refresh token"))
63-
if (!authInfo.clientId.contains(clientCredential.clientId)) {
64-
throw new InvalidClient
65-
}
66-
63+
if (!authInfo.clientId.contains(clientId)) throw new InvalidClient
6764
handler.refreshAccessToken(authInfo, refreshToken).map(createGrantHandlerResult(authInfo, _))
6865
}
6966
}
7067
}
7168

7269
class Password extends GrantHandler {
7370

74-
override def handleRequest[U](request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
75-
val passwordRequest = PasswordRequest(request)
76-
if (clientCredentialRequired && passwordRequest.clientCredential.isEmpty) {
71+
override def handleRequest[U](maybeValidatedClientCred: Option[ClientCredential], request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
72+
/**
73+
* Given that client credentials may be optional, if they are required, they must be fully validated before
74+
* further processing.
75+
*/
76+
if (clientCredentialRequired && maybeValidatedClientCred.isEmpty) {
7777
throw new InvalidRequest("Client credential is required")
78-
}
79-
80-
handler.findUser(passwordRequest).flatMap { maybeUser =>
81-
val user = maybeUser.getOrElse(throw new InvalidGrant("username or password is incorrect"))
82-
val scope = passwordRequest.scope
83-
val maybeClientId = passwordRequest.clientCredential.map(_.clientId)
84-
val authInfo = AuthInfo(user, maybeClientId, scope, None)
85-
86-
issueAccessToken(handler, authInfo)
78+
} else {
79+
val passwordRequest = PasswordRequest(request)
80+
handler.findUser(passwordRequest).flatMap { maybeUser =>
81+
val user = maybeUser.getOrElse(throw new InvalidGrant("username or password is incorrect"))
82+
val scope = passwordRequest.scope
83+
val authInfo = AuthInfo(user, maybeValidatedClientCred.map(_.clientId), scope, None)
84+
85+
issueAccessToken(handler, authInfo)
86+
}
8787
}
8888
}
8989
}
9090

9191
class ClientCredentials extends GrantHandler {
9292

93-
override def handleRequest[U](request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
93+
override def handleRequest[U](maybeValidatedClientCred: Option[ClientCredential], request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
94+
val clientId = maybeValidatedClientCred.getOrElse(throw new InvalidRequest("Client credential is required")).clientId
9495
val clientCredentialsRequest = ClientCredentialsRequest(request)
95-
val clientCredential = clientCredentialsRequest.clientCredential.getOrElse(throw new InvalidRequest("Client credential is required"))
9696
val scope = clientCredentialsRequest.scope
9797

9898
handler.findUser(clientCredentialsRequest).flatMap { optionalUser =>
9999
val user = optionalUser.getOrElse(throw new InvalidGrant("client_id or client_secret or scope is incorrect"))
100-
val authInfo = AuthInfo(user, Some(clientCredential.clientId), scope, None)
100+
val authInfo = AuthInfo(user, Some(clientId), scope, None)
101101

102102
issueAccessToken(handler, authInfo)
103103
}
@@ -107,10 +107,9 @@ class ClientCredentials extends GrantHandler {
107107

108108
class AuthorizationCode extends GrantHandler {
109109

110-
override def handleRequest[U](request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
110+
override def handleRequest[U](maybeValidatedClientCred: Option[ClientCredential], request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
111+
val clientId = maybeValidatedClientCred.getOrElse(throw new InvalidRequest("Client credential is required")).clientId
111112
val authorizationCodeRequest = AuthorizationCodeRequest(request)
112-
val clientCredential = authorizationCodeRequest.clientCredential.getOrElse(throw new InvalidRequest("Client credential is required"))
113-
val clientId = clientCredential.clientId
114113
val code = authorizationCodeRequest.code
115114
val redirectUri = authorizationCodeRequest.redirectUri
116115

@@ -136,14 +135,14 @@ class AuthorizationCode extends GrantHandler {
136135

137136
class Implicit extends GrantHandler {
138137

139-
override def handleRequest[U](request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
138+
override def handleRequest[U](maybeValidatedClientCred: Option[ClientCredential], request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[GrantHandlerResult[U]] = {
139+
val clientId = maybeValidatedClientCred.getOrElse(throw new InvalidRequest("Client credential is required")).clientId
140140
val implicitRequest = ImplicitRequest(request)
141-
val clientCredential = implicitRequest.clientCredential.getOrElse(throw new InvalidRequest("Client credential is required"))
142141

143142
handler.findUser(implicitRequest).flatMap { maybeUser =>
144143
val user = maybeUser.getOrElse(throw new InvalidGrant("user cannot be authenticated"))
145144
val scope = implicitRequest.scope
146-
val authInfo = AuthInfo(user, Some(clientCredential.clientId), scope, None)
145+
val authInfo = AuthInfo(user, Some(clientId), scope, None)
147146

148147
issueAccessToken(handler, authInfo)
149148
}

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,29 @@ trait TokenEndpoint {
77

88
def handleRequest[U](request: AuthorizationRequest, handler: AuthorizationHandler[U])(implicit ctx: ExecutionContext): Future[Either[OAuthError, GrantHandlerResult[U]]] = try {
99
val grantType = request.grantType
10-
val grantHandler = handlers.getOrElse(grantType, throw new UnsupportedGrantType(s"${grantType} is not supported"))
10+
val grantHandler = () => handlers.getOrElse(grantType, throw new UnsupportedGrantType(s"$grantType is not supported"))
1111

12-
request.clientCredential.map { clientCredential =>
13-
handler.validateClient(request).flatMap { validClient =>
14-
if (!validClient) {
15-
Future.successful(Left(new InvalidClient("Invalid client is detected")))
16-
} else {
17-
grantHandler.handleRequest(request, handler).map(Right(_))
12+
request.parseClientCredential.map { maybeCredential =>
13+
maybeCredential.fold(
14+
invalid => Future.successful(Left(invalid)),
15+
clientCredential => {
16+
handler.validateClient(request).flatMap { isValidClient =>
17+
if (!isValidClient) {
18+
Future.successful(Left(new InvalidClient("Invalid client or client is not authorized")))
19+
} else {
20+
grantHandler().handleRequest(Some(clientCredential), request, handler).map(Right(_))
21+
}
22+
}.recover {
23+
case e: OAuthError => Left(e)
24+
}
1825
}
19-
}.recover {
20-
case e: OAuthError => Left(e)
21-
}
26+
)
2227
}.getOrElse {
23-
if (grantHandler.clientCredentialRequired) {
28+
val gh = grantHandler()
29+
if (gh.clientCredentialRequired) {
2430
throw new InvalidRequest("Client credential is not found")
2531
} else {
26-
grantHandler.handleRequest(request, handler).map(Right(_)).recover {
32+
gh.handleRequest(None, request, handler).map(Right(_)).recover {
2733
case e: OAuthError => Left(e)
2834
}
2935
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ class AuthorizationCodeSpec extends FlatSpec with ScalaFutures with OptionValues
1414
val authorizationCode = new AuthorizationCode()
1515
val request = new AuthorizationRequest(Map(), Map("client_id" -> Seq("clientId1"), "client_secret" -> Seq("clientSecret1"), "code" -> Seq("code1"), "redirect_uri" -> Seq("http://example.com/")))
1616
var codeDeleted: Boolean = false
17-
val f = authorizationCode.handleRequest(request, new MockDataHandler() {
17+
val clientCred = request.parseClientCredential.fold[Option[ClientCredential]](None)(_.fold(_ => None, c => Some(c)))
18+
val f = authorizationCode.handleRequest(clientCred, request, new MockDataHandler() {
1819

1920
override def findAuthInfoByCode(code: String): Future[Option[AuthInfo[User]]] = Future.successful(Some(
2021
AuthInfo(user = MockUser(10000, "username"), clientId = Some("clientId1"), scope = Some("all"), redirectUri = Some("http://example.com/"))
@@ -42,7 +43,8 @@ class AuthorizationCodeSpec extends FlatSpec with ScalaFutures with OptionValues
4243
it should "handle request if redirectUrl is none" in {
4344
val authorizationCode = new AuthorizationCode()
4445
val request = new AuthorizationRequest(Map(), Map("client_id" -> Seq("clientId1"), "client_secret" -> Seq("clientSecret1"), "code" -> Seq("code1"), "redirect_uri" -> Seq("http://example.com/")))
45-
val f = authorizationCode.handleRequest(request, new MockDataHandler() {
46+
val clientCred = request.parseClientCredential.fold[Option[ClientCredential]](None)(_.fold(_ => None, c => Some(c)))
47+
val f = authorizationCode.handleRequest(clientCred, request, new MockDataHandler() {
4648

4749
override def findAuthInfoByCode(code: String): Future[Option[AuthInfo[MockUser]]] = Future.successful(Some(
4850
AuthInfo(user = MockUser(10000, "username"), clientId = Some("clientId1"), scope = Some("all"), redirectUri = None)
@@ -63,7 +65,8 @@ class AuthorizationCodeSpec extends FlatSpec with ScalaFutures with OptionValues
6365
it should "return a Failure Future" in {
6466
val authorizationCode = new AuthorizationCode()
6567
val request = new AuthorizationRequest(Map(), Map("client_id" -> Seq("clientId1"), "client_secret" -> Seq("clientSecret1"), "code" -> Seq("code1"), "redirect_uri" -> Seq("http://example.com/")))
66-
val f = authorizationCode.handleRequest(request, new MockDataHandler() {
68+
val clientCred = request.parseClientCredential.fold[Option[ClientCredential]](None)(_.fold(_ => None, c => Some(c)))
69+
val f = authorizationCode.handleRequest(clientCred, request, new MockDataHandler() {
6770

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

0 commit comments

Comments
 (0)