Skip to content

Commit

Permalink
RFC7662 compliant transport types (#114)
Browse files Browse the repository at this point in the history
* make domain field optional

* align introspection result with RFC

* temporarily disable mima

* zoo -> mock

* add transport type for audience

* re-enable mima
  • Loading branch information
majk-p authored Jul 22, 2021
1 parent 207fc71 commit fc3bc1a
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
implicit val mockBackend: SttpBackendStub[IO, Any] = AsyncHttpClientCatsBackend
.stub[IO]
.whenTokenIsRequested()
.thenRespond(Right(AccessTokenResponse(accessToken, "domain", 100.seconds, scope)))
.thenRespond(Right(AccessTokenResponse(accessToken, Some("domain"), 100.seconds, scope)))
.whenTestAppIsRequestedWithToken(accessToken)
.thenRespondOk()

Expand All @@ -57,7 +57,7 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
AsyncHttpClientCatsBackend
.stub[IO]
.whenTokenIsRequested()
.thenRespond(Right(AccessTokenResponse(accessToken, "domain", 100.seconds, scope)))
.thenRespond(Right(AccessTokenResponse(accessToken, Some("domain"), 100.seconds, scope)))
.whenTestAppIsRequestedWithToken(accessToken)
.thenRespondOk()
)
Expand All @@ -82,7 +82,7 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
AsyncHttpClientCatsBackend
.stub[IO]
.whenTokenIsRequested()
.thenRespondF(IO.sleep(200.millis).as(Response.ok(Right(AccessTokenResponse(accessToken, "domain", 100.seconds, scope)))))
.thenRespondF(IO.sleep(200.millis).as(Response.ok(Right(AccessTokenResponse(accessToken, Some("domain"), 100.seconds, scope)))))
.whenTestAppIsRequestedWithToken(accessToken)
.thenRespondOk()
)
Expand All @@ -108,8 +108,8 @@ class SttpOauth2ClientCredentialsCatsBackendSpec extends AsyncWordSpec with Matc
.stub[IO]
.whenTokenIsRequested()
.thenRespondCyclic(
Right(AccessTokenResponse(accessToken1, "domain", 100.millis, scope)),
Right(AccessTokenResponse(accessToken2, "domain", 100.millis, scope))
Right(AccessTokenResponse(accessToken1, Some("domain"), 100.millis, scope)),
Right(AccessTokenResponse(accessToken2, Some("domain"), 100.millis, scope))
)
.whenTestAppIsRequestedWithToken(accessToken1)
.thenRespond("body1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class SttpOauth2ClientCredentialsFutureBackendSpec extends AsyncWordSpec with Ma
implicit val mockBackend: SttpBackendStub[Future, Any] = AsyncHttpClientFutureBackend
.stub
.whenTokenIsRequested()
.thenRespond(Right(AccessTokenResponse(accessToken, "domain", 100.seconds, scope)))
.thenRespond(Right(AccessTokenResponse(accessToken, Some("domain"), 100.seconds, scope)))
.whenTestAppIsRequestedWithToken(accessToken)
.thenRespondOk()

Expand All @@ -52,7 +52,7 @@ class SttpOauth2ClientCredentialsFutureBackendSpec extends AsyncWordSpec with Ma
AsyncHttpClientFutureBackend
.stub
.whenTokenIsRequested()
.thenRespond(Right(AccessTokenResponse(accessToken, "domain", 100.seconds, scope)))
.thenRespond(Right(AccessTokenResponse(accessToken, Some("domain"), 100.seconds, scope)))
.whenTestAppIsRequestedWithToken(accessToken)
.thenRespondOk()
)
Expand All @@ -78,7 +78,7 @@ class SttpOauth2ClientCredentialsFutureBackendSpec extends AsyncWordSpec with Ma
AsyncHttpClientFutureBackend
.stub
.whenTokenIsRequested()
.thenRespondF(Future(Thread.sleep(200)).as(Response.ok(Right(AccessTokenResponse(accessToken, "domain", 100.seconds, scope)))))
.thenRespondF(Future(Thread.sleep(200)).as(Response.ok(Right(AccessTokenResponse(accessToken, Some("domain"), 100.seconds, scope)))))
.whenTestAppIsRequestedWithToken(accessToken)
.thenRespondOk()
)
Expand All @@ -105,8 +105,8 @@ class SttpOauth2ClientCredentialsFutureBackendSpec extends AsyncWordSpec with Ma
.stub
.whenTokenIsRequested()
.thenRespondCyclic(
Right(AccessTokenResponse(accessToken1, "domain", 100.millis, scope)),
Right(AccessTokenResponse(accessToken2, "domain", 100.millis, scope))
Right(AccessTokenResponse(accessToken1, Some("domain"), 100.millis, scope)),
Right(AccessTokenResponse(accessToken2, Some("domain"), 100.millis, scope))
)
.whenTestAppIsRequestedWithToken(accessToken1)
.thenRespond("body1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ object ClientCredentialsToken {

final case class AccessTokenResponse(
accessToken: Secret[String],
domain: String,
domain: Option[String],
expiresIn: FiniteDuration,
scope: Scope
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.ocadotechnology.sttp.oauth2

import java.time.Instant
import com.ocadotechnology.sttp.oauth2.common.Error.OAuth2Error
import com.ocadotechnology.sttp.oauth2.common._
import io.circe.Codec
import io.circe.Decoder
import io.circe.Encoder
import io.circe.refined._
import sttp.client3.ResponseAs
import com.ocadotechnology.sttp.oauth2.common.Error.OAuth2Error

import java.time.Instant

object Introspection {

Expand All @@ -20,31 +23,61 @@ object Introspection {
val response: ResponseAs[Response, Any] =
common.responseWithCommonError[TokenIntrospectionResponse]

// Defined by https://datatracker.ietf.org/doc/html/rfc7662#section-2.2 with some extra fields
final case class TokenIntrospectionResponse(
clientId: String,
domain: String,
exp: Instant,
active: Boolean,
authorities: List[String],
scope: Scope,
tokenType: String
clientId: Option[String] = None,
domain: Option[String] = None,
exp: Option[Instant] = None,
iat: Option[Instant] = None,
nbf: Option[Instant] = None,
authorities: Option[List[String]] = None,
scope: Option[Scope] = None,
tokenType: Option[String] = None,
sub: Option[String] = None,
iss: Option[String] = None,
jti: Option[String] = None,
aud: Option[Audience]
)

object TokenIntrospectionResponse {

private implicit val instantDecoder: Decoder[Instant] = Decoder.decodeLong.map(Instant.ofEpochSecond)

implicit val decoder: Decoder[TokenIntrospectionResponse] =
Decoder.forProduct7(
Decoder.forProduct13(
"active",
"client_id",
"domain",
"exp",
"active",
"iat",
"nbf",
"authorities",
"scope",
"token_type"
"token_type",
"sub",
"iss",
"jti",
"aud"
)(TokenIntrospectionResponse.apply)

}

sealed trait Audience extends Product with Serializable
final case class StringAudience(value: String) extends Audience
final case class SeqAudience(value: Seq[String]) extends Audience

object Audience {

private val encoder: Encoder[Audience] = _ match {
case StringAudience(value) => Encoder.encodeString(value)
case SeqAudience(value) => Encoder.encodeSeq[String].apply(value)
}

private val decoder: Decoder[Audience] =
Decoder.decodeString.map(StringAudience).or(Decoder.decodeSeq[String].map(SeqAudience))

implicit val codec: Codec[Audience] = Codec.from(decoder, encoder)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
// language=JSON
json"""{
"access_token": "TAeJwlzT",
"domain": "zoo",
"domain": "mock",
"expires_in": 2399,
"scope": "secondapp",
"token_type": "Bearer"
Expand All @@ -26,7 +26,7 @@ class ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
response shouldBe Right(
ClientCredentialsToken.AccessTokenResponse(
accessToken = Secret("TAeJwlzT"),
domain = "zoo",
domain = Some("mock"),
expiresIn = 2399.seconds,
scope = Scope.refine("secondapp")
)
Expand All @@ -38,7 +38,7 @@ class ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
// language=JSON
json"""{
"access_token": "TAeJwlzT",
"domain": "zoo",
"domain": "mock",
"expires_in": 2399,
"scope": "",
"token_type": "Bearer"
Expand All @@ -52,7 +52,7 @@ class ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
// language=JSON
json"""{
"access_token": "TAeJwlzT",
"domain": "zoo",
"domain": "mock",
"expires_in": 2399,
"scope": " ",
"token_type": "Bearer"
Expand All @@ -66,7 +66,7 @@ class ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
// language=JSON
json"""{
"access_token": "TAeJwlzT",
"domain": "zoo",
"domain": "mock",
"expires_in": 2399,
"scope": "scope1 scope2",
"token_type": "Bearer"
Expand All @@ -80,7 +80,7 @@ class ClientCredentialsAccessTokenResponseDeserializationSpec extends AnyFlatSpe
// language=JSON
json"""{
"access_token": "TAeJwlzT",
"domain": "zoo",
"domain": "mock",
"expires_in": 2399,
"scope": "secondapp",
"token_type": "BearerToken"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ClientCredentialsTokenDeserializationSpec extends AnyFlatSpec with Matcher
// language=JSON
json"""{
"access_token": "TAeJwlzT",
"domain": "zoo",
"domain": "mock",
"expires_in": 2399,
"panda_session_id": "ac097e1f-f927-41df-a776-d824f538351c",
"scope": "cfc.second-app_scope",
Expand All @@ -32,7 +32,7 @@ class ClientCredentialsTokenDeserializationSpec extends AnyFlatSpec with Matcher
Right(
ClientCredentialsToken.AccessTokenResponse(
accessToken = Secret("TAeJwlzT"),
domain = "zoo",
domain = Some("mock"),
expiresIn = 2399.seconds,
scope = Scope.refine("cfc.second-app_scope")
)
Expand All @@ -45,7 +45,7 @@ class ClientCredentialsTokenDeserializationSpec extends AnyFlatSpec with Matcher
// language=JSON
json"""{
"access_token": "TAeJwlzT",
"domain": "zoo",
"domain": "mock",
"expires_in": 2399,
"panda_session_id": "ac097e1f-f927-41df-a776-d824f538351c",
"scope": "secondapp",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ class IntrospectionSerializationSpec extends AnyWordSpec with Matchers with Opti
"Token" should {
"deserialize token introspection response" in {
val clientId = "Client ID"
val domain = "zoo"
val domain = "mock"
val exp = Instant.EPOCH
val active = false
val authorities = List("aaa", "bbb")
val scope = "cfc.first-app_scope"
val tokenType = "Bearer"
val audience = "Aud1"

val json = json"""{
"client_id": $clientId,
Expand All @@ -27,11 +28,21 @@ class IntrospectionSerializationSpec extends AnyWordSpec with Matchers with Opti
"active": $active,
"authorities": $authorities,
"scope": $scope,
"token_type": $tokenType
"token_type": $tokenType,
"aud": $audience
}"""

json.as[TokenIntrospectionResponse] shouldBe Right(
TokenIntrospectionResponse(clientId, domain, exp, active, authorities, Scope.of(scope).value, tokenType)
TokenIntrospectionResponse(
active = active,
clientId = Some(clientId),
domain = Some(domain),
exp= Some(exp),
authorities = Some(authorities),
scope = Some(Scope.of(scope).value),
tokenType = Some(tokenType),
aud = Some(Introspection.StringAudience(audience))
)
)

}
Expand Down

0 comments on commit fc3bc1a

Please sign in to comment.