DEV Community

Cover image for Deploying Elixir: Creating Your Own Ueberauth Strategy
Erik Guzman
Erik Guzman

Posted on • Edited on

Deploying Elixir: Creating Your Own Ueberauth Strategy

Configuring your application

I've been learning and growing building my own elixir and Phoenix applications. A couple of times already, I've had to use Ueberauth to do authentication. One example is Twitch, and another example is Patreon.

Unfortunately, Ueberauth did not have an up-to-date strategy for Twitch and did not have a strategy for Patreon at all. So I needed to go and either update the library or create my own.

In this article, I'll go through the steps of creating your own Ueberauth Patreon strategy in your project.

If you dont have a project of your own already, let's go ahead and spin one up and follow the setup instructions.

mix phx.new patreon-ueberauth-demo
Enter fullscreen mode Exit fullscreen mode

Let's start getting our app set up with our own Ueberauth integration. First, add the Ueberauth and OAuth2 packages to your mix.exs file. We will need the OAuth2 package to make integrating with any OAuth service much easier.

{:ueberauth, "~> 0.6"},
{:oauth2, "~> 2.0"}
Enter fullscreen mode Exit fullscreen mode

Now install our new dependency.

mix deps.get
Enter fullscreen mode Exit fullscreen mode

In our config.exs anywhere above import_config "#{config_env()}.exs" we are going to register a Patreon strategy with Ueberauth. We haven't gotten around to writing the code yet, but we will get to that shortly.

#...

config :ueberauth, Ueberauth,
    providers: [
      patreon: {Ueberauth.Strategy.Patreon, [default_scope: ""]},
    ]

#...
import_config "#{config_env()}.exs"
Enter fullscreen mode Exit fullscreen mode

Let’s also add to the config.exs the code to fetch the Client ID and Client Secret for the Patreon App we will use.

config :ueberauth, Ueberauth.Strategy.Patreon.OAuth,
  client_id: System.get_env("PATREON_CLIENT_ID"),
  client_secret: System.get_env("PATREON_CLIENT_SECRET")
Enter fullscreen mode Exit fullscreen mode

When you finally deploy your application to a production environment, you will create environment variables PATREON_CLIENT_ID and PATREON_CLIENT_SECRET that your application will use.

But since we're still stuck in local development, well need a secure way to set these files without committing them.

Inside your config folder, create and a new file called dev.secret.exs and add the following code.

import Config

config :ueberauth, Ueberauth.Strategy.Patreon.OAuth,
  client_id: "your_client_id",
  client_secret: "your_secret"
Enter fullscreen mode Exit fullscreen mode

This new file will contain any secrets your application needs for local development, but cant be committed.

Now that we have our new secret file, we need to use it. Add the following line at the bottom of your dev.exs file.

# Rest of dev.exs file

import_config "dev.secret.exs"
Enter fullscreen mode Exit fullscreen mode

By importing the dev.secret.exs config last, anything we set in it will override all other configurations. So the Client ID and Client Secret we put in the config.exs will be overwritten by our local configuration the dev.secret.exs file.

Let's make sure we never commit this new file to source control by adding it to the .gitignore.

# Other .gitignore stuffs

/config/dev.secret.exs
Enter fullscreen mode Exit fullscreen mode

Now it's time to go and create our own Patreon app so we can set the Client ID and Client Secret inside our dev.secret.exs.

Here is the direct link to the dashboard to create your own app for Patreon. You will need an active Patreon account, so make sure you’re logged in. https://www.patreon.com/portal/registration/register-clients

When you are filling out the app form, make sure to set your callback URL to:

http://localhost:4000/auth/patreon/callback
Enter fullscreen mode Exit fullscreen mode

Patreon needs to know ahead where to send callback information for security purposes, so it's vital you correctly set all URLs. Were going to use the standard Ueberauth URL callback scheme in this tutorial.

After creating your app, make sure to go back to your dev.secret.exs and set the Client ID and Client Secret of your application.

OAuth Phases

Now it's time to write the implementation for our Ueberauth Patreon strategy. Two major components go into implementing an OAuth flow. The REQUEST and the CALLBACK phases.

