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:
Stephen Compall 2022-02-04 11:29:10 -05:00 committed by GitHub
parent e3bae961cc
commit 6bdb901127
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 93 additions and 49 deletions

View File

@ -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)
})
}
}

View File

@ -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 {

View File

@ -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)
}
}