Pow: Robust, modular, extendable user authentication and management system

authentication
authorization
user_system
phoenix
pow
#82

I’m just a bit curious. ^^
Is it possible (or could it be) to pass a list of fields to :user_id_field, so that user can authenticate through the field of their choice?

defmodule MyApp.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema,
    user_id_field: [:email, :pseudo, :phone]

  # ...
end
1 Like
#83

Oh that’s interesting. It’s not possible, but I like the idea!

I think it may become too complex to manage in a generalized way, as the user id field is used for more than just authentication. Also there may be some security issues I haven’t thought of.

It doesn’t require much effort to currently set up multiple user id field authentication though:

defmodule MyApp.Users do
  alias MyApp.{Repo, Users.User}
  import Ecto.Query, only: [from: 2]

  use Pow.Ecto.Context,
    repo: Repo,
    user: User

  def authenticate(params) do
    user_id = params["user_id"]
    password = params["password"]

    user_id
    |> get_by_user_id()
    |> maybe_verify_password(password)
  end

  defp get_by_user_id(user_id) do
    query =
      from u in User,
        where: u.email == ^user_id or u.pseudo == ^user_id or u.phone == ^pseudo

      Repo.one(query)
  end

  defp maybe_verify_password(nil, _pasword), do: nil
  defp maybe_verify_password(user, password) do
    case User.verify_password(user, password) do
      true -> user
      false -> nil
    end
  end
end

I’m going to work on WebAuthn at some point which may make it possible to sign in without a user id. I’ve added this comment to the github issue to remind me when I’ll refactor the auth flow. Thanks for the suggestion!

Edit: Another option is instead to have something like :authentication_user_id_fields that’s used exclusively for authentication.

4 Likes
#84

How can I redirect from the session login page to a specific page? Also, how can I set a user’s password from IEx?

#85

Hey @danschultzer, just wanted to pop in and say thanks for a great library! I’m pretty new to Elixir and I’m finding it dead simple to drop this into a project.

As I figure out the language, I’ll see what I can’t do to help on some issues :smiley:

Thanks for your time on this so far!

5 Likes
#86

You can change the callback routes by creating a routes module (from the readme):

defmodule MyAppWeb.Pow.Routes do
  use Pow.Phoenix.Routes
  alias MyAppWeb.Router.Helpers, as: Routes

  def after_sign_in_path(conn), do: Routes.some_path(conn, :index)
end

Take a look at the different routes here.

Easiest way is to call the changeset method on your user schema module in IEX:

changeset = MyApp.Users.User.changeset(user, %{password: password, confirm_password: password})

MyApp.Repo.update!(changeset)
3 Likes
#87

Thank you for making this library.

I was wondering how do I seed with Pow library?

I have a context account which have create_user function but unfortunately I cannot get seed to work.

   9   alias Fumigate.Users.User # pow User schema
  ...
  52   def create_user(attrs \\ %{}) do
  53     %User{}
  54     |> User.changeset(attrs)
  55     |> Repo.insert()
  56   end

My seed file:

   116 {:ok, _user} = Fumigate.Accounts.create_user(%{
   117     username: "admin",
   118     email: "admin@example.com",
   119     password: "easy123456",
   120     role: "admin"
   121 })

Also I believe the role markdown have a typo fyi:

  def changeset(user_or_changeset, attrs) do
    changeset_role
    |> pow_changeset(attrs)
    |> changeset_role(attrs)
  end

I believe the changeset_role should be user_or_changeset.

Thanks again for the awesome library.


I did it thanks!

   116 {:ok, _user} = Fumigate.Accounts.create_user(%{
   117     username: "admin",
   118     email: "admin@example.com",
   119     password: "easy123456",
   120     confirm_password: "easy123456",
   121     password_hash: Pow.Ecto.Schema.Password.pbkdf2_hash("easy123456"),
   122     role: "admin"
   123 })
#88

Thanks for pointing out the typo! You won’t need the :password_hash, all you were missing was :confirm_password:

Fumigate.Accounts.create_user(%{
  username: "admin",
  email: "admin@example.com",
  password: "easy123456",
  confirm_password: "easy123456",
  role: "admin"
})
4 Likes
#89

I’ve only been meaning to look a bit over the documentation of Pow today as I was planning to add it to a project on Monday, and I ended up implementing it. It’s really straight-forward.

Authentication library with email confirmation, password reset, persistent session, i18n support. Really flexible all-around.

3 Likes
#90

For the custom controller guide, I think there is a bug for session controller delete action.

If a user is logged in and stay on until the cookie expires then when signing off you get a cryptic error:

function FumigateWeb.Router.Helpers.pow_session_path/3 is undefined or private

I think the solution is (janky elixir code):

  28   def delete(conn, _params) do
  29     still_logged_in = match?(%Fumigate.Users.User{}, conn.assigns[:current_user])
  30     case still_logged_in do
  31       true ->
  32         {:ok, conn} = Pow.Plug.clear_authenticated_user(conn)
  33         redirect(conn, to: Routes.page_path(conn, :index))
  34       _ -> redirect(conn, to: Routes.page_path(conn, :index))
  35     end
  36   end
1 Like
#91

Or can’t we use Pow.Plug.RequireAuthenticated plug to prevent the delete action from being reached ?

1 Like
#92

However in the guide the “/logout” route seems to be protected. In principle it should have been enough… :thinking:

scope "/", MyAppWeb do
 pipe_through [:browser, :protected]

 delete "/logout", SessionController, :delete, as: :logout