In the REQUEST phase, we will redirect a user to Patreon for REQUEST permission to do actions on their user's behalf. This is always seen to the user as an approval screen.

The CALLBACK phase kicks in where the user says YES to apps request, and Patreon will CALLBACK to us in the form of a request with a special passcode we can use to get their token. Once we use the special passcode to get our hands on the token, we will be able to perform API requests to do things on the users behalf.

Building the Request Phase

Let’s create a controller that will handle all our Patreon Ueberauth phases. Inside your apps controller folder, make a new file called auth_controller.ex With the following contents.

defmodule PatreonUeberauthDemoWeb.AuthController do
  use PatreonUeberauthDemoWeb, :controller
end
Enter fullscreen mode Exit fullscreen mode

In router.ex file, add the Ueberauth plug to your pipeline.

pipeline :browser do
    plug Ueberauth
    ...
end
Enter fullscreen mode Exit fullscreen mode

In the same file, let’s create a new scope that will handle our authentication request

scope "/auth", PatreonUeberauthDemoWeb do
    pipe_through :browser

    get "/:provider", AuthController, :request
end
Enter fullscreen mode Exit fullscreen mode

Next, lets create modules necessary for Ueberauth to know what to do. I will follow a file structure that will allow you to pull out your Ueberauth strategy and make your own package.

In your lib folder, make a new folder called ueberauth. Inside the folder, make another folder called strategy. Now, let's create a file called patreon.ex , which will contain the necessary function to perform our Ueberauth strategy. Finally, let’s put in the boilerplate code to handle the request phase.

defmodule Ueberauth.Strategy.Patreon do
  use Ueberauth.Strategy,
    oauth2_module: Ueberauth.Strategy.Patreon.OAuth

  def handle_request!(conn) do
    params =
      []
      |> with_state_param(conn)
    # Will invoke the OAuth Patreon module we will define next
    module = option(conn, :oauth2_module)
    # Performs the redirect to Patreon to REQUEST access 
    redirect!(conn, apply(module, :authorize_url!, [params]))
  end  
end
Enter fullscreen mode Exit fullscreen mode

We need to implement a new module that contains the OAuth implementation details for Patreon. Inside your strategy folder, create a new folder called patreon and create a file called oauth.ex. Your entire file path should look like lib/ueberauth/strategy/patreon/oauth.ex.

Put the following code inside youroauth.ex file. This contains the Patreon OAuth information the OAuth2 package needs to make our lives a whole lot easier.

defmodule Ueberauth.Strategy.Patreon.OAuth do
  use OAuth2.Strategy

  @defaults [
    strategy: __MODULE__,
    site: "https://www.patreon.com",
    authorize_url: "https://www.patreon.com/oauth2/authorize",
    token_url: "https://www.patreon.com/api/oauth2/token",
    token_method: :post
  ]

  def client(opts \\ []) do
    # This is where we grab the CLient ID and Client Secret we created earilier
    config =
      :ueberauth
      |> Application.fetch_env!(Ueberauth.Strategy.Patreon.OAuth)
      |> check_config_key_exists(:client_id)
      |> check_config_key_exists(:client_secret)

    client_opts =
      @defaults
      |> Keyword.merge(config)
      |> Keyword.merge(opts)

    json_library = Ueberauth.json_library()

    OAuth2.Client.new(client_opts)
    |> OAuth2.Client.put_serializer("application/json", json_library)
  end

  def authorize_url!(params \\ [], opts \\ []) do
    opts
    |> client
    |> OAuth2.Client.authorize_url!(params)
  end

    def authorize_url(client, params) do
    OAuth2.Strategy.AuthCode.authorize_url(client, params)
  end

  defp check_config_key_exists(config, key) when is_list(config) do
    unless Keyword.has_key?(config, key) do
      raise "#{inspect(key)} missing from config :ueberauth, Ueberauth.Strategy.Patreon"
    end

    config
  end

  defp check_config_key_exists(_, _) do
    raise "Config :ueberauth, Ueberauth.Strategy.Patreon is not a keyword list, as expected"
  end
end
Enter fullscreen mode Exit fullscreen mode

