I have a full working web app with user authentication (sessions) and other capability that I am adding a JSON API to.
The first thing I’d like to allow is user login through the API.
I’m following this API guide: Building a JSON API in Elixir with Phoenix 1.4+
In searching, I found this thread that seems related, but really isn’t: [Programming Phoenix] How do sessions work in Programming Phoenix?
Here are the basics:
router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
###################################################################
#
# Pieplines
#
###################################################################
# ...
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
end
pipeline :api_auth do
plug :ensure_authenticated
end
###################################################################
#
# Scopes
#
###################################################################
# ...
scope "/api", MyAppWeb do
pipe_through :api
post "/login", SessionController, :api_create
end
scope "/api", MyAppWeb do
pipe_through [:api, :api_auth]
get "/logout", SessionController, :api_delete
get "/settings", SessionController, :api_settings
end
###################################################################
#
# Functions
#
###################################################################
defp ensure_authenticated(conn, _opts) do
current_user = get_session(conn, :current_user)
if current_user do
conn
else
conn
|> put_status(:unauthorized)
|> put_view(MyAppWeb.ErrorView)
|> render("401.json", message: "Unauthenticated user.")
|> halt
end
end
end
session_controller.ex
defmodule MyAppWeb.SessionController do
use MyAppWeb, :controller
alias MyApp.Repo
alias MyApp.Account.Authentication
plug :put_layout, "hero.html"
###################################################################
#
# Internal Controller Actions
#
###################################################################
# ...
###################################################################
#
# External (API) Controller Actions
#
###################################################################
def api_create(conn, params) do
case Authentication.login(params, Repo) do
{:ok, user} ->
conn
|> put_session(:current_user, user.id)
|> put_status(:ok)
|> put_view(MyAppWeb.SessionView)
|> render("login.json", user: user)
:error ->
conn
|> delete_session(:current_user)
|> put_status(:unauthorized)
|> put_view(MyAppWeb.ErrorView)
|> render("401.json", message: "Invalid credentials.")
end
end
def api_delete(conn, _) do
conn
|> delete_session(:current_user)
|> put_status(:ok)
|> put_view(MyAppWeb.SessionView)
|> render("logout.json", message: "Logged out.")
end
def api_settings(conn, _) do
conn
|> put_view(MyAppWeb.SessionView)
|> render("settings.json", message: "Settings!")
end
end
session_view.ex
defmodule MyAppWeb.SessionView do
use MyAppWeb, :view
alias MyAppWeb.SharedView
def render("login.json", %{user: user}) do
%{
data: %{
user: %{
id: user.id,
email: user.email
}
}
}
end
def render("logout.json", %{message: message}) do
%{data: %{detail: message}}
end
def render("settings.json", %{message: message}) do
%{data: %{detail: message}}
end
end
I may have left out some files you’d like to see. Let me know and I will add them.
I am first logging into the site using the API like this:
curl -H "Content-Type: application/json" -X POST -d '{"username":"bfenner","password":"PasswordPassword"}' http://localhost:4000/api/login
That returns me the correct JSON hash of:
{"data":{"user":{"email":"ben.fenner@myapp.com","id":1}}}
I should then be able to access routes behind the api_auth
check, and something like this should work:
curl -H "Content-Type: application/json" -X GET http://localhost:4000/api/settings -c cookies.txt -b cookies.txt -i
Instead of getting the expected JSON hash of {"data":{"detail":"Settings!"}}
I instead do not pass the api_auth
check and get a return of {"errors":{"detail":"Unauthenticated user."}}
.
I would expect the router pipeline plugs to fetch the correct session, then run it through the ensure_authenticated
function (which it does) but the authentication check fails, because it fetches what looks like an empty session.
What am I doing wrong?