mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
user tokens in oauth2-middleware (#12563)
* operate tokenProvidesClaims on user management service output * ignore everything but the applicationId claim for StandardJWTPayload * let the client tester work with non-custom tokens * test claims check on non-custom token * add changelog CHANGELOG_BEGIN - [Auth Middleware] Supports standard auth tokens for participant user management. See `issue #12563 <https://github.com/digital-asset/daml/pull/12563>`__. CHANGELOG_END
This commit is contained in:
parent
e3bae961cc
commit
6bdb901127
@ -13,7 +13,8 @@ import akka.http.scaladsl.model.headers.{HttpCookie, HttpCookiePair}
|
||||
import akka.http.scaladsl.server.{Directive1, Route}
|
||||
import akka.http.scaladsl.server.Directives._
|
||||
import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}
|
||||
import com.daml.ledger.api.refinements.ApiTypes.ApplicationId
|
||||
import com.daml.ledger.api.{auth => lapiauth}
|
||||
import com.daml.ledger.api.refinements.ApiTypes.{ApplicationId, Party}
|
||||
import com.daml.auth.oauth2.api.{JsonProtocol => OAuthJsonProtocol, Response => OAuthResponse}
|
||||
import com.typesafe.scalalogging.StrictLogging
|
||||
|
||||
@ -21,7 +22,6 @@ import java.util.UUID
|
||||
import com.daml.auth.middleware.api.{Request, RequestStore, Response}
|
||||
import com.daml.jwt.{JwtDecoder, JwtVerifierBase}
|
||||
import com.daml.jwt.domain.Jwt
|
||||
import com.daml.ledger.api.auth.{AuthServiceJWTCodec, CustomDamlJWTPayload, StandardJWTPayload}
|
||||
import com.daml.auth.middleware.api.Tagged.{AccessToken, RefreshToken}
|
||||
import com.daml.ports.{Port, PortFiles}
|
||||
import scalaz.{-\/, \/-}
|
||||
@ -36,6 +36,7 @@ import scala.util.{Failure, Success, Try}
|
||||
class Server(config: Config) extends StrictLogging {
|
||||
import com.daml.auth.middleware.api.JsonProtocol._
|
||||
import com.daml.auth.oauth2.api.JsonProtocol._
|
||||
import Server.rightsProvideClaims
|
||||
|
||||
implicit private val unmarshal: Unmarshaller[String, Uri] = Unmarshaller.strict(Uri(_))
|
||||
|
||||
@ -63,33 +64,11 @@ class Server(config: Config) extends StrictLogging {
|
||||
private def tokenProvidesClaims(accessToken: String, claims: Request.Claims): Boolean = {
|
||||
for {
|
||||
decodedJwt <- JwtDecoder.decode(Jwt(accessToken)).toOption
|
||||
tokenPayload <- AuthServiceJWTCodec
|
||||
tokenPayload <- lapiauth.AuthServiceJWTCodec
|
||||
.readFromString(decodedJwt.payload)
|
||||
.map {
|
||||
case _: StandardJWTPayload =>
|
||||
throw new UnsupportedOperationException(
|
||||
// TODO (i12388): make auth middlware work with user tokens
|
||||
"auth-middleware: user access tokens are not yet supported (https://github.com/digital-asset/daml/issues/12388)."
|
||||
)
|
||||
case payload: CustomDamlJWTPayload => payload
|
||||
}
|
||||
.toOption
|
||||
} yield {
|
||||
(tokenPayload.admin || !claims.admin) &&
|
||||
claims.actAs.map(_.toString).toSet.subsetOf(tokenPayload.actAs.toSet) &&
|
||||
claims.readAs
|
||||
.map(_.toString)
|
||||
.toSet
|
||||
.subsetOf(tokenPayload.readAs.toSet ++ tokenPayload.actAs.toSet) &&
|
||||
((claims.applicationId, tokenPayload.applicationId) match {
|
||||
// No requirement on app id
|
||||
case (None, _) => true
|
||||
// Token valid for all app ids.
|
||||
case (_, None) => true
|
||||
case (Some(expectedAppId), Some(actualAppId)) => expectedAppId == ApplicationId(actualAppId)
|
||||
})
|
||||
}
|
||||
}.getOrElse(false)
|
||||
} yield rightsProvideClaims(tokenPayload, claims)
|
||||
} getOrElse false
|
||||
|
||||
private val requestTemplates: RequestTemplates = RequestTemplates(
|
||||
config.clientId,
|
||||
@ -352,4 +331,37 @@ object Server extends StrictLogging {
|
||||
|
||||
def stop(f: Future[ServerBinding])(implicit ec: ExecutionContext): Future[Done] =
|
||||
f.flatMap(_.unbind())
|
||||
|
||||
private[oauth2] def rightsProvideClaims(
|
||||
r: lapiauth.AuthServiceJWTPayload,
|
||||
claims: Request.Claims,
|
||||
): Boolean = {
|
||||
val (precond, userId) = r match {
|
||||
case tp: lapiauth.CustomDamlJWTPayload =>
|
||||
(
|
||||
(tp.admin || !claims.admin) &&
|
||||
Party
|
||||
.unsubst(claims.actAs)
|
||||
.toSet
|
||||
.subsetOf(tp.actAs.toSet) &&
|
||||
Party
|
||||
.unsubst(claims.readAs)
|
||||
.toSet
|
||||
.subsetOf(tp.readAs.toSet ++ tp.actAs),
|
||||
tp.applicationId,
|
||||
)
|
||||
case tp: lapiauth.StandardJWTPayload =>
|
||||
// NB: in this mode we check the applicationId claim (if supplied)
|
||||
// and ignore everything else
|
||||
(true, Some(tp.userId))
|
||||
}
|
||||
precond && ((claims.applicationId, userId) match {
|
||||
// No requirement on app id
|
||||
case (None, _) => true
|
||||
// Token valid for all app ids.
|
||||
case (_, None) => true
|
||||
case (Some(expectedAppId), Some(actualAppId)) => expectedAppId == ApplicationId(actualAppId)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,10 +12,11 @@ import akka.http.scaladsl.model.headers.{Cookie, Location, `Set-Cookie`}
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import com.daml.auth.middleware.api.{Client, Request, Response}
|
||||
import com.daml.auth.middleware.api.Request.Claims
|
||||
import com.daml.auth.middleware.api.Tagged.{AccessToken, RefreshToken}
|
||||
import com.daml.jwt.JwtSigner
|
||||
import com.daml.jwt.domain.DecodedJwt
|
||||
import com.daml.ledger.api.auth.{AuthServiceJWTCodec, CustomDamlJWTPayload}
|
||||
import com.daml.ledger.api.auth.{AuthServiceJWTCodec, CustomDamlJWTPayload, StandardJWTPayload}
|
||||
import com.daml.ledger.api.refinements.ApiTypes
|
||||
import com.daml.ledger.api.refinements.ApiTypes.Party
|
||||
import com.daml.ledger.api.testing.utils.SuiteResourceManagementAroundAll
|
||||
@ -178,6 +179,19 @@ class TestMiddleware
|
||||
assert(result == None)
|
||||
}
|
||||
}
|
||||
|
||||
"accept user tokens" in {
|
||||
import com.daml.auth.middleware.oauth2.Server.rightsProvideClaims
|
||||
rightsProvideClaims(
|
||||
StandardJWTPayload("foo", None, None),
|
||||
Claims(
|
||||
admin = true,
|
||||
actAs = List(ApiTypes.Party("Alice")),
|
||||
readAs = List(ApiTypes.Party("Bob")),
|
||||
applicationId = Some(ApiTypes.ApplicationId("foo")),
|
||||
),
|
||||
) should ===(true)
|
||||
}
|
||||
}
|
||||
"the /login endpoint" should {
|
||||
"redirect and set cookie" in {
|
||||
|
@ -11,7 +11,12 @@ import akka.http.scaladsl.model.headers.Location
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import com.daml.jwt.JwtDecoder
|
||||
import com.daml.jwt.domain.Jwt
|
||||
import com.daml.ledger.api.auth.{AuthServiceJWTCodec, CustomDamlJWTPayload, StandardJWTPayload}
|
||||
import com.daml.ledger.api.auth.{
|
||||
AuthServiceJWTCodec,
|
||||
CustomDamlJWTPayload,
|
||||
AuthServiceJWTPayload,
|
||||
StandardJWTPayload,
|
||||
}
|
||||
import com.daml.ledger.api.refinements.ApiTypes.Party
|
||||
import com.daml.ledger.api.testing.utils.SuiteResourceManagementAroundAll
|
||||
import org.scalatest.wordspec.AsyncWordSpec
|
||||
@ -22,24 +27,18 @@ import scala.util.Try
|
||||
|
||||
class Test extends AsyncWordSpec with TestFixture with SuiteResourceManagementAroundAll {
|
||||
import Client.JsonProtocol._
|
||||
import Test._
|
||||
|
||||
private def readCustomDamlJWTTokenFromString(
|
||||
private def readJWTTokenFromString[A](
|
||||
serializedPayload: String
|
||||
): Try[CustomDamlJWTPayload] =
|
||||
AuthServiceJWTCodec.readFromString(serializedPayload).map {
|
||||
case _: StandardJWTPayload =>
|
||||
throw new UnsupportedOperationException(
|
||||
// TODO (i12388): make auth middlware work with user tokens
|
||||
"auth-middleware: user access tokens are not yet supported (https://github.com/digital-asset/daml/issues/12388)."
|
||||
)
|
||||
case payload: CustomDamlJWTPayload => payload
|
||||
}
|
||||
)(implicit A: Token[A]): Try[A] =
|
||||
AuthServiceJWTCodec.readFromString(serializedPayload).flatMap { t => Try(A.run(t)) }
|
||||
|
||||
private def requestToken(
|
||||
private def requestToken[A: Token](
|
||||
parties: Seq[String],
|
||||
admin: Boolean,
|
||||
applicationId: Option[String],
|
||||
): Future[Either[String, (CustomDamlJWTPayload, String)]] = {
|
||||
): Future[Either[String, (A, String)]] = {
|
||||
lazy val clientUri = Uri()
|
||||
.withAuthority(clientBinding.localAddress.getHostString, clientBinding.localAddress.getPort)
|
||||
val req = HttpRequest(
|
||||
@ -75,16 +74,16 @@ class Test extends AsyncWordSpec with TestFixture with SuiteResourceManagementAr
|
||||
e => Future.failed(new IllegalArgumentException(e.toString)),
|
||||
Future.successful(_),
|
||||
)
|
||||
payload <- Future.fromTry(readCustomDamlJWTTokenFromString(decodedJwt.payload))
|
||||
payload <- Future.fromTry(readJWTTokenFromString(decodedJwt.payload))
|
||||
} yield Right((payload, refreshToken))
|
||||
case Client.ErrorResponse(error) => Future(Left(error))
|
||||
}
|
||||
} yield result
|
||||
}
|
||||
|
||||
private def requestRefresh(
|
||||
private def requestRefresh[A: Token](
|
||||
refreshToken: String
|
||||
): Future[Either[String, (CustomDamlJWTPayload, String)]] = {
|
||||
): Future[Either[String, (A, String)]] = {
|
||||
lazy val clientUri = Uri()
|
||||
.withAuthority(clientBinding.localAddress.getHostString, clientBinding.localAddress.getPort)
|
||||
val req = HttpRequest(
|
||||
@ -108,18 +107,18 @@ class Test extends AsyncWordSpec with TestFixture with SuiteResourceManagementAr
|
||||
e => Future.failed(new IllegalArgumentException(e.toString)),
|
||||
Future.successful(_),
|
||||
)
|
||||
payload <- Future.fromTry(readCustomDamlJWTTokenFromString(decodedJwt.payload))
|
||||
payload <- Future.fromTry(readJWTTokenFromString(decodedJwt.payload))
|
||||
} yield Right((payload, refreshToken))
|
||||
case Client.ErrorResponse(error) => Future(Left(error))
|
||||
}
|
||||
} yield result
|
||||
}
|
||||
|
||||
private def expectToken(
|
||||
private def expectToken[A: Token](
|
||||
parties: Seq[String],
|
||||
admin: Boolean = false,
|
||||
applicationId: Option[String] = None,
|
||||
): Future[(CustomDamlJWTPayload, String)] =
|
||||
): Future[(A, String)] =
|
||||
requestToken(parties, admin, applicationId).flatMap {
|
||||
case Left(error) => fail(s"Expected token but got error-code $error")
|
||||
case Right(token) => Future(token)
|
||||
@ -130,12 +129,12 @@ class Test extends AsyncWordSpec with TestFixture with SuiteResourceManagementAr
|
||||
admin: Boolean = false,
|
||||
applicationId: Option[String] = None,
|
||||
): Future[String] =
|
||||
requestToken(parties, admin, applicationId).flatMap {
|
||||
requestToken[AuthServiceJWTPayload](parties, admin, applicationId).flatMap {
|
||||
case Left(error) => Future(error)
|
||||
case Right(_) => fail("Expected an error but got a token")
|
||||
}
|
||||
|
||||
private def expectRefresh(refreshToken: String): Future[(CustomDamlJWTPayload, String)] =
|
||||
private def expectRefresh[A: Token](refreshToken: String): Future[(A, String)] =
|
||||
requestRefresh(refreshToken).flatMap {
|
||||
case Left(error) => fail(s"Expected token but got error-code $error")
|
||||
case Right(token) => Future(token)
|
||||
@ -210,5 +209,24 @@ class Test extends AsyncWordSpec with TestFixture with SuiteResourceManagementAr
|
||||
assert(token.applicationId == None)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
object Test {
|
||||
final class Token[A](val run: AuthServiceJWTPayload => A) extends AnyVal
|
||||
|
||||
object Token extends TokenLow {
|
||||
implicit val custom: Token[CustomDamlJWTPayload] = new Token({
|
||||
case _: StandardJWTPayload =>
|
||||
throw new IllegalStateException(
|
||||
"auth-middleware: user access tokens are not expected here"
|
||||
)
|
||||
case payload: CustomDamlJWTPayload => payload
|
||||
})
|
||||
}
|
||||
|
||||
sealed abstract class TokenLow {
|
||||
implicit val any: Token[AuthServiceJWTPayload] = new Token(identity)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user