View Source Dynamic strategy configuration

In some cases it'll be necessary to dynamically update the provider strategy based on context. PowAssent includes PowAssent.Plug.merge_provider_config/3 to dynamically update the provider configuration.

With this function the configuration can be updated based on attributes within the conn. A common use cases would be to update request parameters based on query parameters, such as setting the connection for Auth0 strategy:

# lib/my_app_web/pow_assent_auth0_plug.ex
defmodule MyAppWeb.PowAssentAuth0Plug do
  def init(opts), do: opts

  def call(conn, _opts) do
    updated_config = [authorization_params: [connection: conn.params["connection"]]]

    PowAssent.Plug.merge_provider_config(conn, :auth0, updated_config)
  end
end

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  # ...

  pipeline :configure_auth0 do
    plug MyAppWeb.PowAssentAuth0Plug
  end

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

    pow_routes()
    pow_assent_routes()
  end

  # ...
end

Incremental authorization with Google

Google (and many other OAuth 2.0 providers that support granular scope configuration) strongly recommends to only request authorization with the minimum required scopes on first signup to keep the onboarding experience smooth. This will minimize the number of consent modals for the end-user by not asking for a bunch of permissions that your app won't even need up-front.

The below example will show how you enable Incremental Authorization with the Google strategy.

In this case, you may only want to request the email and profile scopes when user signs up, but enable opt-in Google Drive scope. Let's set up a custom plug to add the required scopes based on query param.

First we remove the scope from the config:

# config/config.exs
config :my_app, :pow_assent,
  providers: [
    google: [
      client_id: System.get_env("GOOGLE_CLIENT_ID"),
      client_secret: System.get_env("GOOGLE_CLIENT_SECRET"),
      authorization_params: [
        access_type: "offline",
        prompt: "consent",
        include_granted_scopes: true
      ],
      strategy: Assent.Strategy.Google
    ]
  ]

Then we set up a plug to add optional scopes:

# lib/my_app_web/pow_assent_google_incremental_auth_plug.ex
defmodule MyAppWeb.PowAssentGoogleIncrementalAuthPlug do
  @moduledoc """
  This plug enables incremental auth scopes for the Google strategy.

  ## Example

      plug MyAppWeb.PowAssentGoogleIncrementalAuthPlug
  """
  def init(opts), do: opts

  @required_scopes [
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/userinfo.profile"
  ]

  @optional_scopes %{
    "google_drive" => ["https://www.googleapis.com/auth/drive.file"]
  }

  def call(conn, _opts) do
    additional_scopes =
      @optional_scopes
      |> Map.keys()
      |> Enum.filter(& &1 in Map.keys(conn.params))
      |> Enum.map(& @optional_scopes[&1])

    scope = Enum.join(@required_scopes ++ additional_scopes, " ")

    PowAssent.Plug.merge_provider_config(conn, :google, authorization_params: [scope: scope])
  end
end

And finally we add this plug to the pipeline:

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  # ...

  pipeline :configure_google do
    plug MyAppWeb.PowAssentGoogleIncrementalAuthPlug
  end

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

    pow_routes()
    pow_assent_routes()
  end

  # ...
end

Now you can use the authorization url with the google_drive=true query to enable drive.file permission:

~p"/auth/google/new?#{[google_drive: true]}"

You can add any number of additional optional scopes to the plug.

Test modules

# test/my_app_web/pow_assent_google_incremental_auth_plug_test.exs
defmodule MyAppWeb.PowAssentGoogleIncrementalAuthPlugTest do
  use MyAppWeb.ConnCase

  alias MyAppWeb.PowAssentGoogleIncrementalAuthPlug

  @pow_config [otp_app: :my_app]
  @provider :google
  @plug_opts []

  test "call/2 without additional scopes", %{conn: conn} do
    conn = run_plug(~p"/auth/#{@provider}/new")

    assert fetch_provider_scope(conn) ==
      "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
  end

  test "call/2 with google_drive=true query", %{conn: conn} do
    conn = run_plug(~p"/auth/#{@provider}/new?#{[google_drive: true]}")

    opts = PowAssentGoogleIncrementalAuthPlug.init(@plug_opts)
    conn = PowAssentGoogleIncrementalAuthPlug.call(conn, opts)

    assert fetch_provider_scope(conn) ==
      "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/drive.file"
  end

  defp fetch_provider_scope(conn) do
    config = Pow.Plug.fetch_config(conn)

    config[:pow_assent][:providers][@provider][:authorization_params][:scope]
  end

  defp run_plug(uri) do
    opts = PowAssentGoogleIncrementalAuthPlug.init(@plug_opts)

    :get
    |> build_conn(uri)
    |> Pow.Plug.put_config(@pow_config)
    |> Plug.Conn.fetch_query_params()
    |> PowAssentGoogleIncrementalAuthPlug.call(opts)
  end
end