POW: Custom controller doubts

hi @danschultzer I am planning on adding a referral code in Login form, i generated the templates and added referral_code as text field, in the generated registration form. I have few doubts with the custom controllers
(The ask is): I want to add this referral code only on registration controller, in a transactional way. if code given , then i check presence of code and finally create user. If code not given, i directly create user.
I use pow_assent. I have read about invitation extension but i want to go with my own.

Doubts:

  1. I want to modify only create action of registration controller. Do i need to modify the routes as well?
  2. Do i need to add all action for both registration, session controllers and point their routes as mentioned in https://hexdocs.pm/pow/1.0.11/custom_controllers.html#routes.
  3. Is there a way to overwrite only create action of registration controller?

How do you want to verify and use the referral code during registration? You can set up a changeset as described in the Creating custom controller callbacks with Pow - #2 by danschultzer thread. It describes how to check for an association only when the user is created.

Yeah, but you only need to override that single route:

  scope "/", MyAppWeb do
    resources "/registration", RegistrationController, singleton: true, only: [:create]
  end

  scope "/" do
    pow_routes()
  end

No.

See above.

Alternatively you can set up a custom context:

defmodule MyApp.Users do
  use Pow.Ecto.Context,
    repo: MyApp.Repo,
    user: MyApp.Users.User

  def create(params) do
    case verify_referral_code(params) do
      {:ok, _any}         -> pow_create(params)
      {:error, changeset} -> {:error, changeset}
    end
  end

  # ...
end

If the referral code is also used in the PowAssent callback phase then you would need to do this for the user identities context too.

3 Likes

Is there a generator for Contexts (or Controllers, for that matter)? I actually just posted a related question on Spec about this a few hours ago!

No, but the context modules are minimal since they use macros. It’s done this way so you only have to override the methods that you want custom logic for, and let Pow handle the rest. The callbacks should be used as reference for what to override.

As for controllers, I think it’s best to let developers decide the design rather than having a generator set it up. You only need to use Pow.Plug methods, Pow controllers are extremely thin. There is a guide in the docs that shows how to set up custom controllers: Custom controllers — Pow v1.0.15

In PowAssent it’s more complex though.

I set up Pow and PowAssent and got a new Phoenix app running locally with email and Github login. After creating account with email and a password, I logged out and then tried an Oauth login via a Github account. That Github account is also under the same email address. Rather than logging me in or maybe asking me to verify my existing password, it asks for another email to link to the Github auth (which seems like a bad idea).

We had a discussion about this recently on Github. TLDR; it works like this for security, but the UX would be much better (as you wrote) if the user had the option to auth themselves to link up the provider instead of selecting another user id or having to exit the flow and sign in first. I plan to look into this as soon as I got free time for it.

1 Like

Basically the main thing I was looking for was a way to get whatever the Auth provider sends back in its callback so I could parse it and deal with it. That’s what I’m currently doing with Ueberauth.

One other question. Is there a way to generate the context for users or see what’s in pow_user_fields()?

I’d like a way to get the equivalent of Accounts.list_users() or Accounts.get_user!(57) and to have a module to add more related logic.

Can we add fields to the Pow User schema? I’ve looked through the guide on Github and I’m trying to figure out how to use Pow in the context of a larger app.

What info is it you want to retrieve and what do you want to do with it after? FYI Assent is what’s used under the hood as the low level multi provider framework, while PowAssent handles the Phoenix/Plug/Ecto integration. The PowAssent callback controller action sends along the info to the context and changeset.

It’s described in the docs: Pow.Ecto.Schema — Pow v1.0.15

Just do what you normally would do in a Phoenix app and create the context module yourself :smile: It’s the idea of the custom context module in Pow, it works the same way as what you usually do in Phoenix/Ecto. No magic, it’s the same as you would do without Pow.

Yep, works the same way as without Pow: Pow.Ecto.Schema — Pow v1.0.15

pow_user_fields/0 could be replaced with the appropriate field macro calls. It’s just a helper to ease integration and development.

You might also be interested in seeing how you can store the access token with PowAssent: Capture access token — PowAssent v0.4.5

1 Like

Thanks for all the answers! This is really helpful.

Since I don’t like typing boilerplate, I normally use built-in Phoenix generators and then add and/or customize as needed :innocent:

For this very simple app, I just want to get the authenticated user’s email, avatar image and name. I’ll use the email to determine whether or not an account already exists or not (as explained in my original question). Then, I’ll use the name and avatar_url to update their User record.

For other apps, I might want to inspect the contents of what the Auth provider sends back and use other details from struct.

