defmodule Ueberauth.Strategy do
@moduledoc """
The Strategy is the work-horse of the system.
Strategies are implemented outside this library to meet your needs, the
strategy provides a consistent API and behaviour.
Each strategy operates through two phases.
1. `request phase`
2. `callback phase`
These phases can be understood with the following psuedocode.
### Request Phase
request (for the request phase - default /auth/:provider)
|> relevant_strategy.handle_request!(conn)
|> continue with request plug pipeline
The request phase follows normal plug pipeline behaviour. The request will not
continue if the strategy halted the connection.
### Callback Phase
request (for a callback phase - default /auth/:provider/callback)
|> relevant_strategy.handle_auth!(conn)
if connection does not have ueberauth failure
|> set ueberauth auth with relevant_strategy.auth
|> cleanup from the strategy with relevant_strategy.handle_cleanup!
|> continue with plug pipeline
The callback phase is essentially a decorator and does not usually redirect or
halt the request. Its result is that one of two cases will end up in your
connections assigns when it reaches your controller.
* On Failure - An `Ueberauth.Failure` struct is available at `:ueberauth_failure`
* On Success - An `Ueberauth.Auth` struct is available at `:ueberauth_auth`
### An example
The simplest example is an email/password strategy. This does not intercept
the request and just decorates it with the `Ueberauth.Auth` struct. (it is
always successful)
defmodule Ueberauth.Strategies.Identity do
use Ueberauth.Strategy
alias Ueberauth.Auth.Credentials
alias Ueberauth.Auth.Extra
def uid(conn), do: conn.params["email"]
def extra(conn), do: struct(Extra, raw_info: conn.params)
def credentials(conn) do
%Credentials{
other: %{
password: conn.params["password"],
password_confirmation: conn.params["password_confirmation"]
}
}
end
end
After the strategy has run through the `c:handle_callback!/1` function, since
there are no errors added, Ueberauth will add the constructed auth struct to
the connection.
The Auth struct is constructed like:
def auth(conn, strategy) do
%Auth{
provider: Ueberauth.Strategy.Helpers.strategy_name(conn),
strategy: strategy,
uid: strategy.uid(conn),
info: strategy.info(conn),
extra: strategy.extra(conn),
credentials: strategy.credentials(conn)
}
end
Each component of the struct is a separate function and receives the connection
object. From this Ueberauth will construct and assign the struct for processing
in your own controller.
### Redirecting during the request phase
Many strategies may require a redirect (looking at you OAuth). To do this,
implement the `c:handle_request!/1` function.
def handle_request!(conn) do
callback_url = callback_url(conn)
redirect!(conn, callback_url)
end
### Callback phase
The callback phase may not do anything other than instruct the strategy where
to get the information to construct the auth struct. In that case define the
functions for the components of the struct and fetch the information from the
connection struct.
In the case where you do need to take some other step, the `c:handle_callback!/1`
function is where its at.
def handle_callback!(conn) do
conn
|> call_external_service_and_assign_result_to_private
end
def uid(conn) do
fetch_from_my_private_area(conn, :username)
end
def handle_cleanup!(conn) do
remove_my_private_area(conn)
end
This provides a simplistic psuedocode look at what a callback + cleanup phase
might look like. By setting the result of your call to the external service in
the connections private assigns, you can use that to construct the auth struct
in the auth component functions. Of course, as a good citizen you also cleanup
the connection before the request continues.
### Cleanup phase
The cleanup phase is provided for you to be a good citizen and clean up after
your strategy. During the callback phase, you may need to temporarily store
information in the private section of the conn struct. Once this is done,
the cleanup phase exists to cleanup that temporary storage after the strategy
has everything it needs.
Implement the `c:handle_cleanup!/1` function and return the cleaned conn struct.
### Adding errors during callback
You have two options when you're in the callback phase. Either you can let the
connection go through and Ueberauth will construct the auth hash for you, or
you can add errors.
You should add errors before you leave your `c:handle_callback!/1` function.
def handle_callback!(conn) do
errors = []
if (something_bad), do: errors = [error("error_key", "Some message") | errors]
if (length(errors) > 0) do
set_errors!(errors)
else
conn
end
end
Once you've set errors, Ueberauth will not set the auth struct in the connections
assigns at `:ueberauth_auth`, instead it will set a `Ueberauth.Failure` struct at
`:ueberauth_failure` with the information provided detailing the failure.
"""
alias Plug.Conn
alias Ueberauth.Strategy.Helpers
alias Ueberauth.Failure.Error
alias Ueberauth.Auth
alias Ueberauth.Auth.Credentials
alias Ueberauth.Auth.Info
alias Ueberauth.Auth.Extra
@state_param_cookie_name "ueberauth.state_param"
@doc """
The request phase implementation for your strategy.
Setup, redirect or otherwise in here. This is an information gathering phase
and should provide the end user with a way to provide the information
required for your application to authenticate them.
"""
@callback handle_request!(Plug.Conn.t()) :: Plug.Conn.t()
@doc """
The callback phase implementation for your strategy.
In this function you should make any external calls you need, check for
errors etc. The result of this phase is that either a failure
(`Ueberauth.Failure`) will be assigned to the connections assigns at
`ueberauth_failure` or an `Ueberauth.Auth` struct will be constrcted and
added to the assigns at `:ueberauth_auth`.
"""
@callback handle_callback!(Plug.Conn.t()) :: Plug.Conn.t()
@doc """
The cleanup phase implementation for your strategy.
The cleanup phase runs after the callback phase and is present to provide a
mechanism to cleanup any temporary data your strategy may have placed in the
connection.
"""
@callback handle_cleanup!(Plug.Conn.t()) :: Plug.Conn.t()
@doc """
Provides the uid for the user.
This is one of the component functions that is used to construct the auth
struct. What you return here will be in the auth struct at the `uid` key.
"""
@callback uid(Plug.Conn.t()) :: binary | nil
@doc """
Provides the info for the user.
This is one of the component functions that is used to construct the auth
struct. What you return here will be in the auth struct at the `info` key.
"""
@callback info(Plug.Conn.t()) :: Info.t()
@doc """
Provides the extra params for the user.
This is one of the component functions that is used to construct the auth
struct. What you return here will be in the auth struct at the `extra` key.
You would include any additional information within extra that does not fit
in either `info` or `credentials`
"""
@callback extra(Plug.Conn.t()) :: Extra.t()
@doc """
Provides the credentials for the user.
This is one of the component functions that is used to construct the auth
struct. What you return here will be in the auth struct at the `credentials`
key.
"""
@callback credentials(Plug.Conn.t()) :: Credentials.t()
@doc """
Returns the default options configuration of the strategy.
"""
@callback default_options() :: keyword()
@doc """
When defining your own strategy you should use Ueberauth.Strategy.
This provides default callbacks for all required callbacks to meet the
Ueberauth.Strategy behaviour and imports some helper functions found in
`Ueberauth.Strategy.Helpers`
### Imports
* Ueberauth.Stratgey.Helpers
* Plug.Conn
## Default Options
When using the strategy you can pass a keyword list for default options:
defmodule MyStrategy do
use Ueberauth.Strategy, some: "options"
# …
end
MyStrategy.default_options # [ some: "options" ]
These options are made available to your strategy at `YourStrategy.default_options`.
On a per usage level, other options can also be passed to the strategy to provide
customization.
### Cross-Site Request Forgery
By default strategies must implement https://tools.ietf.org/html/rfc6749#section-10.12
if you wish to disable this feature, you can use the `:ignores_csrf_attack` option:
defmodule MyStrategy do
use Ueberauth.Strategy,
ignores_csrf_attack: true
# …
end
We strongly recommend never disabling this feature, unless you have some technical
limitations that forces you to do so.
To change the SameSite attribute of the cookie holding the state parameter, you can use the `:state_param_cookie_same_site` option:
defmodule MyStrategy do
use Ueberauth.Strategy,
state_param_cookie_same_site: "None"
# …
end
"""
defmacro __using__(opts \\ []) do
quote location: :keep do
@behaviour Ueberauth.Strategy
import Ueberauth.Strategy.Helpers
import Plug.Conn, except: [request_url: 1]
def default_options, do: unquote(opts)
def uid(conn), do: nil
def info(conn), do: %Info{}
def extra(conn), do: %Extra{}
def credentials(conn), do: %Credentials{}
def handle_request!(conn), do: conn
def handle_callback!(conn), do: conn
def handle_cleanup!(conn), do: conn
defoverridable uid: 1,
info: 1,
extra: 1,
credentials: 1,
handle_request!: 1,
handle_callback!: 1,
handle_cleanup!: 1
end
end
@doc false
def run_request(conn, strategy) do
conn
|> maybe_add_state_param(strategy)
|> run_handle_request(strategy)
end
@doc false
def run_callback(conn, strategy) do
with false <- get_ignores_csrf_attack_option(strategy),
false <- state_param_matches?(conn) do
add_state_mismatch_error(conn, strategy)
else
true -> run_handle_callback(conn, strategy)
end
end
defp handle_callback_result(%{halted: true} = conn, _), do: conn
defp handle_callback_result(%{assigns: %{ueberauth_failure: _}} = conn, _), do: conn
defp handle_callback_result(%{assigns: %{ueberauth_auth: %{}}} = conn, _), do: conn
defp handle_callback_result(conn, strategy) do
Plug.Conn.assign(conn, :ueberauth_auth, auth(conn, strategy))
end
defp auth(conn, strategy) do
%Auth{
provider: Helpers.strategy_name(conn),
strategy: strategy,
uid: strategy.uid(conn),
info: strategy.info(conn),
extra: strategy.extra(conn),
credentials: strategy.credentials(conn)
}
end
defp state_param_matches?(conn) do
param_cookie = conn.params["state"]
not is_nil(param_cookie) and param_cookie == get_state_cookie(conn)
end
defp add_state_mismatch_error(conn, strategy) do
conn
|> Helpers.set_errors!([
%Error{message_key: "csrf_attack", message: "Cross-Site Request Forgery attack"}
])
|> run_handle_cleanup(strategy)
end
defp run_handle_request(conn, strategy) do
apply(strategy, :handle_request!, [conn])
end
defp run_handle_callback(conn, strategy) do
conn = remove_state_cookie(conn)
strategy
|> apply(:handle_callback!, [conn])
|> handle_callback_result(strategy)
|> run_handle_cleanup(strategy)
end
defp run_handle_cleanup(conn, strategy) do
apply(strategy, :handle_cleanup!, [conn])
end
defp maybe_add_state_param(conn, strategy) do
if get_ignores_csrf_attack_option(strategy) do
conn
else
add_state_param(conn, strategy)
end
end
defp get_state_param_cookie_same_site(strategy) do
strategy_option(strategy, :state_param_cookie_same_site, "Lax")
end
defp get_ignores_csrf_attack_option(strategy) do
strategy_option(strategy, :ignores_csrf_attack, false)
end
defp strategy_option(strategy, name, fallback) do
strategy
|> apply(:default_options, [])
|> Keyword.get(name, fallback)
end
defp add_state_param(conn, strategy) do
state = create_state_param()
conn
|> Conn.put_resp_cookie(@state_param_cookie_name, state,
same_site: get_state_param_cookie_same_site(strategy)
)
|> Helpers.add_state_param(state)
end
defp get_state_cookie(conn) do
conn
|> Conn.fetch_session()
|> Map.get(:cookies)
|> Map.get(@state_param_cookie_name)
end
defp remove_state_cookie(conn) do
Conn.delete_resp_cookie(conn, @state_param_cookie_name)
end
defp create_state_param do
24 |> :crypto.strong_rand_bytes() |> Base.url_encode64() |> binary_part(0, 24)
end
end