I have a standard password based verification that uses the default encode_and_sign/3
function provided by elixir:
user controller:
def create(conn, %{"user" => user_params}) do
user_params_full = Map.put(user_params, "distant", false)
with {:ok, %User{} = user} <- User.create_user(user_params_full) do
authorize_account(conn, user.email, user_params["password"])
end
end
def sign_in(conn, %{"email" => email, "password" => password}) do
authorize_account(conn, email, password)
end
defp authorize_account(conn, email, password) do
case GuardianUser.authenticate(email, password) do
{:ok, user, token} ->
conn
|> Plug.Conn.put_session(:user_id, user.id)
|> put_status(:ok)
|> render("show_token.json", token: token)
{:error, :unauthorized} ->
{:error, :unauthorized, "authentication failed"}
end
end
GuardianUser.authenticate
just checks that the email is correct and creates a new token with the default encode_and_sign
function.
Now what i want to add is another endpoint that is the callback used in a OIDC request, where i get a token and claims back after a successful authentication:
def callback(conn, %{"state" => returned_state, "code" => code}) do
# Retrieve the original state from the session
original_state = get_session(conn, :oidc_state)
if returned_state == original_state do
with {:ok, tokens} <- OpenIDConnect.fetch_tokens(:authelia, %{code: code}),
{:ok, claims} <- OpenIDConnect.verify(:authelia, tokens["id_token"]) do
# Process user information, e.g., create a session
email = claims["email"]
user = case Users.get_user_by_email(email) do
nil ->
# create user using data from scope and put it in the db
new_user = Users.create(etc. etc.)
existing_user ->
existing_user
end
# Now we call a custiom function in the guardian module like
# encode_and_sign_authelia(user,token) that puts the token in the
# guardian db with the relevant user id, lifetime, claims etc.
with {:ok, token, _claims} <- GuardianUser.encode_and_sign_authelia(user, token) do
conn
|> put_status(:created)
|> put_session(:user_id, user.id)
|> render("show_token.json", token: token)
end
else
err ->
# handle errors here
err
end
else
{:error, :unauthorized, "invalid state parameter"}
end
end
Doing things this way would allow the verify session plug of my pipeline to handle token verfication, and i wouldn’t need to change any of my endpoints. I can’t quite wrap my head around what encode_and_sign does exactly but i believe this could work.
For reference here is the (currently incomplete) GuardianUser module:
defmodule Backend24hWeb.Auth.GuardianUser do
use Guardian, otp_app: :backend24h
alias Backend24h.Users
def subject_for_token(%{id: id}, _claims) do
sub = to_string(id)
{:ok, sub}
end
def subject_for_token(_, _) do
{:error, :no_id_provided}
end
def resource_from_claims(%{"sub" => id}) do
case Users.get_user(id) do
nil -> {:error, :not_found}
resource -> {:ok, resource}
end
end
@spec authenticate(any(), any()) :: {:error, :unauthorized} | {:ok, any(), binary()}
def authenticate(email, password) do
case Users.get_user_by_email(email) do
nil -> {:error, :unauthorized}
user->
case user.distant do
false ->
case validate_password(password, user.password) do
true -> create_token(user)
false -> {:error, :unauthorized}
true ->
{:error, :unauthorized} # no support for distant users yet
end
end
end
end
defp validate_password(password, hash_password) do
Bcrypt.verify_pass(password, hash_password)
end
defp create_token(user) do
case user.distant do
false ->
with {:ok, token, _claims} <- encode_and_sign(user) do
{:ok, user, token}
end
true ->
{:error, :unauthorized, "token creation failed"} # no support for distant users yet
end
end
end