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

Yes, but the PR you’ve looked at was closed since I didn’t like the approach. What I would recommend is to set up an umbrella app with two separate web apps that each has their own Pow setup.

It’s possible to run Pow with two different user schemas in the same web app too, but all the work lies on the shoulders of the developer.

If you go down that route (I strongly recommend just separating the context with an umbrella app, or instead have user roles), then you would need to set up both routes and endpoint to handle multiple contexts, as well as some handling of routes.

Something like this (untested):

# endpoint.ex

plug Plug.Session,
  otp_app: :my_app,
  user: MyApp.Admins.Admin,
  current_user_assigns_key: :current_admin,
  session_key: "admin_auth",
  session_store: {Pow.Store.CredentialsCache, ttl: :timer.minutes(30), namespace: "admin_credentials"},
  routes_backend: MyAppWeb.AdminPowRoutes

plug Plug.Session, otp_app: :my_app
# router.ex

pipeline :admin do
  plug Pow.Plug.Session,
    user: MyApp.Admins.Admin,
    current_user_assigns_key: :current_admin,
    session_key: "admin_auth",
    session_store: {Pow.Store.CredentialsCache, ttl: :timer.minutes(30), namespace: "admin_credentials"},
    routes_backend: MyAppWeb.AdminPowRoutes
end

scope "/admin", as: "admin" do
  pipe_through [:browser, :admin]

  pow_routes()
end

scope "/" do
  pipe_through :browser

  pow_routes()
end
# admin_pow_routes.ex

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

  def url_for(conn, verb, vars \\ [], query_params \\ []) do
     plug = admin_namespace(plug)

     Pow.Phoenix.Routes.url_for(conn, plug, verb, vars, query_params)
  end

  def path_for(conn, plug, verb, vars \\ [], query_params \\ []) do
     plug = admin_namespace(plug)

     Pow.Phoenix.Routes.path_for(conn, plug, verb, vars, query_params)
  end

  defp admin_namespace(plug) do
    [web_module | rest] = Module.split(plug)

    Module.concat([web_module, "Admin"] ++ rest)
  end
end

You can take a look at the thought process I went through when I was working on this in the #24 #53 and #56 issues.

2 Likes

Here are the 2 pipelines I tried:

  pipeline :no_layout do
    plug :put_layout, false
  end
  pipeline :lite_layout do
    plug :put_layout, {AdminWeb.LayoutView, :lite}
  end

then used them like this

 # Session
 scope "/" do
    pipe_through [:browser, :no_layout]
    pow_session_routes()
 end

Or

# Session
  scope "/" do
    pipe_through [:browser, :lite_layout]
    pow_session_routes()
  end

:no_layout pipeline throws following error:

[error] #PID<0.581.0> running AdminWeb.Endpoint (connection #PID<0.577.0>, stream id 2) terminated
Server: localhost:4000 (http)
Request: GET /session/new?request_path=%2F
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in Pow.Phoenix.ViewHelpers.build_layout/2
        (pow) lib/pow/phoenix/views/view_helpers.ex:109: Pow.Phoenix.ViewHelpers.build_layout(false, AdminWeb)
        (pow) lib/pow/phoenix/views/view_helpers.ex:56: Pow.Phoenix.ViewHelpers.layout/1 
        (pow) lib/pow/phoenix/controllers/session_controller.ex:1: Pow.Phoenix.SessionController.phoenix_controller_pipeline/2
        (admin) lib/admin_web/endpoint.ex:1: AdminWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:275: Phoenix.Router.__call__/1
        (admin) lib/admin_web/endpoint.ex:1: AdminWeb.Endpoint.plug_builder_call/2
        (admin) lib/plug/debugger.ex:122: AdminWeb.Endpoint."call (overridable 3)"/2
        (admin) lib/admin_web/endpoint.ex:1: AdminWeb.Endpoint.call/2
        (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:33: Phoenix.Endpoint.Cowboy2Handler.init/2
        (cowboy) /home/kia/projets/elixir/umbrella/bella_portal/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
        (cowboy) /home/kia/projets/elixir/umbrella/bella_portal/deps/cowboy/src/cowboy_stream_h.erl:296: :cowboy_stream_h.execute/3
        (cowboy) /home/kia/projets/elixir/umbrella/bella_portal/deps/cowboy/src/cowboy_stream_h.erl:274: :cowboy_stream_h.request_process/3
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

and finally :lite_layout pipeline throws similar error:

[error] #PID<0.577.0> running AdminWeb.Endpoint (connection #PID<0.576.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /session/new?request_path=%2F
** (exit) an exception was raised:
    ** (MatchError) no match of right hand side value: ["Elixir.AdminWeb.LayoutView"]
        (pow) lib/pow/phoenix/views/view_helpers.ex:110: Pow.Phoenix.ViewHelpers.build_layout/2
        (pow) lib/pow/phoenix/views/view_helpers.ex:56: Pow.Phoenix.ViewHelpers.layout/1
        (pow) lib/pow/phoenix/controllers/session_controller.ex:1: Pow.Phoenix.SessionController.phoenix_controller_pipeline/2
        (admin) lib/admin_web/endpoint.ex:1: AdminWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:275: Phoenix.Router.__call__/1
        (admin) lib/admin_web/endpoint.ex:1: AdminWeb.Endpoint.plug_builder_call/2
        (admin) lib/plug/debugger.ex:122: AdminWeb.Endpoint."call (overridable 3)"/2
        (admin) lib/admin_web/endpoint.ex:1: AdminWeb.Endpoint.call/2
        (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:33: Phoenix.Endpoint.Cowboy2Handler.init/2
        (cowboy) /home/kia/projets/elixir/umbrella/bella_portal/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
        (cowboy) /home/kia/projets/elixir/umbrella/bella_portal/deps/cowboy/src/cowboy_stream_h.erl:296: :cowboy_stream_h.execute/3
        (cowboy) /home/kia/projets/elixir/umbrella/bella_portal/deps/cowboy/src/cowboy_stream_h.erl:274: :cowboy_stream_h.request_process/3
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

I have to add that those pipelines work as expected with other routes except for Pow routes.

For now I keep the default layout template app.html and check if the user is authenticated then render the right layout.

<%= render layout_template(@current_user), assigns %>

and define

defmodule AdminWeb.LayoutView do
  use AdminWeb, :view

  def layout_template(nil), do: "login.html"
  def layout_template(_current_user), do: "dash_board.html"
end

Thanks, that was indeed a bug in Pow!

I’ve merged a fix so please go ahead and see if it works for you now by using the master branch: {:pow, github: "danschultzer/pow"}

6 Likes

@danschultzer just wanted to say thank you for your time spent developing and helping people with this project. It’s a real asset to the community.

14 Likes

I just tested it, and it works perfectly!
Thank you. ^^

2 Likes

Hi @kidbombay - would you be able to share your code for the postgres sessions cache?

cheers

Dave

Ya I could.
Just need to remove company specific stuff. It should be able to be a POW extension ideally but don’t know enough about the internals to do that.

3 Likes

That would be awesome if you could - I need something very similar for a project I’m doing - just the basics on how you achieved this would be brilliant.

cheers

Dave

1 Like

I would recommend it to either be a library, put up on e.g. gist for copy-paste, or maybe as a guide like the redis cache store guide.

Pow extensions are for adding or modifying to the ecto/phoenix/plug logic, while with a new cache store backend you would need to update your config with this: cache_store_backend: PostgresCache.

1 Like

I’ve been following this thread and the Pow GitHub repos for a while and I just want to commend @danschultzer for the incredible support you are providing for your library. You go above and beyond what is commonly seen for open source libraries in terms of support and guidance. :clap:

PS: Be careful not to get burned out :slightly_smiling_face:

11 Likes

Fully agreed with @IRLeif!

Dan has been privately providing me support without ever having to – and I’m very grateful. :sparkling_heart: :024:

I’m pondering compiling some short Pow how-to-guides and propose them as PRs. When I get the time I’ll do it.

4 Likes

Thanks for the kind words both of you!

In my last startup I needed a good user auth library, so I began working on Pow. The business didn’t work out, but I have been able to work full time on Pow since. I’ve been fortunate to be able to build several businesses on top of the open source work from thousands of developers, so I only feel it’s right that I contribute back.

I’ve yet to feel burned out, and I do enjoy helping out everyone. It also helps me identify issues and make Pow more ergonomic to work with (I feel too many libraries are missing this). All this work doesn’t pay the bills though so my time is getting more limited as I’m heading back into building new businesses :smile:

14 Likes

I’ve started using Pow and PowAssent. Works pretty well so far. I also tend to dive into the source of the packages I use to understand them better.

I’ve got two pieces of constructive criticism. First, I notice that you tend to use pipelines a lot, which is fine. However, there are many cases where things can fail in your pipeline and you end up handling it by making the next command in the pipeline aware of the possibility of that failure. A better mechanism for this in my opinion is with, which I don’t see you using anywhere (though I may have missed some place). with allows you to short circuit a sequence of steps when there is a failure. Meaning subsequent steps don’t need to consider that failure.

The second, is that to customize a something like a changeset, you have the code define changeset/2 and pow_changeset/2 for example. Instead of defining pow_changeset/2, you can just do defoverridable changeset: 2. That allows the user of your library to define their own changeset/2 and call super to call the one your library defined.

https://hexdocs.pm/elixir/Kernel.html#defoverridable/1

2 Likes

Thank @blatyo!

I’m really happy to get some feedback for the code. That’s the only way to improve it :slight_smile:

Yeah, I can see with makes sense in some of the ecto changeset methods. I’ve never done anything with with, and have always preferred just using pipe operators because it’s easy for me to understand what happens step by step.

I would love to see some examples of code in Pow that can be made more concise using with!

That actually already happens. changeset/2 is defined, and can be overridden. You can take a look at the use macro here:

You can use super, but calling pow_changeset/2 method makes it more explicit. As you can read in the docs, there’s also a pow_extension_changeset/2 method since the extension modules are separated from the Pow core module.

I often try to think of how someone new to my codebase would understand it, and how to make the code as explicit as possible.

Best case would be to call something like Pow.Ecto.Schema.changeset/2 rather than using a method defined in the use macro, but if you look at the Pow.Ecto.Schema module you can see that due to compile time config it’s necessary to define these methods in the macro so Pow is ergonomic to work with.

Feel free to open any PR/issues on GitHub too :slight_smile:

I guess I should have realized that since you can obviously override the changeset functions. It was just the fact that there were the pow_* functions that threw me off.

I did see yesterday when customizing routes, that you had used defoverride there as well. However, you chose not to create pow_* functions there. Perhaps the changeset situation would be more obvious if you suggest people write @impl true before their changeset:

@impl true
def changeset(...) do
1 Like

with is still step by step. The difference is that you don’t need every subsequent step to be aware of the errors of the steps before them. I sometimes call it the happy case operator. Because it allows you to describe what should happen if nothing failed and then address the situation where things did fail later or pass off the error for something else to handle.

I’ll throw up a PR sometime this weekend.

6 Likes

Hello Dan,

I’ve followed your guide for implementing custom controllers but the confirmation email is not sent. When submitting registration on the default pow routes (/registraton), the confirmation email is sent successfully. Also when submitting to the custom registration controller the user is signed in without confirmation but works properly on the default pow controllers. I used exactly the same code you used in the referenced link above, is there something that needs to be added to the code so the mail can be sent or am I making other mistakes.

Thanks

When you use custom controllers you’ll have to implement any extension logic yourself (the example only shows how to do it for the create action on the session controller, but it needs to be implemented on any controller actions that signs in users): https://github.com/danschultzer/pow/blob/master/guides/CUSTOM_CONTROLLERS.md#further-customization

Unless you got a good reason for not using the default Pow controllers, I would recommend that you just use them instead of custom controllers.

1 Like

Pow 1.0.8 released

Changelog: Github
Hex: https://hex.pm/packages/pow/1.0.8

Multitenancy

You can now easily pass repo opts for your multitenant app: pow/guides/MULTITENANCY.md at v1.0.8 · pow-auth/pow · GitHub

Custom mailer layout

It’s now possible to set a mailer layout similar to Phoenix controller layout: Pow.Phoenix.Mailer.Mail — Pow v1.0.8

Decoupling

Ecto/Plug/Phoenix modules already was very decoupled, but there was some specs that didn’t reflect that, as well as expectation for the changeset being an Ecto.Changeset struct. Now there’s a clearer separation.

Happy coding :rocket:

7 Likes