-
Notifications
You must be signed in to change notification settings - Fork 223
Session token Ecto Persistance
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.
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 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.