Implement POST /hostapi/auth/token

This commit is contained in:
Nat 2023-06-07 10:12:33 -07:00
parent 2a406ef236
commit 1468d21c57
Signed by: nat
GPG Key ID: B53AB05285D710D6
8 changed files with 104 additions and 3 deletions

View File

@ -28,3 +28,7 @@ config :logger, level: :warning
# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime
# Decrease the number of rounds used to encrypt passwords and whatnot
# to improve performance
config :bcrypt_elixir, :log_rounds, 4

View File

@ -18,5 +18,13 @@ defmodule Hostas.Denizen do
denizen
|> cast(attrs, [:name, :handle, :password])
|> validate_required([:name, :handle, :password])
|> unique_constraint(:handle)
|> hash_password
end
# Hash the password
defp hash_password(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do
change(changeset, Bcrypt.add_hash(password))
end
defp hash_password(changeset), do: changeset
end

View File

@ -5,6 +5,7 @@ defmodule Hostas.Token do
schema "tokens" do
field :denizen_id, :integer
field :expires, :utc_datetime
field :token, :string
# Link tokens to denizens
belongs_to :denizens, Hostas.Denizen

View File

@ -1,7 +1,79 @@
defmodule HostasWeb.Auth.TokenController do
import Ecto.Query, only: [from: 2]
use HostasWeb, :controller
def index(_conn, _params) do
alias Hostas.Repo
alias Hostas.Denizen
alias Hostas.Token
@doc """
Generates an API token. Responds with the token if the user
provides the correct password
"""
def create(conn, %{"handle" => handle, "password" => given_password}) do
case Repo.one(from d in Denizen,
where: d.handle == ^handle,
select: %{id: d.id, password: d.password}) do
nil ->
conn
|> put_status(404)
|> json(%{"error" => "No user with handle #{handle}"})
denizen ->
%{id: denizen_id, password: real_password_hash} = denizen
if Bcrypt.verify_pass(given_password, real_password_hash) do
# Create a random token
token = Base.encode64(:crypto.strong_rand_bytes(256))
# Calculate when the token should expire
{:ok, time_now} = DateTime.now("Etc/UTC")
expiry = DateTime.add(time_now, 30, :day)
|> DateTime.truncate(:second)
# Register the token
{:ok, token_struct} = Repo.insert(
%Token{denizen_id: denizen_id, token: token, expires: expiry})
conn
|> put_status(201)
|> json(Map.take(token_struct, [:token, :expires]))
else
# Reject the request, as passwords don't match
conn
|> put_status(401)
|> json(%{"error" => "Password mismatch"})
end
end
end
def create(conn, params) do
conn
|> put_status(422)
|> json(params)
# |> json(%{"error" => "Missing required parameters"})
end
@doc """
Responds with 200 OK if the requester's `Bearing` header
contains a valid, non-expired API token
"""
def verify(_conn, _params) do
:ok
end
@doc """
Deletes the token the requester used in the `Bearing` header
"""
def revoke(_conn, _params) do
:ok
end
@doc """
Deletes the token the requester used in the `Bearing` header
and responds with a new one if the old one was valid and unexpired
"""
def renew(_conn, _params) do
:ok
end
end

View File

@ -23,7 +23,11 @@ defmodule HostasWeb.Router do
scope "/hostapi/", HostasWeb do
pipe_through :api
resources "/auth/token", Auth.TokenController
# create, verify, renew, revoke
post "/auth/token", Auth.TokenController, :create
get "/auth/token", Auth.TokenController, :verify
delete "/auth/token", Auth.TokenController, :revoke
get "/auth/token/renew", Auth.TokenController, :renew
end
# Enable LiveDashboard and Swoosh mailbox preview in development

View File

@ -49,7 +49,8 @@ defmodule Hostas.MixProject do
{:telemetry_poller, "~> 1.0"},
{:gettext, "~> 0.20"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"}
{:plug_cowboy, "~> 2.5"},
{:bcrypt_elixir, "~> 3.0"}
]
end

View File

@ -1,6 +1,8 @@
%{
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.7", "77de20ac77f0e53f20ca82c563520af0237c301a1ec3ab3bc598e8a96c7ee5d9", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2768b28bf3c2b4f788c995576b39b8cb5d47eb788526d93bd52206c1d8bf4b75"},
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},

View File

@ -0,0 +1,9 @@
defmodule Hostas.Repo.Migrations.AddTokenField do
use Ecto.Migration
def change do
alter table(:tokens) do
add :token, :string
end
end
end