Skip to content

Commit ff37bb8

Browse files
authored
Treat empty scope in token response as None instead of failing (#488)
* treat empty scope in token response as None instead of failing * add test for invalid content of scope
1 parent 6cbfc11 commit ff37bb8

File tree

5 files changed

+67
-1
lines changed

5 files changed

+67
-1
lines changed

oauth2-circe/shared/src/main/scala/com/ocadotechnology/sttp/oauth2/json/circe/CirceJsonDecoders.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cats.syntax.all._
44
import org.polyvariant.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse
55
import org.polyvariant.sttp.oauth2.UserInfo
66
import org.polyvariant.sttp.oauth2.common.Error.OAuth2Error
7+
import org.polyvariant.sttp.oauth2.common.Scope
78
import org.polyvariant.sttp.oauth2.ExtendedOAuth2TokenResponse
89
import org.polyvariant.sttp.oauth2.Introspection.Audience
910
import org.polyvariant.sttp.oauth2.Introspection.SeqAudience
@@ -26,6 +27,12 @@ trait CirceJsonDecoders {
2627
implicit def jsonDecoder[A](implicit decoder: Decoder[A]): JsonDecoder[A] =
2728
(data: String) => io.circe.parser.decode[A](data).leftMap(error => JsonDecoder.Error(error.getMessage, cause = Some(error)))
2829

30+
implicit val optionScopeDecoder: Decoder[Option[Scope]] =
31+
Decoder.decodeOption[String].flatMap {
32+
case Some("") => Decoder.decodeString.map[Option[Scope]](_ => None)
33+
case _ => Decoder.decodeOption[Scope]
34+
}
35+
2936
implicit val userInfoDecoder: Decoder[UserInfo] = (
3037
Decoder[Option[String]].at("sub"),
3138
Decoder[Option[String]].at("name"),

oauth2-jsoniter/shared/src/main/scala/com/ocadotechnology/sttp/oauth2/json/jsoniter/JsoniterJsonDecoders.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ trait JsoniterJsonDecoders {
5656
Secret(reader.readString(default = null))
5757
}
5858

59+
private[jsoniter] implicit val optionScopeDecoder: JsonValueCodec[Option[Scope]] = customDecoderWithDefault[Option[Scope]] { reader =>
60+
Try {
61+
reader.readString(default = null)
62+
}.flatMap {
63+
case "" => Try(None)
64+
case value => Scope.of(value).toRight(JsonDecoder.Error(s"$value is not a valid $Scope")).toTry.map(Some(_))
65+
}
66+
}(None)
67+
5968
private[jsoniter] implicit val scopeDecoder: JsonValueCodec[Scope] = customDecoderWithDefault[Scope] { reader =>
6069
Try {
6170
reader.readString(default = null)

oauth2/shared/src/main/scala/com/ocadotechnology/sttp/oauth2/common.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ object common {
2222

2323
implicit def scopeValidate: Validate.Plain[String, ValidScope] =
2424
Validate.fromPredicate(_.matches(scopeRegex), scope => s""""$scope" matches ValidScope""", ValidScope())
25+
2526
}
2627

2728
type Scope = String Refined ValidScope

oauth2/shared/src/test/scala/com/ocadotechnology/sttp/oauth2/json/ClientCredentialsAccessTokenResponseDeserializationSpec.scala

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ trait ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
6060
)
6161
}
6262

63-
"Token with empty scope" should "not be deserialized" in {
63+
"Token with empty scope" should "be deserialized with None scope" in {
6464
val json =
6565
// language=JSON
6666
"""
@@ -73,6 +73,28 @@ trait ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
7373
}
7474
"""
7575

76+
JsonDecoder[ClientCredentialsToken.AccessTokenResponse].decodeString(json).value shouldBe
77+
ClientCredentialsToken.AccessTokenResponse(
78+
accessToken = Secret("TAeJwlzT"),
79+
domain = Some("mock"),
80+
expiresIn = 2399.seconds,
81+
scope = None
82+
)
83+
}
84+
85+
"Token with malformed scope" should "not be deserialized" in {
86+
val json =
87+
// language=JSON
88+
"""
89+
{
90+
"access_token": "TAeJwlzT",
91+
"domain": "mock",
92+
"expires_in": 2399,
93+
"scope": "déjà vu",
94+
"token_type": "Bearer"
95+
}
96+
"""
97+
7698
JsonDecoder[ClientCredentialsToken.AccessTokenResponse].decodeString(json).left.value shouldBe a[JsonDecoder.Error]
7799
}
78100

oauth2/shared/src/test/scala/com/ocadotechnology/sttp/oauth2/json/ClientCredentialsTokenDeserializationSpec.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,33 @@ trait ClientCredentialsTokenDeserializationSpec extends AnyFlatSpec with Matcher
9191
)
9292
}
9393

94+
"token response JSON with empty scope" should "be deserialized to proper response with None scope" in {
95+
val json =
96+
// language=JSON
97+
"""
98+
{
99+
"access_token": "TAeJwlzT",
100+
"domain": "mock",
101+
"expires_in": 2399,
102+
"scope": "",
103+
"panda_session_id": "ac097e1f-f927-41df-a776-d824f538351c",
104+
"token_type": "Bearer"
105+
}
106+
"""
107+
108+
val response = JsonDecoder[Either[OAuth2Error, AccessTokenResponse]].decodeString(json)
109+
response shouldBe Right(
110+
Right(
111+
ClientCredentialsToken.AccessTokenResponse(
112+
accessToken = Secret("TAeJwlzT"),
113+
domain = Some("mock"),
114+
expiresIn = 2399.seconds,
115+
scope = None
116+
)
117+
)
118+
)
119+
}
120+
94121
"JSON with error" should "be deserialized to proper type" in {
95122
val json =
96123
// language=JSON

0 commit comments

Comments
 (0)