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
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"}
Now install our new dependency.
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.
#...
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.
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.
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.
# 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
.
# 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
When you are filling out the app form, make sure to set your callback URL to:
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.
defmodule PatreonUeberauthDemoWeb.AuthController do
use PatreonUeberauthDemoWeb, :controller
end
In router.ex
file, add the Ueberauth
plug to your pipeline.
pipeline :browser do
plug Ueberauth
...
end
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
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
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
@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
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
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
Now let's try that request again.
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.
[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.
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.
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.
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:
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.
Top comments (1)
Thank you very much for this piece, very very informative.