From 0f7ccbaebd3f08ca4906e5a3696486a7ab71d8b1 Mon Sep 17 00:00:00 2001 From: Eddy Oyieko <67474838+mobley-trent@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:10:39 +0000 Subject: [PATCH 1/2] initial commit --- .../scala/zio/http/gen/scala/CodeGen.scala | 3 ++ .../src/main/scala/zio/http/Header.scala | 47 +++++++++++++++++- .../scala/zio/http/codec/HeaderCodecs.scala | 11 +++-- .../scala/zio/http/endpoint/AuthType.scala | 6 +-- .../scala/zio/http/endpoint/Endpoint.scala | 6 +-- .../zio/http/endpoint/http/HttpFile.scala | 19 +++++++- .../zio/http/endpoint/http/HttpGenSpec.scala | 48 +++++++++++++++++++ 7 files changed, 126 insertions(+), 14 deletions(-) diff --git a/zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala b/zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala index bda36f4f79..afccd4462e 100644 --- a/zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala +++ b/zio-http-gen/src/main/scala/zio/http/gen/scala/CodeGen.scala @@ -318,6 +318,9 @@ object CodeGen { case "age" => "HeaderCodec.age" case "allow" => "HeaderCodec.allow" case "authorization" => "HeaderCodec.authorization" + case "basicAuth" => "HeaderCodec.basicAuth" + case "bearerAuth" => "HeaderCodec.bearer" + case "digestAuth" => "HeaderCodec.digestAuth" case "cache-control" => "HeaderCodec.cacheControl" case "clear-site-data" => "HeaderCodec.clearSiteData" case "connection" => "HeaderCodec.connection" diff --git a/zio-http/shared/src/main/scala/zio/http/Header.scala b/zio-http/shared/src/main/scala/zio/http/Header.scala index bccf90fde9..bda762d016 100644 --- a/zio-http/shared/src/main/scala/zio/http/Header.scala +++ b/zio-http/shared/src/main/scala/zio/http/Header.scala @@ -1030,8 +1030,21 @@ object Header { final case class Basic(username: String, password: Secret) extends Authorization - object Basic { + object Basic extends HeaderType { def apply(username: String, password: String): Basic = new Basic(username, Secret(password)) + + override def name: String = "basicAuth" + + override type HeaderValue = Authorization.Basic + + def parse(value: String): Either[String, Authorization.Basic] = { + parseBasic(value).asInstanceOf[Either[String, Authorization.Basic]] + } + + def render(header: Authorization.Basic): String = { + val encoded = Base64.getEncoder.encodeToString((header.username + ":" + header.password.value).getBytes) + s"Basic $encoded" + } } final case class Digest( @@ -1048,10 +1061,40 @@ object Header { userhash: Boolean, ) extends Authorization + object Digest extends HeaderType { + override def name: String = "digestAuth" + + override type HeaderValue = Authorization.Digest + + def parse(value: String): Either[String, Authorization.Digest] = { + parseDigest(value).asInstanceOf[Either[String, Authorization.Digest]] + } + + def render(header: Digest): String = { + s"""Digest response="${header.response}",username="${header.username}",realm="${header.realm}",uri=${header.uri.toString},opaque="${header.opaque}",algorithm=${header.algorithm},""" + + s"""qop=${header.qop},cnonce="${header.cnonce}",nonce="${header.nonce}",nc=${header.nc},userhash=${header.userhash.toString}""" + } + } + final case class Bearer(token: Secret) extends Authorization - object Bearer { + object Bearer extends HeaderType { def apply(token: String): Bearer = Bearer(Secret(token)) + + override def name: String = "bearerAuth" + + override type HeaderValue = Authorization.Bearer + + def parse(value: String): Either[String, Authorization.Bearer] = { + val parts = value.split(" ").filter(_.nonEmpty) + if (parts.length == 2) { + Right(Bearer(Secret(parts(1)))) + } else { + Left("Invalid Bearer Authorization header value") + } + } + + def render(header: Authorization.Bearer): String = s"Bearer ${header.token.value}" } final case class Unparsed(authScheme: String, authParameters: Secret) extends Authorization diff --git a/zio-http/shared/src/main/scala/zio/http/codec/HeaderCodecs.scala b/zio-http/shared/src/main/scala/zio/http/codec/HeaderCodecs.scala index a6ced4dec4..c0b362362a 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/HeaderCodecs.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/HeaderCodecs.scala @@ -83,10 +83,13 @@ private[codec] trait HeaderCodecs { final val age: HeaderCodec[Header.Age] = header(Header.Age) final val allow: HeaderCodec[Header.Allow] = header(Header.Allow) final val authorization: HeaderCodec[Header.Authorization] = header(Header.Authorization) - final val cacheControl: HeaderCodec[Header.CacheControl] = header(Header.CacheControl) - final val clearSiteData: HeaderCodec[Header.ClearSiteData] = header(Header.ClearSiteData) - final val connection: HeaderCodec[Header.Connection] = header(Header.Connection) - final val contentBase: HeaderCodec[Header.ContentBase] = header(Header.ContentBase) + final val basicAuth: HeaderCodec[Header.Authorization.Basic] = header(Header.Authorization.Basic) + final val bearerAuth: HeaderCodec[Header.Authorization.Bearer] = header(Header.Authorization.Bearer) + final val digestAuth: HeaderCodec[Header.Authorization.Digest] = header(Header.Authorization.Digest) + final val cacheControl: HeaderCodec[Header.CacheControl] = header(Header.CacheControl) + final val clearSiteData: HeaderCodec[Header.ClearSiteData] = header(Header.ClearSiteData) + final val connection: HeaderCodec[Header.Connection] = header(Header.Connection) + final val contentBase: HeaderCodec[Header.ContentBase] = header(Header.ContentBase) final val contentEncoding: HeaderCodec[Header.ContentEncoding] = header(Header.ContentEncoding) final val contentLanguage: HeaderCodec[Header.ContentLanguage] = header(Header.ContentLanguage) final val contentLength: HeaderCodec[Header.ContentLength] = header(Header.ContentLength) diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/AuthType.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/AuthType.scala index c97051fcaf..93d5033d55 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/AuthType.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/AuthType.scala @@ -31,18 +31,18 @@ object AuthType { case object Basic extends AuthType { type ClientRequirement = Header.Authorization.Basic override val codec: HeaderCodec[Header.Authorization.Basic] = - HeaderCodec.authorization.asInstanceOf[HeaderCodec[Header.Authorization.Basic]] + HeaderCodec.basicAuth } case object Bearer extends AuthType { type ClientRequirement = Header.Authorization.Bearer override val codec: HeaderCodec[Header.Authorization.Bearer] = - HeaderCodec.authorization.asInstanceOf[HeaderCodec[Header.Authorization.Bearer]] + HeaderCodec.bearerAuth } case object Digest extends AuthType { type ClientRequirement = Header.Authorization.Digest override val codec: HeaderCodec[Header.Authorization.Digest] = - HeaderCodec.authorization.asInstanceOf[HeaderCodec[Header.Authorization.Digest]] + HeaderCodec.digestAuth } final case class Custom[ClientReq](override val codec: HttpCodec[HttpCodecType.RequestType, ClientReq]) diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala index 3eab47c46c..162ad11bf6 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala @@ -261,21 +261,21 @@ final case class Endpoint[PathInput, Input, Err, Output, Auth <: AuthType]( def authCodec(authType: AuthType): HttpCodec[HttpCodecType.RequestType, Unit] = authType match { case AuthType.None => HttpCodec.empty case AuthType.Basic => - HeaderCodec.authorization.transformOrFail { + HeaderCodec.basicAuth.transformOrFail { case Header.Authorization.Basic(_, _) => Right(()) case _ => Left("Basic auth required") } { case () => Left("Unsupported") } case AuthType.Bearer => - HeaderCodec.authorization.transformOrFail { + HeaderCodec.bearerAuth.transformOrFail { case Header.Authorization.Bearer(_) => Right(()) case _ => Left("Bearer auth required") } { case () => Left("Unsupported") } case AuthType.Digest => - HeaderCodec.authorization.transformOrFail { + HeaderCodec.digestAuth.transformOrFail { case _: Header.Authorization.Digest => Right(()) case _ => Left("Digest auth required") } { case () => diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/http/HttpFile.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/http/HttpFile.scala index 9537e71e60..2858352a69 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/http/HttpFile.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/http/HttpFile.scala @@ -69,7 +69,17 @@ final case class HttpEndpoint( private def renderHeaders = if (headers.isEmpty) "" - else headers.map(h => s"${h.capitalize}: {{${h.capitalize}}}").mkString("\n", "\n", "") + else + headers + .map(h => + h.toLowerCase() match { + case "basicauth" | "bearerauth" | "digestauth" => + s"Authorization: {{${h.capitalize}}}" + case _ => + s"${h.capitalize}: {{${h.capitalize}}}" + }, + ) + .mkString("\n", "\n", "") private def renderPath = { if (method == Method.ANY) { @@ -82,7 +92,12 @@ final case class HttpEndpoint( final case class HttpVariable(name: String, value: Option[String], docString: Option[String] = None) { def render = { - val variable = s"@$name=${value.getOrElse("")}" + val variable = name.toLowerCase() match { + case "basicauth" | "bearerauth" | "digestauth" => + s"@Authorization=${value.getOrElse("")}" + case _ => + s"@$name=${value.getOrElse("")}" + } if (docString.isDefined) { docString.get.split("\n").map(line => s"# $line").mkString("", "\n", "\n") + variable } else variable diff --git a/zio-http/shared/src/test/scala/zio/http/endpoint/http/HttpGenSpec.scala b/zio-http/shared/src/test/scala/zio/http/endpoint/http/HttpGenSpec.scala index b7e9a582f6..3497e604b8 100644 --- a/zio-http/shared/src/test/scala/zio/http/endpoint/http/HttpGenSpec.scala +++ b/zio-http/shared/src/test/scala/zio/http/endpoint/http/HttpGenSpec.scala @@ -85,6 +85,54 @@ object HttpGenSpec extends ZIOSpecDefault { |Authorization: {{Authorization}}""".stripMargin assertTrue(rendered == expected) }, + test("Basic Auth Header Codec") { + val endpoint = Endpoint(Method.GET / "api" / "foo").header(HeaderCodec.basicAuth) + val httpEndpoint = HttpGen.fromEndpoint(endpoint) + val rendered = httpEndpoint.render + val expected = + """ + |@Authorization= + | + |GET /api/foo + |Authorization: {{BasicAuth}}""".stripMargin + assertTrue(rendered == expected) + + val endpointWithExample = Endpoint(Method.GET / "api" / "foo") + .header(HeaderCodec.basicAuth.examples("default" -> Header.Authorization.Basic("admin", "admin"))) + val httpEndpointWithExample = HttpGen.fromEndpoint(endpointWithExample) + val renderedWithExample = httpEndpointWithExample.render + val expectedWithExample = + """ + |@Authorization=Basic YWRtaW46Q2h1bmsoYSxkLG0saSxuKQ== + | + |GET /api/foo + |Authorization: {{BasicAuth}}""".stripMargin + assertTrue(renderedWithExample == expectedWithExample) + }, + test("Bearer Auth Header Codec") { + val endpoint = Endpoint(Method.GET / "api" / "foo").header(HeaderCodec.bearerAuth) + val httpEndpoint = HttpGen.fromEndpoint(endpoint) + val rendered = httpEndpoint.render + val expected = + """ + |@Authorization= + | + |GET /api/foo + |Authorization: {{BearerAuth}}""".stripMargin + assertTrue(rendered == expected) + }, + test("Digest Auth Header Codec") { + val endpoint = Endpoint(Method.GET / "api" / "foo").header(HeaderCodec.digestAuth) + val httpEndpoint = HttpGen.fromEndpoint(endpoint) + val rendered = httpEndpoint.render + val expected = + """ + |@Authorization= + | + |GET /api/foo + |Authorization: {{DigestAuth}}""".stripMargin + assertTrue(rendered == expected) + }, test("Header with example") { val endpoint = Endpoint(Method.GET / "api" / "foo") .header(HeaderCodec.authorization.examples("default" -> Header.Authorization.Basic("admin", "admin"))) From a9a67e875213b6712b6c5a7a3a92dc837e071a0d Mon Sep 17 00:00:00 2001 From: Eddy Oyieko <67474838+mobley-trent@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:56:43 +0000 Subject: [PATCH 2/2] fix: Pattern match error in Endpoint.scala --- .../shared/src/main/scala/zio/http/endpoint/Endpoint.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala index 162ad11bf6..0760112736 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala @@ -263,21 +263,21 @@ final case class Endpoint[PathInput, Input, Err, Output, Auth <: AuthType]( case AuthType.Basic => HeaderCodec.basicAuth.transformOrFail { case Header.Authorization.Basic(_, _) => Right(()) - case _ => Left("Basic auth required") + case null => Left("Basic auth required") } { case () => Left("Unsupported") } case AuthType.Bearer => HeaderCodec.bearerAuth.transformOrFail { case Header.Authorization.Bearer(_) => Right(()) - case _ => Left("Bearer auth required") + case null => Left("Bearer auth required") } { case () => Left("Unsupported") } case AuthType.Digest => HeaderCodec.digestAuth.transformOrFail { case _: Header.Authorization.Digest => Right(()) - case _ => Left("Digest auth required") + case null => Left("Digest auth required") } { case () => Left("Unsupported") }