Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
148 lines
4.8 KiB
Elixir
148 lines
4.8 KiB
Elixir
defmodule TarakanWeb.API.ClientAuthControllerTest do
|
|
use TarakanWeb.ConnCase, async: true
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Tarakan.Accounts.ApiCredentials
|
|
|
|
test "browser approval exchanges a device code for one scoped credential", %{conn: conn} do
|
|
start_response =
|
|
conn
|
|
|> post(~p"/api/client-auth/start", %{client_name: "Tarakan CLI on laptop"})
|
|
|> json_response(201)
|
|
|
|
assert start_response["device_code"] =~ "trkd_"
|
|
assert start_response["user_code"] =~ ~r/^[A-Z2-7]{4}-[A-Z2-7]{4}$/
|
|
|
|
assert start_response["verification_uri_complete"] =~
|
|
"/client/authorize/#{start_response["user_code"]}"
|
|
|
|
pending =
|
|
build_conn()
|
|
|> post(~p"/api/client-auth/exchange", %{device_code: start_response["device_code"]})
|
|
|> json_response(400)
|
|
|
|
assert pending == %{"error" => "authorization_pending"}
|
|
|
|
account = account_fixture()
|
|
|
|
{:ok, view, _html} =
|
|
build_conn()
|
|
|> log_in_account(account)
|
|
|> live(~p"/client/authorize/#{start_response["user_code"]}")
|
|
|
|
assert has_element?(view, "#client-authorization-code")
|
|
assert has_element?(view, "#client-authorization-approve-button")
|
|
|
|
view
|
|
|> element("#client-authorization-approve-button")
|
|
|> render_click()
|
|
|
|
assert has_element?(view, "#client-authorization-approved")
|
|
|
|
exchange_response =
|
|
build_conn()
|
|
|> post(~p"/api/client-auth/exchange", %{device_code: start_response["device_code"]})
|
|
|> json_response(200)
|
|
|
|
assert exchange_response["token"] =~ "trkn_"
|
|
assert exchange_response["token_type"] == "Bearer"
|
|
assert "tasks:read" in exchange_response["scopes"]
|
|
assert "tasks:claim" in exchange_response["scopes"]
|
|
assert "contributions:write" in exchange_response["scopes"]
|
|
assert "reviews:submit" in exchange_response["scopes"]
|
|
refute "reviews:verify" in exchange_response["scopes"]
|
|
refute "reviews:read" in exchange_response["scopes"]
|
|
|
|
assert {:ok, ^account, credential} = ApiCredentials.authenticate(exchange_response["token"])
|
|
assert credential.name == "Tarakan CLI on laptop"
|
|
|
|
# Device-minted credentials are short-lived (agent blast-radius control).
|
|
expires_in_days = DateTime.diff(credential.expires_at, DateTime.utc_now(), :day)
|
|
assert expires_in_days in 6..7
|
|
|
|
consumed =
|
|
build_conn()
|
|
|> post(~p"/api/client-auth/exchange", %{device_code: start_response["device_code"]})
|
|
|> json_response(400)
|
|
|
|
assert consumed == %{"error" => "invalid_device_code"}
|
|
|
|
revoke_conn =
|
|
build_conn()
|
|
|> put_req_header("authorization", "Bearer #{exchange_response["token"]}")
|
|
|> delete(~p"/api/client-auth/session")
|
|
|
|
assert response(revoke_conn, 204) == ""
|
|
assert :error = ApiCredentials.authenticate(exchange_response["token"])
|
|
end
|
|
|
|
test "the browser can deny a login without issuing a credential", %{conn: conn} do
|
|
start_response =
|
|
conn
|
|
|> post(~p"/api/client-auth/start", %{})
|
|
|> json_response(201)
|
|
|
|
{:ok, view, _html} =
|
|
build_conn()
|
|
|> log_in_account(account_fixture())
|
|
|> live(~p"/client/authorize/#{start_response["user_code"]}")
|
|
|
|
view
|
|
|> element("#client-authorization-deny-button")
|
|
|> render_click()
|
|
|
|
assert has_element?(view, "#client-authorization-denied")
|
|
|
|
denied =
|
|
build_conn()
|
|
|> post(~p"/api/client-auth/exchange", %{device_code: start_response["device_code"]})
|
|
|> json_response(403)
|
|
|
|
assert denied == %{"error" => "access_denied"}
|
|
end
|
|
|
|
test "authorization page requires a signed-in web account", %{conn: conn} do
|
|
start_response =
|
|
conn
|
|
|> post(~p"/api/client-auth/start", %{})
|
|
|> json_response(201)
|
|
|
|
assert {:error, {:redirect, %{to: path}}} =
|
|
live(build_conn(), ~p"/client/authorize/#{start_response["user_code"]}")
|
|
|
|
assert path == ~p"/accounts/log-in"
|
|
end
|
|
|
|
test "a signed-in session can approve without recent reauth", %{conn: conn} do
|
|
start_response =
|
|
conn
|
|
|> post(~p"/api/client-auth/start", %{})
|
|
|> json_response(201)
|
|
|
|
return_to = ~p"/client/authorize/#{start_response["user_code"]}"
|
|
stale_at = DateTime.add(DateTime.utc_now(:second), -9 * 60, :minute)
|
|
|
|
{:ok, view, _html} =
|
|
build_conn()
|
|
|> log_in_account(account_fixture(), token_authenticated_at: stale_at)
|
|
|> live(return_to)
|
|
|
|
assert has_element?(view, "#client-authorization-approve-button")
|
|
|
|
view
|
|
|> element("#client-authorization-approve-button")
|
|
|> render_click()
|
|
|
|
assert has_element?(view, "#client-authorization-approved")
|
|
end
|
|
|
|
test "invalid device codes fail without revealing authorization state", %{conn: conn} do
|
|
response =
|
|
conn
|
|
|> post(~p"/api/client-auth/exchange", %{device_code: "not-a-device-code"})
|
|
|> json_response(400)
|
|
|
|
assert response == %{"error" => "invalid_device_code"}
|
|
end
|
|
end
|