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.