In that case mix phx.gen.context Users User users --no-schema will suffice :smile:

Setting name and avatar should only happen on registration, or every time the user auths?

The first is easiest (basically what’s in the readme):

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

  schema "users" do
    field :name, :string
    field :picture, :string

    pow_user_fields()

    timestamps()
  end

  def user_identity_changeset(user_or_changeset, user_identity, attrs, user_id_attrs) do
    user_or_changeset
    |> Ecto.Changeset.cast(attrs, [:picture, :name])
    |> pow_assent_user_identity_changeset(user_identity, attrs, user_id_attrs)
  end
end

The second you would also need a custom context module to trigger when upserting the user identity, so the name/avatar gets updated on each auth request:

defmodule MyApp.UserIdentities do
  use PowAssent.Ecto.UserIdentities.Context,
    repo: MyApp.Repo,
    user: MyApp.Users.User

  def upsert(user, user_identity_params) do
    MyApp.Repo.transaction fn ->
      case pow_upsert(user, user_identity_params) do
        {:ok, user}     -> update_user(user, user_identity_params)
        {:error, error} -> {:error, error}
      end
    end
  end

  defp update_user(user, user_identity_params) do
    user
    |> MyApp.Users.User.changeset(user_identity_params)
    |> MyApp.Repo.update()
  end
end

If you use the second, you have to remember to update the changeset so it casts :picture and :name, since user_identity_changeset only triggers when a user is created. You can also cast it right there in the context method.

Remember to add user_identities_context: MyApp.UserIdentities to the config.

Assent conforms the result to OpenID Connect Core 1.0 Standard Claims spec for all strategies (why for Github it’s picture instead of avatar_url). You can inspect the values in custom changeset or context if it’s only for dev purpose.

This generates a Context file where every function raises “TODO”. It’s better than nothing at all generated, but certainly not as good as the default experience with Phoenix generators.

A more typical use case would probably involve some fields on the user, so it would probably make sense to have an example of a mix phx.gen.html (or api) where there’s at least one field on the user to see how it interacts with Pow. (I’ll write one such example myself once I understand how everything works and figure out a nice flow!)

This was useful in getting a handle with which to inspect the incoming data. However, adding it also causes my (new Phoenix) app to crash entirely on Github auth because PowAssent.RegistrationView isn’t available.

Here’s my entire User.ex:

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

  schema "users" do
    pow_user_fields()

    timestamps()
  end

  def user_identity_changeset(user_or_changeset, user_identity, attrs, user_id_attrs) do
    user_or_changeset
    |> Ecto.Changeset.cast(attrs, [])
    |> pow_assent_user_identity_changeset(user_identity, attrs, user_id_attrs)
  end
end

I saw your comments on this thread so I tried making some changes in my router but with no success thus far. Other than the addition of the one protected route, “/secret”, it’s just a fresh install plus what the PowAssent “getting started” section advised. Here’s the relevant portion of it:

scope "/" do
  pipe_through :skip_csrf_protection

  pow_assent_authorization_post_callback_routes()
end

scope "/" do
  pipe_through [:browser]
  pow_routes()
  pow_assent_routes()
end

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

  scope "secret" do
    pipe_through [:protected]
    get "/", PageController, :secret
  end

  get "/", PageController, :index
end

Oh right, forgot that the Phoenix generator just adds todo stubs when no schema will be generated. I don’t think it’s flexible enough to use the existing schema module.

Can you share the stack trace? PowAssent routes are scoped with PowAssent.Phoenix module, so I can only think of some invalid route setup that causes this. Your routes and schema looks right though.

Sure thing, this is what it looks like:
GET /auth/github/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
Parameters: %{“code” => “13eec5f249e194d7085a”, “provider” => “github”, “state” => “9b20d1c24ade88cff3ee6fb38e1bc5e06daea3f33bf6b53”}
Pipelines: [:browser]
warning: This request will NOT be verified for valid SSL certificate
(assent) lib/assent/http_adapter/httpc.ex:22: Assent.HTTPAdapter.Httpc.request/5
(assent) lib/assent/strategy.ex:42: Assent.Strategy.request/5
(assent) lib/assent/strategies/oauth2.ex:222: Assent.Strategy.OAuth2.get_access_token/2
(assent) lib/assent/strategies/oauth2.ex:119: Assent.Strategy.OAuth2.callback/3
(assent) lib/assent/strategies/oauth2/base.ex:69: Assent.Strategy.OAuth2.Base.callback/3
(pow_assent) lib/pow_assent/plug.ex:74: PowAssent.Plug.callback/4
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:44: PowAssent.Phoenix.AuthorizationController.process_callback/2
(pow) lib/pow/phoenix/controllers/controller.ex:99: Pow.Phoenix.Controller.action/3
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
(phoenix) lib/phoenix/router.ex:288: Phoenix.Router.call/2
(my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
(my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint.“call (overridable 3)”/2
(my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
(phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4
(cowboy) /Users/me/my_app/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy) /Users/me/my_app/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3

warning: This request will NOT be verified for valid SSL certificate
(assent) lib/assent/http_adapter/httpc.ex:22: Assent.HTTPAdapter.Httpc.request/5
(assent) lib/assent/strategy.ex:42: Assent.Strategy.request/5
(assent) lib/assent/strategies/oauth2.ex:256: Assent.Strategy.OAuth2.get/4
(assent) lib/assent/strategies/oauth2.ex:266: Assent.Strategy.OAuth2.get_user/3
(assent) lib/assent/strategies/github.ex:57: Assent.Strategy.Github.get_user/3
(assent) lib/assent/strategies/oauth2.ex:237: Assent.Strategy.OAuth2.fetch_user/3
(assent) lib/assent/strategies/oauth2/base.ex:69: Assent.Strategy.OAuth2.Base.callback/3
(pow_assent) lib/pow_assent/plug.ex:74: PowAssent.Plug.callback/4
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:44: PowAssent.Phoenix.AuthorizationController.process_callback/2
(pow) lib/pow/phoenix/controllers/controller.ex:99: Pow.Phoenix.Controller.action/3
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
(phoenix) lib/phoenix/router.ex:288: Phoenix.Router.call/2
(my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
(my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint.“call (overridable 3)”/2
(my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
(phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4

warning: This request will NOT be verified for valid SSL certificate
(assent) lib/assent/http_adapter/httpc.ex:22: Assent.HTTPAdapter.Httpc.request/5
(assent) lib/assent/strategy.ex:42: Assent.Strategy.request/5
(assent) lib/assent/strategies/github.ex:70: Assent.Strategy.Github.get_email/4
(assent) lib/assent/strategies/oauth2.ex:237: Assent.Strategy.OAuth2.fetch_user/3
(assent) lib/assent/strategies/oauth2/base.ex:69: Assent.Strategy.OAuth2.Base.callback/3
(pow_assent) lib/pow_assent/plug.ex:74: PowAssent.Plug.callback/4
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:44: PowAssent.Phoenix.AuthorizationController.process_callback/2
(pow) lib/pow/phoenix/controllers/controller.ex:99: Pow.Phoenix.Controller.action/3
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
(pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
(phoenix) lib/phoenix/router.ex:288: Phoenix.Router.call/2
(my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
(my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint.“call (overridable 3)”/2
(my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
(phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4
(cowboy) /Users/me/my_app/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy) /Users/me/my_app/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3

[debug] QUERY OK source=“user_identities” db=0.9ms decode=1.4ms queue=1.2ms idle=7355.3ms
SELECT u1.“id”, u1.“password_hash”, u1.“email”, u1.“inserted_at”, u1.“updated_at” FROM “user_identities” AS u0 LEFT OUTER JOIN “users” AS u1 ON u1.“id” = u0.“user_id” WHERE ((u0.“provider” = $1) AND (u0.“uid” = $2)) [“github”, “236820”]
[debug] QUERY OK db=0.2ms idle=7438.4ms
begin
[debug] QUERY ERROR db=8.8ms
INSERT INTO “users” (“email”,“inserted_at”,“updated_at”) VALUES ($1,$2,$3) RETURNING “id” [“person@example.com”, ~N[2019-12-30 14:54:31], ~N[2019-12-30 14:54:31]]
[debug] QUERY OK db=0.1ms
rollback
[info] Sent 302 in 2557ms
[info] GET /auth/github/add-user-id
[debug] Processing with PowAssent.Phoenix.RegistrationController.add_user_id/2
Parameters: %{“provider” => “github”}
Pipelines: [:browser]
[info] Sent 500 in 59ms
[error] #PID<0.496.0> running MyAppWeb.Endpoint (connection #PID<0.464.0>, stream id 8) terminated
Server: localhost:4000 (http)
Request: GET /auth/github/add-user-id
** (exit) an exception was raised:
** (UndefinedFunctionError) function MyAppWeb.PowAssent.RegistrationView.render/2 is undefined (module MyAppWeb.PowAssent.RegistrationView is not available)
MyAppWeb.PowAssent.RegistrationView.render(“add_user_id.html”, %{action: “/auth/github/create”, changeset: ecto.Changeset<action: nil, changes: %{}, errors: [password_hash: {“can’t be blank”, [validation: :required]}, password: {“can’t be blank”, [validation: :required]}, email: {“can’t be blank”, [validation: :required]}], data: #MyApp.Users.User<>, valid?: false>, conn: %Plug.Conn{adapter: {Plug.Cowboy.Conn, :…}, assigns: %{action: “/auth/github/create”, changeset: ecto.Changeset<action: nil, changes: %{}, errors: [password_hash: {“can’t be blank”, [validation: :required]}, password: {“can’t be blank”, [validation: :required]}, email: {“can’t be blank”, [validation: :required]}], data: #MyApp.Users.User<>, valid?: false>, current_user: nil, layout: {MyAppWeb.LayoutView, “app.html”}}, before_send: [#Function<0.61503622/1 in Plug.CSRFProtection.call/2>, #Function<2.10523259/1 in Phoenix.Controller.fetch_flash/2>, #Function<0.92191472/1 in Plug.Session.before_send/2>, #Function<0.40842786/1 in Plug.Telemetry.call/2>, #Function<0.105594227/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>], body_params: %{}, cookies: …

To clarify, it works for creating new accounts. What breaks it is creating an account with email & password, logging out and then trying to log in via Oauth using an account tied to that user’s email.

You have to generate the views and templates for PowAssent, as described in the readme:

mix pow_assent.phoenix.gen.templates

It’s because you have set web_module: MyAppWeb in your config for Pow.

1 Like

Right you are! I must have forgotten that 2nd pow generator when deleting and rebuilding the app in the process of trying to debug it.

Now logging in via ouath is no longer crashing and is asking for a 2nd email address as before. I’ll dig back in in the morning and see if I can figure out what’s causing that behavior and how to override it (since I just want to use the existing email from their Oauth).

Thanks again for all the help!

It happens when there’s an error on the user id field, both if it has been taken already or if it’s not the right format (though with Github and standard Pow setup that would not happen). Here’s the logic: pow_assent/lib/pow_assent/phoenix/controllers/authorization_controller.ex at v0.4.5 · pow-auth/pow_assent · GitHub

The context is what sends back the {:error, {:invalid_user_id_field, changeset}} error, as seen here: pow_assent/lib/pow_assent/ecto/user_identities/context.ex at v0.4.5 · pow-auth/pow_assent · GitHub

So if you don’t really worry about security implications, you could add a custom create_user/3 context method, where after pow_assent_create_user/3 returns the above error, you can just look up the user for the email and then call pow_assent_upsert/2.

If you want to instead let users log in first, then you have to set up custom controllers. But as I wrote before, I’ll look into that when I got time.

1 Like

@danschultzer, I tried both,
The custom context worked fine, yet i need to add a validation error message on invalid code.
but the custom controllers threw error the following error

assign @action not available in eex template.

I just need to add the action path here. I thought it would be great if you update the doc such that the next person will not face it. or shall i send a PR for this?

1 Like

Saw the registration html files, As i generated templates, the url was pointing to @action, i forgot to update them

https://hexdocs.pm/pow/1.0.11/custom_controllers.html#templates

“Routes.signup_path(@conn, :create)”

1 Like

@danschultzer, I went with custom controller which helps me to return a valid error message to the user. And i have set the action variable, which worked fine on generated templates.

def create(conn, %{"user" => user_params}) do
    # We'll leverage [`Pow.Plug`](Pow.Plug.html), but you can also follow the classic Phoenix way:
    # user =
    #   %MyApp.Users.User{}
    #   |> MyApp.Users.User.changeset(user_params)
    #   |> MyApp.Repo.insert()

    case MyApp.Users.create_user_using_referral(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "Welcome, Lets find your spouse!")
        |> redirect(to: Routes.dashboard_path(conn, :index))
      {:invalid_code, changeset} ->
        conn
        |> put_flash(:error, "You have entered an invalid code")
        |> render("new.html", changeset: changeset, action: Routes.registration_path(conn, :create))
      {:error, changeset} ->
        conn
        |> put_flash(:error, "Please correct the errors")
        |> render("new.html", changeset: changeset, action: Routes.registration_path(conn, :create))
    end
  end
1 Like

What was invovled in setting up a custom controller? Was simply naming a controller MyAppWeb.UserController and overwriting the functions in Pow?

I’m interested in doing this. The primary pain I hit comes from having controller actions hidden in a library and not having control over how possible issues are handled, redirects, error messages, etc.