end
1 Like
#93

I mean… I have a navbar with a logout button that still point to sessioncontroller delete. Not sure if that matter. I’m going to try your suggestion.

Oh… I think I know what’s happening:

I’m using error_handler: Pow.Phoenix.PlugErrorHandler

Ah that’s the problem. I wanted to be lazy but turns out it was bad to be lazy. Thanks!

1 Like
#94

Thanks for this library, we’ve been using it instead of Coherence for our new projects!

Is there an easy way to store sessions in the database with Ecto? The 2 provided solution “ets” and “mnesia” shows how to implement a backend for session but it’s not as simple as a “user_id” -> “token” in the database. There is a concept of “keys” per user, invalidators…

Did someone already implemented this?

Thanks!

#95

Yup, the guide shows how to set up the proper error handler since the default error handler assumes you don’t have custom routes :smile:

Sorry for the late response, I’ve been swamped!

I haven’t seen anyone implementing this. You should note that the store backends works as key-value cache stores, so there is no concept of relational structure or assumption of user association.

However, you should be able to set up an ecto schema with user association for it since since currently the cache store is used for session ids, reset password tokens and persistent session tokens. It would be safe for future releases if you just do some basic pattern matching.

You would need a way to invalidate the keys. I think the GenServer setup in the Mnesia cache works well for that. Since you would leverage the DB you just need to have namespace, key, user association and TTL, and a unique index on the namespace-key columns.

You can build from the Mnesia cache module. You would only have to change the table_get/2, table_update/4, table_delete/2 and table_keys/2 methods, and remove the mnesia logic for table initialization and namespace of keys.

I may post an example for ecto integration when I got some free time :smile:

4 Likes
#96

We made a version of postgres session cache but it could probably use some more testing.

5 Likes
#97

I’m trying to figure out how to add “Login” and “Sign Up” breadcrumbs to my site. The way I do this on the rest of the site is to create a list of tuples of {name, route} in my controller actions, but I’m not sure how to do this for the Login and Sign Up routes. Any advice here?

1 Like
#98

There are several ways of doing this. I would recommend moving the logic to relevant view module.

In the below example I make the assumption that you continue to assign breadcrumbs in your other controllers as :breadcrumbs, and just want to catch the Pow registration and session controllers (untested code, from the top of my head):

defmodule MyAppWeb.LayoutView do
  use MyAppWeb, :view

  alias MyAppWeb.Router.Helpers, as: Routes

  @spec breadcrumbs(Plug.Conn.t()) :: [{binary(), binary()}]
  def breadcrumbs(%{assigns: %{breadcrumbs: breadcrumbs}}), do: breadcrumbs
  def breadcrumbs(%{private: %{phoenix_controller: Pow.Phoenix.RegistrationController, phoenix_action: :new}}), do: [{"Sign up", Routes.pow_registration_path(conn, :new)}]
  def breadcrumbs(%{private: %{phoenix_controller: Pow.Phoenix.SessionController, phoenix_action: :new}}), do: [{"Sign in", Routes.pow_session_path(conn, :new)}]
  def breadcrumbs(_conn), do: []
end

Now you just call breadcrumbs(@conn) in your layout template (or partial) to fetch the list.

Edit: If a user inputs an invalid password the breadcrumbs in the above example will be empty. You can either change the pattern matching to handle multiple actions for the controller:

  def breadcrumbs(%{private: %{phoenix_controller: Pow.Phoenix.SessionController, phoenix_action: action}}) when action in [:new, :create], do: [{"Sign in", Routes.pow_session_path(conn, :new)}]

Or you could even just use the view and template instead of controller and action:

  def breacrumbs(%{private: %{phoenix_view: Pow.Phoenix.RegistrationView, phoenix_template: "new.html"}})), do: [{"Sign in", Routes.pow_session_path(conn, :new)}]
2 Likes
#99

I’m working on an umbrella project that has one admin web app.

For the login page I would like to disable the default app layout (since it would show informations we don’t want guest to see) or at least use a minimalistic view layout.

When I tried to do something like
plug :put_layout, false or plug :put_layout, {AdminWeb.LayoutView, :login}
in the router.ex then pipe only the login route through it, Pow threw an error about building the template layout.

Please, is there a way to fix this, or should I write a custom session_controller ?

– more details in the case it can help –

Since it is an admin web app, to disable registration I generated the templates and add the app view_module in pow config, then add routes only to edit and delete profile.
For session routes I also added them by hand.
In other words I didn’t use Pow macro such as pow_routes() or pow_sessions()

#100

Hello Dan,

I want to migrate a project from coherence to pow, for that purpose I tried out your library in a new dummy project.

First I want to ask if multiple user support is still available in the latest versions?

I couldn’t get it to work following your instructions in the guide above and your comments in some github issues. Whenever I access one of the scopes it always tries to log the user from the last plug in the endpoint (Pow.Plug.Session) and that user is assigned to both current user keys (current_user and current_admin).

Also the command for generating templates and views with namespaces didn’t work properly as it didn’t use the namespace names in generating the locations of the files. The dummy project is available at the link below, in it we created the namespace folder names manually.

Thank you!

#101

That looks like an issue with Phoenix rather than Pow. How does your routes and pipeline look? And what was the exact error thrown? It should be something like:

pipeline :admin_layout do
  plug :put_layout, {AdminWeb.LayoutView, :login}
end

scope "/" do
  pipe_through [:browser, :admin_layout]

  pow_session_routes() # Or the custom route setup you have written
end
1 Like