Deploying Elixir: Creating Your Own Ueberauth Strategy
Tags: elixir, phoenix, tutorial, ueberauth
Published: 2022-02-28 16:13:32

## 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.

```bash
mix phx.new patreon-ueberauth-demo
```

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.

```elixir
{:ueberauth, "~> 0.6"},
{:oauth2, "~> 2.0"}
```

Now install our new dependency.

```bash
mix deps.get
```

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.

```elixir
#...

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

#...
import_config "#{config_env()}.exs"
```

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.

```elixir
config :ueberauth, Ueberauth.Strategy.Patreon.OAuth,
  client_id: System.get_env("PATREON_CLIENT_ID"),
  client_secret: System.get_env("PATREON_CLIENT_SECRET")
```

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. 

```elixir
import Config

config :ueberauth, Ueberauth.Strategy.Patreon.OAuth,
  client_id: "your_client_id",
  client_secret: "your_secret"
```

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.

```elixir
# Rest of dev.exs file

import_config "dev.secret.exs"
```

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`.

```elixir
# Other .gitignore stuffs

/config/dev.secret.exs
```

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](https://www.patreon.com/portal/registration/register-clients)

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

```elixir
http://localhost:4000/auth/patreon/callback
```

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. 

```elixir
defmodule PatreonUeberauthDemoWeb.AuthController do
  use PatreonUeberauthDemoWeb, :controller
end
```

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

```elixir
pipeline :browser do
    plug Ueberauth
    ...
end
```

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

```elixir
scope "/auth", PatreonUeberauthDemoWeb do
    pipe_through :browser

    get "/:provider", AuthController, :request
end
```

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.

```elixir
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
```

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 your`oauth.ex` file. This contains the Patreon OAuth information the OAuth2 package needs to make our lives a whole lot easier.

```elixir
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
```
@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:

```elixir
http://localhost:4000/auth/patreon
```

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.

```elixir
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
```

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

```elixir
scope "/auth", PatreonUeberauthDemoWeb do
    pipe_through :browser

    get "/:provider", UeberauthController, :request
    get "/:provider/callback", UeberauthController, :callback
    post "/:provider/callback", UeberauthController, :callback
end
```

Now let's try that request again.

```elixir
http://localhost:4000/auth/patreon
```

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.

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

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.

```elixir
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
```

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.

```elixir
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
```

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

```elixir
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
```

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

```elixir
http://localhost:4000/auth/patreon
```

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](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies).