Unsatisfied with authentication systems / and some questions about POW

Hello,

(frenglish incoming)

I have a long time expercient doing (mostly PHP) websites professionnaly, and a long experience with Erlang as a hobby.

I use Elixir from quite some time now and try to make a place for it in my professional work.

But I see the same pattern for authentication systems as in the PHP world : each new library adds some features, is easier to work with, (currently I have settled with Pow which is great) but requires to modify your code in many places, and is tightly coupled to your application code (to my taste, but maybe I do not do things correctly).

I would like to find/create/help-to-create a library where the authentication system is 99% automated and separated from the application code. I do not know if it is a good idea so I would like to have your opinion.

This system would not be a good fit for any application of course but sufficient for prototypes and most CRUD applications.

The idea would be to have a standalone Pow application along the Phoenix app with its extensions opt-in from config. Forms/view/templates would be automated : if you enable github login for instance, the sign-up/log-in templates would have a button for github auth.

Then we would simply plug the auth system in MyApp.Endpoint (just like a socket) with a base path.

Then, we would have hooks to implemplent:

  • When a user is created, we should implement a hook receiving the Pow User id so we could create our data for a user.
  • When a user is deleted, a hook with its id to delete our own user data type.
  • Hooks on log-in / log-out, called with the id and the conn to set assings/session stuff.

Pow manages its users and we have another data type for our users, usually with a foreign key to the pow user.id.

As an example, I implement small games (quizs, card games) and I like to have a Player data type. To me it’s simpler to have a Player decoupled from the auth system so I can implement AI Players with the same interface as a human one.
For a shop, you could have a customer for yourself and another for your company with the same authenticated entity.
Or you can just rely on the Pow User if you do not need anything else.

Also we should provide a way to override the templates from Pow, and/or maybe just pass the url for a custom CSS file.

So, I don’t know if this is currently possible with Pow (I may have missed a hooks section from the docs), but I would be great if we could package that in a small library.

1 Like

I’m happy to hear feedback like this, it’s good to question why things are done the way it’s done! There is a lot to unload here, but it seems like what you are basically looking for is something like an auth server (e.g. Keycloak).

It would definitely be possible to build an app with Pow that does that. If you mostly just want to separate auth from the rest of your system you could also set up an umbrella app. Then you can have one app that only deals with auth (having both phoenix and ecto if you wish to separate the user schema too).

Pow is built to be explicit and functional. I’ve prevented any conditionals in the templates, and to automatically include extensions. This is because with most other auth systems in Elixir it was becoming pretty difficult to work with complex configuration. I also think that’s why Devise was never moved to Elixir.

In Elixir it makes a lot more sense for configuration be part of the code (for most things). This is not a great example, but should highlight the issue I have with using env config for customization:

# Implicit (e.g. in a library)
def my_function(value) do
  expected_value = Application.get_env(:my_app, :expected)

  case value do
    ^expected_value -> # ...
    _any            -> # ...
  end
end

# Explicit (e.g. in your code)
def my_function(value) do
  case value do
    true -> # ...
    _any -> # ....
  end
end

The implicit example is basically what all auth systems did in an abstract sense (the ones I tried to work with before I created Pow). I decided that it’s much better to let the developer handle as much possible explicitly in their code.

As an example, this is how you can update the changeset to hook in player assoc creation (only added for new users):

defmodule MyApp.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema

  schema "users" do
    has_one :player, MyApp.Users.Player

    pow_user_fields()

    timestamps()
  end

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

  defp put_player(changeset) do
    player = %{}

    case Ecto.get_meta(changeset.data, :state) do
      :built -> Ecto.Changeset.put_assoc(changeset, :player, player)
      _any   -> changeset
    end
  end
end

Any customization like the above is easy to unit test, and it’s easy for new developers to understand what happens without having to know what Pow does. I’ve seen a tendency to solve the above as a configuration setting in the library, e.g.:

defmodule MyApp.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema
end

config :my_app, :pow,
  on_user_insert: &add_player/1

def add_player(user) do
  # ...
end

This makes it very difficult to understand what’s going on unless you dig into the library source code, and unit tests doesn’t fit. Dialyzer probably won’t catch any issues here as well. Any changes to the API could invalidate configuration settings, and you may end up with dead code that’s difficult to find.

I’ve tried to limit the number of changes required as much as possible when setting up Pow. That’s why templates are not generated at first, and only needs to be generated when you need to modify them. You can implement the auth system step by step until it’s fits your requirements.

I definitely understand the desire to have it all working out-of-the-box with near no code changes at all, but there are many caveats and pitfalls to this. Pow is the best compromise I could come up with to have an explicit auth system. But there are several things I would like to improve, e.g. I’m not happy with the extension system and how callbacks works.

Hopefully the above gives you a good idea of why Pow was designed the way it was :smile:

Feel free to open any issues or PR’s on github too! Maybe most of this can be dealt with with improved documentation.

3 Likes

If authentication or authorization is part of your logic, you have to change your code when you make such changes :slight_smile:

What are you trying to do?

“automated” authentication system - does it mean “out-of-box” auth system? That sounds great at first, but I found in the long term you’d better to make everything explicit. Bad abstraction is worse than no abstraction. You don’t want to rely on a library for your core auth behavior.

Having auth system separated from the application - It’s easy to extract “registration” and “authentication” part completely out of “application”. Your app code just need 1) the request is authenticated and 2) under which authentication identity. Then your app should focus on the features, not “how you authenticate user”.

On using a library auth system: Whether to use “all-in-one” library like Pow (although it’s composable and configurable) or not is really up to you how much you can leverage the library without too complicated configuration. For example, you may need to own extension for Pow, if existing code does not meet your need.

Hi,

Thank you for your answer.

Yes I’ve been working with auth servers that provided, for instance, a “trusted header”, which could only be defined if the user was authenticated. This is a great feeling. My idea would be to give the same feeling of simplicity, but with the “all in BEAM” touch, just like we have a very capable web server provided by default.

This makes it very difficult to understand what’s going on unless you dig into the library source code, and unit tests doesn’t fit. Dialyzer probably won’t catch any issues here as well.

I was more thiking about giving a hook module in the configuration rather than defining callbacks. A module that should implement a behaviour. Do you think dialiyzer would cough here ? I have to admit it’s been a long time since I have used it. I got to try. I see Pow already uses get_env to define callback modules.

But I do not agree on “you have to dig into the library source code”, because you always has too if you want to understand how things work, and having an explicit name as on_user_insert makes it easy to find where your callback module is called. Although it’s true that API changes may result in dead code, I think semver is good enough to help.

I’d like to hear more about those caveats and pitfalls, to know if they are something I could accept or if it’s a dead-end. My idea is more about “I need auth right now on this simple app to coninue my work” than having a perfect solution.

Thanks four your hook example in the schema. I might use it, but it leaves the same problem I was complaining about : I’d have to handle the deletions too, and make a custom plug to set/delete the player from session on log-in/out.

Hopefully the above gives you a good idea of why Pow was designed the way it was :smile:

Feel free to open any issues or PR’s on github too! Maybe most of this can be dealt with with improved documentation.

This is not criticism against how Pow (or others) do things, neither it is a proposal for modifications on Pow (or others) (although I’d be happy to help if I could !). I think Pow is great and is the best I’ve dealt with. Beeing explicit and Fp-ish is good. I was more about a glue lib that would interface a default Pow (with all extensions opt-in) on 4 hooks :slight_smile:

So maybe a standalone Pow app on its own server with short lived tokens to create the session in MyApp could be simpler.

Thank you for the input !

Hi chulkilee,

If authentication or authorization is part of your logic, you have to change your code when you make such changes

Actually it is not part of my logic. My need is to have an entity that represents human beings so they can create data and keep it around. Like this forum, you want people to be able to post with their name, have bookmarks, watchers, avatars, etc.

But I do not care how they would be authenticated. We use email mainly because email providers defend well agains robots, and because we can reach the email adress owners with it. But beyond this I do not care about auth. I like more relying on better developer’s work ! :slight_smile:

What are you trying to do?

“automated” authentication system - does it mean “out-of-box” auth system? That sounds great at first, but I found in the long term you’d better to make everything explicit.

Yes, I mean plug-and-play out-of-box auth system.

Alike with Dan, I have a hard time understanding why hooks do not seem “explicit” to you. Is it because it is in a config file ? In the long term I agree with you but most apps I’ve worked on required email/password, half of them required an username. Most of the time, requiring authentication is only because the app needs an entity to work with, not because we want to hide stuff from the world. Of course there is private data like Direct Messages in forums but the “I need a human to select the related private messages in the database” is, I believe, the main mechanic here. Hiding them from other users is a byproduct. (not sure of the englis usage here …)

Your app code just need 1) the request is authenticated and 2) under which authentication identity. Then your app should focus on the features, not “how you authenticate user”.

Well I think that is wat I was trying to say. I want to focus on my features so I need a user in this controller actions. That is why I would like to plug SomeDefaultAuthSystem in my Endpoint and forget about it. I do not want to have to edit Pow migrations when I want to add a username to the user schema neither I want to read Pow source code to see if I am doing things wrong when I edit the changeset.

But that is a stance of course. I am currently using Pow and its easy (still had to read the code but I enjoy that). I am unsatisfied because I have auth related stuff in my codebase.

Thank you for your answer :slight_smile:

