Skip to content

Commit 0bc1ea9

Browse files
authored
Endpoint Documentation: Describing Authentication Types (#3808)
* describing authentication types. * add sse server time example. * sfix and fmt. * skip importing local style package. * remove unused import. * fix compilation for scala 2.12.x
1 parent 391b2e9 commit 0bc1ea9

File tree

2 files changed

+276
-0
lines changed

2 files changed

+276
-0
lines changed

docs/reference/endpoint.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,30 @@ val endpoint3: Endpoint[Unit, Unit, ZNothing, Either[(Article, Header.Date), (Bo
402402
Endpoint(RoutePattern.GET / "resources")
403403
.outCodec(articleCodec | bookCodec)
404404
```
405+
406+
To model streaming responses, such as server-sent events or large file downloads, we can use ZIO Streams as the output type of the endpoint:
407+
408+
```scala mdoc:compile-only
409+
import zio.stream._
410+
411+
val sseEndpoint: Endpoint[Unit, Unit, ZNothing, ZStream[Any, Nothing, ServerSentEvent[String]], AuthType.None] =
412+
Endpoint(Method.GET / "server-time")
413+
.outStream[ServerSentEvent[String]](MediaType.text.`event-stream`)
414+
```
415+
416+
This endpoint describes a server-sent events stream that sends the current server time to the client.
417+
418+
<details>
419+
<summary><b>Full Implementation Showcase</b></summary>
420+
421+
```scala mdoc:passthrough
422+
import utils._
423+
424+
printSource("zio-http-example/src/main/scala/example/endpoint/SSEServerTimeExample.scala")
425+
```
426+
427+
</details>
428+
405429
## Describing Failures
406430

407431
For failure outputs, we can describe the output properties using the `Endpoint#outError*` methods. Let's see an example:
@@ -512,6 +536,139 @@ val endpoint: Endpoint[Int, (Int, Header.Authorization), BookNotFound | Authenti
512536
.orOutError[AuthenticationError](Status.Unauthorized)
513537
```
514538

539+
## Describing Authentication Types
540+
541+
Endpoints can specify authentication requirements using the `Endpoint#auth` method. ZIO HTTP supports several built-in authentication types and allows for custom authentication schemes. It can be `AuthType.None`, `AuthType.Basic`, `AuthType.Bearer`, `AuthType.Digest`, `AuthType.Custom`, or unions of these types.
542+
543+
For example, Basic authentication can be specified using `AuthType.Basic`:
544+
545+
```scala mdoc:invisible
546+
import zio.schema.{DeriveSchema, Schema}
547+
548+
case class Book(title: String, authors: List[String])
549+
object Book {
550+
implicit val schema = DeriveSchema.gen[Book]
551+
}
552+
```
553+
554+
```scala mdoc:compile-only
555+
import zio.http._
556+
import zio.http.endpoint._
557+
558+
val endpoint = Endpoint(Method.GET / "me" / "favorites" / "books")
559+
.out[List[Book]]
560+
.auth(AuthType.Basic)
561+
```
562+
563+
This describes an endpoint that requires the client to provide HTTP Basic authentication credentials in the `Authorization` header.
564+
565+
### Multiple Authentication Types
566+
567+
An endpoint can accept a union of multiple authentication types, for example:
568+
569+
```scala mdoc:compile-only
570+
import zio.http._
571+
import zio.http.endpoint._
572+
573+
val endpoint = Endpoint(Method.GET / "me" / "favorites" / "books")
574+
.out[List[Book]]
575+
.auth(AuthType.Basic | AuthType.Bearer)
576+
```
577+
578+
This endpoint accepts either `Basic` or `Bearer` authentication, providing flexibility for clients.
579+
580+
### Custom Authentication
581+
582+
For custom authentication schemes, use `AuthType.Custom` with an `HttpCodec`:
583+
584+
```scala mdoc:compile-only
585+
import zio.http._
586+
import zio.http.endpoint._
587+
import zio.http.codec.HttpCodec
588+
589+
val endpoint = Endpoint(Method.GET / PathCodec.string("user_id") / "favorites" / "books")
590+
.out[List[Book]]
591+
.auth(AuthType.Custom(HttpCodec.query[String]("token")))
592+
```
593+
594+
This endpoint uses a custom authentication scheme that extracts the authentication token from a query parameter.
595+
596+
### Working with Authentication Context
597+
598+
To extract and use authentication information in your handlers, use `HandlerAspect.customAuthProviding` to provide an authentication context:
599+
600+
```scala mdoc:compile-only
601+
import zio.http._
602+
import zio.http.endpoint._
603+
import zio.Config.Secret
604+
605+
case class AuthContext(username: String)
606+
607+
val authMiddleware = HandlerAspect.customAuthProviding[AuthContext] { request =>
608+
request.headers.get(Header.Authorization).flatMap {
609+
case Header.Authorization.Basic(username, password) if Secret(username.reverse) == password =>
610+
Some(AuthContext(username))
611+
case _ =>
612+
None
613+
}
614+
}
615+
616+
val endpoint = Endpoint(Method.GET / "me" / "favorites" / "books")
617+
.out[List[Book]]
618+
.auth(AuthType.Basic)
619+
620+
def favoriteBooks(username: String): Task[List[Book]] = ???
621+
622+
val routes = Routes(
623+
endpoint.implementHandler(
624+
handler((_: Unit) =>
625+
withContext((ctx: AuthContext) => favoriteBooks(ctx.username).orDie)
626+
)
627+
)
628+
) @@ authMiddleware
629+
```
630+
631+
The `customAuthProviding` middleware extracts authentication information from the request and provides it as context that can be accessed in handlers using `withContext`.
632+
633+
### Multiple Authentication with Context
634+
635+
You can support multiple authentication types in your middleware:
636+
637+
```scala mdoc:compile-only
638+
import zio.http._
639+
import zio.http.endpoint._
640+
import zio.Config.Secret
641+
642+
case class AuthContext(username: String)
643+
644+
val multiAuthMiddleware = HandlerAspect.customAuthProviding[AuthContext] { request =>
645+
request.headers.get(Header.Authorization).flatMap {
646+
case Header.Authorization.Basic(username, password)
647+
if Secret(username.reverse) == password =>
648+
Some(AuthContext(username))
649+
case Header.Authorization.Bearer(token)
650+
if token == Secret("admin-token") =>
651+
Some(AuthContext("admin"))
652+
case _ =>
653+
None
654+
}
655+
}
656+
657+
def favoriteBooks(username: String): Task[List[Book]] = ???
658+
659+
val endpoint = Endpoint(Method.GET / "me" / "favorites" / "books")
660+
.out[List[Book]]
661+
.auth(AuthType.Basic | AuthType.Bearer)
662+
663+
val routes = Routes(
664+
endpoint.implementHandler(
665+
handler((_: Unit) =>
666+
withContext((ctx: AuthContext) => favoriteBooks(ctx.username).orDie)
667+
)
668+
)
669+
) @@ multiAuthMiddleware
670+
```
671+
515672
## Transforming Endpoint Input/Output and Error Types
516673

517674
To transform the input, output, and error types of an endpoint, we can use the `Endpoint#transformIn`, `Endpoint#transformOut`, and `Endpoint#transformError` methods, respectively. Let's see an example:
@@ -535,6 +692,7 @@ In the above example, we mapped over the input type of the `endpoint` and transf
535692
The `transformOut` and `transformError` methods work similarly to the `transformIn` method.
536693

537694
## CodecConfig
695+
538696
The `CodecConfig` is injected when building any `Endpoint` API codecs. You can see this in the definition of `BinaryCodecWithSchema`:
539697

540698
```scala mdoc:compile-only
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//> using dep "dev.zio::zio:2.1.22"
2+
//> using dep "dev.zio::zio-http:3.5.1"
3+
4+
package example.endpoint
5+
6+
import java.time.LocalDateTime
7+
import java.time.format.DateTimeFormatter
8+
9+
import zio._
10+
11+
import zio.stream._
12+
13+
import zio.http._
14+
import zio.http.endpoint.AuthType.None
15+
import zio.http.endpoint._
16+
import zio.http.template2._
17+
18+
object SSEServerTimeExample extends ZIOAppDefault {
19+
20+
val sseEndpoint: Endpoint[Unit, Unit, ZNothing, ZStream[Any, Nothing, ServerSentEvent[String]], None] =
21+
Endpoint(Method.GET / "server-time")
22+
.outStream[ServerSentEvent[String]](MediaType.text.`event-stream`)
23+
24+
// Stream that emits current time every second
25+
val timeStream: ZStream[Any, Nothing, ServerSentEvent[String]] =
26+
ZStream.repeatWithSchedule(
27+
ServerSentEvent(DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now)),
28+
Schedule.fixed(1.second),
29+
)
30+
31+
val sseRoute: Route[Any, Nothing] =
32+
sseEndpoint.implementHandler(Handler.succeed(timeStream))
33+
34+
val pageRoute: Route[Any, Nothing] =
35+
Method.GET / Root -> handler {
36+
val page = html(
37+
head(
38+
meta(charset := "UTF-8"),
39+
meta(name := "viewport", content := "width=device-width, initial-scale=1.0"),
40+
titleAttr := "Server Time using SSE",
41+
zio.http.template2.style.inlineCss("""
42+
body {
43+
font-family: Arial, sans-serif;
44+
display: flex;
45+
justify-content: center;
46+
align-items: center;
47+
min-height: 100vh;
48+
margin: 0;
49+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
50+
}
51+
.container {
52+
text-align: center;
53+
background: white;
54+
padding: 3rem;
55+
border-radius: 20px;
56+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
57+
}
58+
h1 {
59+
color: #333;
60+
margin-bottom: 2rem;
61+
font-size: 2rem;
62+
}
63+
#time {
64+
font-size: 4rem;
65+
font-weight: bold;
66+
color: #667eea;
67+
font-family: 'Courier New', monospace;
68+
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
69+
}
70+
.status {
71+
margin-top: 1rem;
72+
font-size: 0.9rem;
73+
color: #666;
74+
}
75+
.connected {
76+
color: #10b981;
77+
}
78+
.disconnected {
79+
color: #ef4444;
80+
}
81+
""".stripMargin),
82+
),
83+
body(
84+
div(
85+
className := "container",
86+
h1("Server Time"),
87+
div(id := "time", "Connecting..."),
88+
div(className := "status", id := "status", "Establishing connection..."),
89+
),
90+
script.inlineJs(js"""
91+
const timeElement = document.getElementById('time');
92+
const statusElement = document.getElementById('status');
93+
94+
const eventSource = new EventSource('/server-time');
95+
96+
eventSource.onopen = function() {
97+
statusElement.textContent = 'Connected';
98+
statusElement.className = 'status connected';
99+
};
100+
101+
eventSource.onmessage = function(event) {
102+
timeElement.textContent = event.data;
103+
};
104+
105+
eventSource.onerror = function(error) {
106+
statusElement.textContent = 'Connection lost. Reconnecting...';
107+
statusElement.className = 'status disconnected';
108+
};
109+
""".stripMargin),
110+
),
111+
)
112+
Response.html(page)
113+
}
114+
115+
val routes: Routes[Any, Response] = Routes(pageRoute, sseRoute)
116+
117+
def run = Server.serve(routes).provide(Server.default)
118+
}

0 commit comments

Comments
 (0)