Skip to content

Commit

Permalink
Treat empty scope in token response as None instead of failing (#488)
Browse files Browse the repository at this point in the history
* treat empty scope in token response as None instead of failing

* add test for invalid content of scope
  • Loading branch information
majk-p authored May 21, 2024
1 parent 6cbfc11 commit ff37bb8
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cats.syntax.all._
import org.polyvariant.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse
import org.polyvariant.sttp.oauth2.UserInfo
import org.polyvariant.sttp.oauth2.common.Error.OAuth2Error
import org.polyvariant.sttp.oauth2.common.Scope
import org.polyvariant.sttp.oauth2.ExtendedOAuth2TokenResponse
import org.polyvariant.sttp.oauth2.Introspection.Audience
import org.polyvariant.sttp.oauth2.Introspection.SeqAudience
Expand All @@ -26,6 +27,12 @@ trait CirceJsonDecoders {
implicit def jsonDecoder[A](implicit decoder: Decoder[A]): JsonDecoder[A] =
(data: String) => io.circe.parser.decode[A](data).leftMap(error => JsonDecoder.Error(error.getMessage, cause = Some(error)))

implicit val optionScopeDecoder: Decoder[Option[Scope]] =
Decoder.decodeOption[String].flatMap {
case Some("") => Decoder.decodeString.map[Option[Scope]](_ => None)
case _ => Decoder.decodeOption[Scope]
}

implicit val userInfoDecoder: Decoder[UserInfo] = (
Decoder[Option[String]].at("sub"),
Decoder[Option[String]].at("name"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ trait JsoniterJsonDecoders {
Secret(reader.readString(default = null))
}

private[jsoniter] implicit val optionScopeDecoder: JsonValueCodec[Option[Scope]] = customDecoderWithDefault[Option[Scope]] { reader =>
Try {
reader.readString(default = null)
}.flatMap {
case "" => Try(None)
case value => Scope.of(value).toRight(JsonDecoder.Error(s"$value is not a valid $Scope")).toTry.map(Some(_))
}
}(None)

private[jsoniter] implicit val scopeDecoder: JsonValueCodec[Scope] = customDecoderWithDefault[Scope] { reader =>
Try {
reader.readString(default = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ object common {

implicit def scopeValidate: Validate.Plain[String, ValidScope] =
Validate.fromPredicate(_.matches(scopeRegex), scope => s""""$scope" matches ValidScope""", ValidScope())

}

type Scope = String Refined ValidScope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ trait ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
)
}

"Token with empty scope" should "not be deserialized" in {
"Token with empty scope" should "be deserialized with None scope" in {
val json =
// language=JSON
"""
Expand All @@ -73,6 +73,28 @@ trait ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
}
"""

JsonDecoder[ClientCredentialsToken.AccessTokenResponse].decodeString(json).value shouldBe
ClientCredentialsToken.AccessTokenResponse(
accessToken = Secret("TAeJwlzT"),
domain = Some("mock"),
expiresIn = 2399.seconds,
scope = None
)
}

"Token with malformed scope" should "not be deserialized" in {
val json =
// language=JSON
"""
{
"access_token": "TAeJwlzT",
"domain": "mock",
"expires_in": 2399,
"scope": "déjà vu",
"token_type": "Bearer"
}
"""

JsonDecoder[ClientCredentialsToken.AccessTokenResponse].decodeString(json).left.value shouldBe a[JsonDecoder.Error]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,33 @@ trait ClientCredentialsTokenDeserializationSpec extends AnyFlatSpec with Matcher
)
}

"token response JSON with empty scope" should "be deserialized to proper response with None scope" in {
val json =
// language=JSON
"""
{
"access_token": "TAeJwlzT",
"domain": "mock",
"expires_in": 2399,
"scope": "",
"panda_session_id": "ac097e1f-f927-41df-a776-d824f538351c",
"token_type": "Bearer"
}
"""

val response = JsonDecoder[Either[OAuth2Error, AccessTokenResponse]].decodeString(json)
response shouldBe Right(
Right(
ClientCredentialsToken.AccessTokenResponse(
accessToken = Secret("TAeJwlzT"),
domain = Some("mock"),
expiresIn = 2399.seconds,
scope = None
)
)
)
}

"JSON with error" should "be deserialized to proper type" in {
val json =
// language=JSON
Expand Down

0 comments on commit ff37bb8

Please sign in to comment.