Some quick notes

  1. Some part of your application do care about how a user authenticates.

For example, MyApp.Forum context may not care how a user authenticates - either email or Google etc. However, your login page cares it.

  1. I’m not sure “Endpoint” is the right place to hook the system into your application completely with 100% authomation. Phoenix endpoint is just for web endpoint - and there should be some way to configure your app to be aware of what are the authentication system.

That is why I would like to plug SomeDefaultAuthSystem in my Endpoint and forget about it. I do not want to have to edit Pow migrations when I want to add a username to the user schema neither I want to read Pow source code to see if I am doing things wrong when I edit the changeset.

Won’t you be able to add more migration, not editing existing pow migration? I’m not pow user, but I believe somewhere you need to put the migration anyway - so I don’t think “having all-in-one” library will solve this.

Ecto changeset is the kind of contract there - you may have the exact same problem even you have “all-one-ine” library.


If you just want authentication, you may leverage lower-level auth-only libraries, such as ueberauth, and handle others by yourself (e.g. storing user name). However, if you need more features, such as password reset, etc… then right it can be useful to have “all-in-one” solution, although I’m not sure that’s a good approach in the long term - compared to composable modules even it requires more configuration to glue things together.


Again, I haven’t used pow - but feel free to share your feedback here or pow repo! For example, due to the nature of “compiled” language, Elixir library may choose macro to implement some features. Does it work well? We will see what pros and cons different approach have.

:heart:

Well the login page is something that should be taken care of by the auth system. I just need to add some styles. Maybe be able to show the form in another page, but just by calling a function.

Adding another migration instead of editing one, yes, actually it is better practice. What I wanted to say is I would rather not touch these tables.

So I would not handle any changeset from Pow, the contract would only be some unique id (basically the users primary key). It is more abstract but less coupled. Maybe it is a bad ideay though :slight_smile:

This is not about how Pow works (despite the edit to the topic title made by AstonJ :stuck_out_tongue: ) but how to provide a plug-and-play auth system based on such libraries.

I tried ueberauth (years ago) but it was all over the app code … this is the source of the problem. But now that I am also thinking about a standalone app that would just do auth, it could be a good fit.

Thank you.

It would be fine if a @behaviour is implemented. Pow has several callback modules, but it’s out of necessity rather than preference. An example of why I don’t really like it would be something like this:

defmodule MyModule do
  def do_this(opts) do
    opts
    |> callback()
    |> do_that()
  end

  defp callback(opts) do
    module = Keyword.get(opts, :callback_module, DefaultCallbackModule)

    module.callback(opts)
  end

  # ...
end

I think handling this explicitly is much easier to understand (and dialyzer can easily pick up on this):

defmodule MyModule do
  def do_this(opts) do
    opts
    |> CallbackModule.callback()
    |> do_that()
  end

  # ...
end

The caveats and pitfalls are essentially you create a tightly coupled library where any change will affect the rest of the library. The tests would have to be far more exhaustive. You can take a look at most of the older auth systems to see the problems that arise with this catch-all approach, instead of pushing that responsibility towards the developer. With this added complexity, you may also end up risking security (e.g. Coherence has a huge security flaw).

Every app has their own way to deal with auth, and it’s much easier for the devs of the individual apps to decide what’s essential in their auth story and add the proper logic/tests. IMO what the auth system should do is to make it easy to build that story.

Also, just to be clear, Pow is very plug n’ play. It works from when you have generated the migrations/user schema, and updated config and endpoint. But it only does auth and registration, nothing else. The files generated is all you need to start customization. You want to use username instead of email as user id? Update the generated migration and user schema, and that’s it. Then step by step things can be added to enable password reset, email confirmation, social auth, etc.

If instead it’s handled in the config (let’s say changing the user id field to :username), you have to ensure that the compiled version did have access to that config value during compilation, and that it hasn’t changed since. The migration would be dynamically build during compile, the same with the ecto schema. It obfuscates your data model, and it’s messy.

I think a library that basically acts as an auth server could work, but you end up with a very specialized library that only work for a subset of devs. As you see, changing the user id field won’t be trivial with app env configuration. The easiest way I could deal with adding the extensions, was to have this controller callbacks setup and let devs plug it in that way (and I’m not really happy about it since it’s not explicit). Then there’s also migrations and ecto schema that needs compile time configuration so you have to generate the migrations, and update the schema with what extensions to enable.

My recommendation would be to set your app up as an umbrella app, and then keep all auth to one separate app. You’ll have the separation between your application logic and the auth. Then from there you can see how it can be turned into an external library if needed.

2 Likes

There are many things to think about in here.

I will not answer because I need to think and I’d have to try things before, but I’ve read it carefully.

Thank you very much :slight_smile:

1 Like