Authentication

Let people log in to your terminal app

Logging in means proving who the user is, then writing a session so the next command remembers them. Two flows cover most apps: a password prompt, and a browser handoff for OAuth or SSO. Both end the same way: calling put_current_user/1.

Password

Read the email out loud and the password hidden, then check them against whatever your app already uses to authenticate.

@desc "Log in with your email and password"
def login do
  email = gets("Email: ") |> String.trim()
  password = read_secret("Password: ")

  case MyApp.Accounts.authenticate(email, password) do
    {:ok, user} ->
      put_current_user(user)
      puts("Logged in as #{user.email}")

    :error ->
      warn("Invalid email or password")
  end
end

read_secret/1 keeps the password off the screen. MyApp.Accounts.authenticate/2 is your code, the same function your web login calls.

Browser

When login goes through OAuth, SSO, or a provider you don’t control, send the user to the browser and wait for your web app to hand a result back. The command runs in its own process, so it can sit in a receive while the user signs in.

Mint a one-time nonce, subscribe to a topic keyed on it, and open an authorize page:

@desc "Log in through your browser"
def login do
  nonce = Base.url_encode64(:crypto.strong_rand_bytes(16), padding: false)
  token = Phoenix.Token.sign(MyAppWeb.Endpoint, "cli login", nonce)
  Phoenix.PubSub.subscribe(MyApp.PubSub, "cli_login:#{nonce}")

  url = url(~p"/cli/login?#{[token: token]}")
  puts("Opening #{url}")
  Terminalwire.Server.Context.browser_launch(context(), url)

  receive do
    {:cli_login, user_id} ->
      put_current_user(MyApp.Accounts.get_user!(user_id))
      puts("Logged in")
  after
    :timer.minutes(5) -> warn("Login timed out. Try again.")
  end
end

The page is an ordinary authenticated route in your Phoenix app. The user signs in the usual way, approves the request, and the controller broadcasts their id back to the waiting command:

defmodule MyAppWeb.CliLoginController do
  use MyAppWeb, :controller

  plug :require_authenticated_user

  # GET /cli/login?token=… — show an "Authorize this terminal app?" page.
  def new(conn, %{"token" => token}) do
    render(conn, :new, token: token)
  end

  # POST /cli/login — the user approved; signal the command and confirm.
  def create(conn, %{"token" => token}) do
    case Phoenix.Token.verify(MyAppWeb.Endpoint, "cli login", token, max_age: 600) do
      {:ok, nonce} ->
        Phoenix.PubSub.broadcast(
          MyApp.PubSub,
          "cli_login:#{nonce}",
          {:cli_login, conn.assigns.current_user.id}
        )
        render(conn, :done)

      {:error, _} ->
        conn |> put_flash(:error, "This login link expired.") |> redirect(to: ~p"/")
    end
  end
end

The nonce ties the browser session to the command that started it, and the token expires in ten minutes. Nothing about the user crosses the wire except an id you signed, broadcast over PubSub to the process that’s waiting for it.