@defaults is the most important part of the above code, this is where you will set the correct URLs for your OAuth flow. If you're trying to set up OAuth for a different service, make sure to set the correct authorize_url and token_url.

We should have all the code necessary to handle the REQUEST phase of the OAuth flow. Let’s test it out and see it is working. Start your application and go to:

http://localhost:4000/auth/patreon
Enter fullscreen mode Exit fullscreen mode

You should be redirected to the Patreon REQUEST flow screen asking if you want to give permission for your Patreon application to have access or do specific actions on your behalf.

After you agree, Patreon will attempt to trigger the CALLBACK flow, but you are most likely going to get the following error

function PatreonUeberauthDemoWeb.UeberauthController.callback/2 is undefined or private

This error is expected because we have yet to implement any of the code for the CALLBACK phase. So let’s do that now.

Building the Callback Phase

In your controller auth_controller.ex, add the following to handle the callback request from Patreon.

defmodule PatreonUeberauthDemoWeb.AuthController do
  use PatreonUeberauthDemoWeb, :controller

  def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: "/")
  end

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    conn
    |> redirect(to: "/")
  end
end
Enter fullscreen mode Exit fullscreen mode

Next, let’s update our router.ex to route the Patreon callback to our new controller actions.

scope "/auth", PatreonUeberauthDemoWeb do
    pipe_through :browser

    get "/:provider", UeberauthController, :request
    get "/:provider/callback", UeberauthController, :callback
    post "/:provider/callback", UeberauthController, :callback
end
Enter fullscreen mode Exit fullscreen mode

Now let's try that request again.

http://localhost:4000/auth/patreon
Enter fullscreen mode Exit fullscreen mode

That callback should succeed, and you will be redirected back to the root of your app. For example, if you look at your server console, you should see the following callback request.

[info] GET /auth/patreon/callback
[debug] Processing with PatreonUeberauthDemoWeb.AuthController.callback/2
  Parameters: %{"code" => "HqmYNWa1K7jPy44l38aXGpQeozaMjH", "provider" => "patreon", "state" => "pONpHnuU7mTCozQrtUJtJquM"}
Enter fullscreen mode Exit fullscreen mode

Nice, we have successfully implemented the first part of the CALLBACK phase, getting the passcode. But the work isn't over yet; we need to exchange that passcode for the token to actually do things on the user's behalf.

If you are curious and want to see the missing credential information. You can add an IO.inspect to your callback action in your controller to see.

defmodule PatreonUeberauthDemoWeb.AuthController do
  use PatreonUeberauthDemoWeb, :controller

  def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: "/")
  end

  def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
    IO.inpsect(auth) #### Check the console to see your emptry struct
    conn
    |> redirect(to: "/")
  end
end
Enter fullscreen mode Exit fullscreen mode

Let’s update our Patreon strategy to implement the token exchange step in our CALLBACK phase. We will be adding a couple of new functions, the most important ones being the handle_callback! and handle_cleanup functions. Your lib/ueberauth/strategy/patreon.ex file should look something like this now.

defmodule Ueberauth.Strategy.Patreon do
  use Ueberauth.Strategy,
    oauth2_module: Ueberauth.Strategy.Patreon.OAuth

  alias Ueberauth.Auth.Credentials

  def handle_request!(conn) do
    params =
      []
      |> with_state_param(conn)

    module = option(conn, :oauth2_module)
    redirect!(conn, apply(module, :authorize_url!, [params]))
  end

  # ///////// ----------- New function here --------------
  # This function will be called before our auth controller request handler
  def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do
    module = option(conn, :oauth2_module)
    # Uses our oauth module to perform the token fetch
    token = apply(module, :get_token!, [[code: code]])

    # After getting the token using our pass code
    if token.access_token == nil do
      set_errors!(conn, [
        error(token.other_params["error"], token.other_params["error_description"])
      ])
    else
      put_private(conn, :patreon_token, token)
    end
  end
  # ///////// ----------- New function here --------------
  def handle_callback!(conn) do
    set_errors!(conn, [error("missing_code", "No code received")])
  end

  # ///////// ----------- New function here --------------
  def handle_cleanup!(conn) do
    conn
    |> put_private(:patreon_token, nil)
  end

  # Gets called after handle_callback! to set the credentials struct with the token information
  def credentials(conn) do
    token = conn.private.patreon_token

    %Credentials{
      token: token.access_token,
      token_type: token.token_type,
      refresh_token: token.refresh_token,
      expires_at: token.expires_at
    }
  end

  defp option(conn, key) do
    Keyword.get(options(conn) || [], key, Keyword.get(default_options(), key))
  end
end
Enter fullscreen mode Exit fullscreen mode

We all need to update the OAuth module to implement the get_token logic. Your file should look something like this now.

defmodule Ueberauth.Strategy.Patreon.OAuth do
use OAuth2.Strategy

  @defaults [
    strategy: __MODULE__,
    site: "https://www.patreon.com/",
    authorize_url: "https://www.patreon.com/oauth2/authorize",
    token_url: "https://www.patreon.com/api/oauth2/token",
    token_method: :post
  ]

  def client(opts \\ []) do
    config =
      :ueberauth
      |> Application.fetch_env!(Ueberauth.Strategy.Patreon.OAuth)
      |> check_credential(:client_id)
      |> check_credential(:client_secret)

    client_opts =
      @defaults
      |> Keyword.merge(config)
      |> Keyword.merge(opts)

    json_library = Ueberauth.json_library()

    OAuth2.Client.new(client_opts)
    |> OAuth2.Client.put_serializer("application/json", json_library)
    |> OAuth2.Client.put_serializer("application/vnd.api+json", json_library)
  end

  def authorize_url!(params \\ [], opts \\ []) do
    opts
    |> client
    |> OAuth2.Client.authorize_url!(params)
  end

  def get_token!(params \\ [], options \\ []) do
    headers = Keyword.get(options, :headers, [])
    options = Keyword.get(options, :options, [])

    client_options = Keyword.get(options, :client_options, [])

    client = OAuth2.Client.get_token!(client(client_options), params, headers, options)

    client.token
  end

  # Strategy Callbacks
  def authorize_url(client, params) do
    OAuth2.Strategy.AuthCode.authorize_url(client, params)
  end

  # ///////// ----------- New function here --------------
  def get_token(client, params, headers) do
    client = client
    |> put_param("grant_type", "authorization_code")
    |> put_header("Accept", "application/json")

    OAuth2.Strategy.AuthCode.get_token(client, params, headers)
  end

  defp check_credential(config, key) do
    check_config_key_exists(config, key)

    case Keyword.get(config, key) do
      value when is_binary(value) ->
        config

      {:system, env_key} ->
        case System.get_env(env_key) do
          nil ->
            raise "#{inspect(env_key)} missing from environment, expected in config :ueberauth, Ueberauth.Strategy.Patreon"

          value ->
            Keyword.put(config, key, value)
        end
    end
  end

  defp check_config_key_exists(config, key) when is_list(config) do
    unless Keyword.has_key?(config, key) do
      raise "#{inspect(key)} missing from config :ueberauth, Ueberauth.Strategy.Patreon"
    end

    config
  end

  defp check_config_key_exists(_, _) do
    raise "Config :ueberauth, Ueberauth.Strategy.Patreon is not a keyword list, as expected"
  end
end
Enter fullscreen mode Exit fullscreen mode

We have implemented the second half of the CALLBACK phase, exchanging the passcode for a token. Initiate another OAuth flow be going to:

http://localhost:4000/auth/patreon
Enter fullscreen mode Exit fullscreen mode

If you check your console, you should see your credentials populated with the users access_token, refresh_token, and other details. Using this information, you can use authenticated Patreon API endpoints to perform all sorts of cool stuff.

It’s recommended from here to persist the access_token and refresh_token so you can use it anytime you need. We also didn't implement a way to specify your own scopes. But we won’t be covering that in this tutorial. I wanted you to have just enough information to be dangerous with Ueberauth, there are still a few more things you can do, but just this much goes a long way.

I recommend checking out the list of Ueberauth strategies to get some more inspiration for how you want to customize your CALLBACK phase https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies.

Top comments (1)

Collapse
 
andrewinsoul profile image
Okoye Andrew

Thank you very much for this piece, very very informative.