Skip to content

Session token Ecto Persistance

Fabian Jahr edited this page Apr 27, 2018 · 4 revisions

The Coherence.Authentication.Session plug supports persisting the credentials in a database through the Coherence.DbStore protocol.

With this implemented, verification of a logged in user's credentials is first done against the in memory Agent store. If the credentials are found in the Agent, nothing else if required. If the credentials are not found, then the database is checked for the user. So, the database is accessed on login, logout, and if the Agent data is lost (server restart).

The following example is from a project that uses my plug_auth package, before I moved it over to Coherence. So, I have not tested this example on Coherence, but expect that is should work. Please feel free to update this wiki entry if you try this and have verified that it works/does not work.

Setup

Implement the Coherence.DbStore protocol somewhere in your project.

# lib/my_project/db_store.ex
defimpl Coherence.DbStore, for: MyProject.User do
  alias MyProject.Repo
  alias MyProject.EctoDbSession

  def get_user_data(user, creds, id_key), 
    do: EctoDbSession.get_user_data(Repo, user, creds, id_key)

  def put_credentials(user, creds, id_key), 
    do: EctoDbSession.put_credentials(Repo, user, creds, id_key)

  def delete_credentials(user, creds), 
    do: EctoDbSession.delete_credentials(user, creds)
end

Create a module to handle the protocol.

# lib/my_project/ecto_db_session.ex
defmodule MyProject.EctoDbSession do
  require Logger
  import Ecto.Query

  @session_model Application.get_env(:coherence, :session_model)
  @session_repo  Application.get_env(:coherence, :session_repo)
 
  def get_user_data(repo, user, creds, id_key) do 
    @session_model
    |> where([s], s.token == ^creds)
    |> @session_repo.one
    |> case do
      nil -> nil
      session -> 
        user_id = get_id user, id_key, session.user_id

        session.user_type
        |> String.to_atom
        |> where([u], field(u, ^id_key) == ^user_id)
        |> repo.one
    end
  end

  def put_credentials(_repo, user, creds, id_key) do 
    id_str = "#{Map.get user, id_key}"
    params = %{
      token: creds, 
      user_type: Atom.to_string(user.__struct__), 
      user_id: id_str
    }

    where(@session_model, [s], s.user_id == ^id_str)
    |> @session_repo.delete_all

    @session_model.changeset(@session_model.__struct__, params) 
    |> @session_repo.insert
    |> case do
      {:ok, _} -> :ok
      {:error, changeset} -> {:error, changeset}
    end
  end

  def delete_credentials(_user, creds) do
    @session_model
    |> where([s], s.token == ^creds)
    |> @session_repo.one 
    |> case do
      nil -> 
        nil
      user -> 
        @session_repo.delete user
    end
  end

  # handle converting the users id into correct model type
  defp get_id(user, id_key, user_id) do
    case user.__struct__.__schema__(:type, id_key) do
      int when int in [:integer, :id] -> 
        String.to_integer user_id
      :string ->
        user_id
    end
  end
end

Create a migration for the Session table

# priv/repo/migrations/xxxxxxxx_create_session.exs
defmodule MyProject.Repo.Migrations.CreateSession do
  use Ecto.Migration

  def up do
    create table(:sessions) do
      add :token, :string, unique: true
      add :user_type, :string
      add :user_id, :string

      timestamps
    end

    create unique_index(:sessions, [:token])
    create index(:sessions, [:user_id])
  end

  def down do
    drop table(:sessions) 
  end
end

Create a schema for the session.

# web/models/session.ex
defmodule MyProject.Session do
  use Ecto.Schema

  import Ecto.Changeset

  schema "sessions" do
    field :token, :string
    field :user_type, :string
    field :user_id, :string
    timestamps
  end

  @fields ~w(token user_type user_id)a

  def changeset(model, params \\ %{}) do
    model
    |> cast(params, @fields)
    |> validate_required(@fields)
    |> unique_constraint(:token)
  end
end

Configure the router.

defmodule MyProjectWeb.Router do
  use MyProjectWeb, :router
  use Coherence.Router

  @user_schema Application.get_env(:coherence, :user_schema)
  @id_key Application.get_env(:coherence, :schema_key)

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug Coherence.Authentication.Session,
      store: Coherence.CredentialStore.Session,
      db_model: @user_schema,
      id_key: @id_key
  end

  pipeline :protected do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug Coherence.Authentication.Session,
      protected: true,
      store: Coherence.CredentialStore.Session,
      db_model: @user_schema,
      id_key: @id_key
  end

  # ...

end

Finally, check if you need to add missing keys to your config.

config :coherence,
  session_model: MyProject.Session,
  session_repo: MyProject.Repo,
  schema_key: :id

Token authentication

Token authentication expects a different function signature for get_user_data, so we'll have to do some extra setup.

Create a module to handle token authentication somewhere in your app.

# lib/my_project/token_db_store.ex
defmodule MyProject.TokenDbStore do
  @user_schema Application.get_env(:coherence, :user_schema)
  @id_key Application.get_env(:coherence, :schema_key)
  
  def get_user_data(creds),
    do: Coherence.CredentialStore.Session.get_user_data({creds, @user_schema, @id_key})
end

Add it to the router.

# lib/my_project_web/router.ex
defmodule MyProject.Router do
  use MyProject, :router
  use Coherence.Router

  # ...

  pipeline :protected_api do
    plug :accepts, ["json"]
    plug Coherence.Authentication.Token,
      protected: true,
      store: MyProject.TokenDbStore,
      source: :header,
      param: "x-auth-token"

  # ...

end

Finally, create new controllers and views for token auth as mentioned in https://github.com/smpallen99/coherence/issues/173, making sure to persist created tokens using Coherence.CredentialStore.Session. Note that the example may not be functional with recent versions of Phoenix.