Using both a password based and OpenId based authentication using Guardian

Issue:

I’m currently trying to implement support for openID authentication using guardian for a RESTful api.

The api i’m currently working with supports authentication with a password stored as a hash on the api’s db. I’d like to add another option to login using openID through authelia.

All users have a :distant, :boolean field used to denote wether a user authenticates through a password in the db or authelia. I want to make it so that guardian generates a token itself only when a user is not distant. If it is distant i want instead for guardian to store the token given by authelia, and use that token afterwards. That way i can use the protected endpoints of my project (based on the EnsureAuthenticated Plug) without any modifications

Goal:

I want to modify the way guardian creates and verifies tokens to switch between verifying the password itself or letting authelia handle verifications/token creation.
I want the resulting token to be used transparently by the EnsureAuthneicated Guardian plug so that i don’t have to modifiy the endpoints of my project.

I don’t fully understand the authentication life cycle of the phoenix connection, so i don’t know where to start.

Current pipeline

This is the pipeline is use for authentication. ideally i’d like to only modify this part so that changes are transparent for the rest of the code (apart from the new routes needed to authenticate through authelia ofc.)

defmodule Backend24hWeb.Auth.SwimmerPipeline do
  use Guardian.Plug.Pipeline, otp_app: :backend24,
  module: Backend24hWeb.Auth.GuardianNageur,
  error_handler: Backend24hWeb.Auth.GuardianErrorHandler

  plug Guardian.Plug.VerifySession
  plug Guardian.Plug.VerifyHeader
  plug Guardian.Plug.EnsureAuthenticated
  plug Guardian.Plug.LoadResource


end

Thanks in advance.

I understand you’re trying to integrate Authelia OpenID authentication alongside your existing Guardian password-based auth. Based on your description, there are a few key areas you might want to focus on:

  1. Consider where in Guardian’s authentication lifecycle you need to intervene. Since you mentioned wanting to handle tokens differently based on the :distant flag, you might want to look into Guardian’s token creation and verification callbacks.
  2. Take a look at Guardian’s documentation about implementing custom token types or encoding strategies. This could help you understand how to make Guardian work with Authelia’s tokens when needed.
  3. The pipeline you shared looks standard - but since Guardian is modular, you can likely achieve what you want by implementing the right callbacks in your Guardian module rather than modifying the pipeline itself.

Would you mind sharing what you’ve tried so far in terms of Guardian customization? That would help me give more specific guidance.

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

After verifying the data from the callback and retrieving or creating the user, you can use the create_token function in GuardianUser.

Then, use the token and user to set the necessary data, and return JSON in a similar way to the authorize_account function in the user controller.

I hope this helps you get started on resolving the issue. I’m on mobile and unable to provide a code example this week, but feel free to reach out if you have any questions!