Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add docs related to auth when using Kino.Proxy #433

Merged
merged 4 commits into from
Jun 3, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions lib/kino/proxy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Kino.Proxy do
Plug.Conn.send_resp(conn, 200, "hello")
end

> #### Plug {: .info}
> #### Plug dependency {: .info}
>
> In order to use this feature, you need to add `:plug` as a dependency.

Expand All @@ -29,12 +29,10 @@ defmodule Kino.Proxy do
Using the proxy feature, we can use Livebook apps to build APIs.
For example, we could provide a data export endpoint:

data = <<...>>
token = "auth-token"

Kino.Proxy.listen(fn
%{path_info: ["export", "data"]} = conn ->
["Bearer " <> ^token] = Plug.Conn.get_req_header(conn, "authorization")
data = "some data"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, using bearer tokens may be the best example, since tokens are most common for APIs (and Kino.Proxy would most likely be used for APIs?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed.

I'll change from HTTP basic auth to HTTP Bearer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about leaving this example as simple as possible, with no auth. And the auth would be below, inside the admonition block.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. Keeping it simple is a good idea.


conn
|> Plug.Conn.put_resp_header("content-type", "application/csv")
Expand All @@ -46,8 +44,32 @@ defmodule Kino.Proxy do
|> Plug.Conn.send_resp(200, "use /export/data to get extract the report data")
end)

Once deployed as an app, the user would be able to export the data
Once deployed as an app, the API client would be able to export the data
by sending a request to `/apps/:slug/proxy/export/data`.

> #### Authentication {: .warning}
>
> The paths exposed by `Kino.Proxy` don't use the authentication mechanisms
> defined in your Livebook instance.
Comment on lines +52 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josevalim I started to wonder if we should put them in the /public namespace. The main downside is that it differs more from the base app/session path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can always add more routes in the future. I think we have a good starting set. I can also see it being used for coordinating tasks inside their own infrastructure, but still not exposing it to the world.

>
> If you need to authenticate requests, you should
> implement your own authentication mechanism. Here's a simple example.
>
> ```elixir
> Kino.Proxy.listen(fn conn ->
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josevalim what about this code example with a simple Bearer auth?

Kino.Proxy.listen(fn conn ->
  api_token = "my-secret-api-token"

  with ["Bearer " <> client_token] <- Plug.Conn.get_req_header(conn, "authorization"),
       true <- api_token == client_token do
    Plug.Conn.send_resp(conn, 200, "hello")
  else
    _ ->
      conn
      |> Plug.Conn.put_resp_header("www-authenticate", "Bearer")
      |> Plug.Conn.send_resp(401, "Unauthorized")
  end
end)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kino.Proxy.listen(fn conn ->
  api_token = "my-secret-api-token"

  case Plug.Conn.get_req_header(conn, "authorization") do
    ["Bearer " <> ^api_token] ->
      Plug.Conn.send_resp(conn, 200, "hello")
    _ ->
      conn
      |> Plug.Conn.put_resp_header("www-authenticate", "Bearer")
      |> Plug.Conn.send_resp(401, "Unauthorized")
  end
end)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the example above is unsafe, we want to use Plug.Crypto.secure_compare :( It may be worth keeping it as user:pass just for simplicity. Sorry for the back and forth. :(

> expected_token = "my-secret-api-token"
>
> with ["Bearer " <> user_token] <- Plug.Conn.get_req_header(conn, "authorization"),
> true <- Plug.Crypto.secure_compare(user_token, expected_token) do
> Plug.Conn.send_resp(conn, 200, "hello")
> else
> _ ->
> conn
> |> Plug.Conn.put_resp_header("www-authenticate", "Bearer")
> |> Plug.Conn.send_resp(401, "Unauthorized")
> end
> end)
> ```
"""

@doc """
Expand Down
Loading