defmodule Ueberauth.Strategy.Auth0 do
@moduledoc """
Provides an Ueberauth strategy for authenticating with Auth0.
You can edit the behaviour of the Strategy by including some options when
you register your provider.
To set the `uid_field`
config :ueberauth, Ueberauth,
providers: [
auth0: { Ueberauth.Strategy.Auth0, [uid_field: :email] }
]
Default is `:sub`
To set the default ['scope'](https://auth0.com/docs/scopes) (permissions):
config :ueberauth, Ueberauth,
providers: [
auth0: { Ueberauth.Strategy.Auth0, [default_scope: "openid profile email"] }
]
Default is `"openid profile email"`.
To set the [`audience`](https://auth0.com/docs/glossary#audience)
config :ueberauth, Ueberauth,
providers: [
auth0: { Ueberauth.Strategy.Auth0, [default_audience: "example-audience"] }
]
Not used by default (set to `""`).
To set the [`connection`](https://auth0.com/docs/identityproviders), mostly useful if
you want to use a social identity provider like `facebook` or `google-oauth2`. If empty
it will redirect to Auth0's Login widget. See https://auth0.com/docs/api/authentication#social
config :ueberauth, Ueberauth,
providers: [
auth0: { Ueberauth.Strategy.Auth0, [default_connection: "facebook"] }
]
Not used by default (set to `""`)
To set the [`state`](https://auth0.com/docs/protocols/oauth2/oauth-state). This is useful
to prevent from CSRF attacks and redirect users to the state before the authentication flow
started.
config :ueberauth, Ueberauth,
providers: [
auth0: { Ueberauth.Strategy.Auth0, [default_state: "some-opaque-state"] }
]
Not used by default (set to `""`)
These 4 parameters can also be set in the request to authorization. e.g.
You can call the `auth0` authentication endpoint with values:
`/auth/auth0?scope="some+new+scope&audience=events:read&connection=facebook&state=opaque_value`
## About the `state` param
Usually a static `state` value is not very useful so it's best to pass it to
the request endpoint as a parameter. You can then read back the state after
authentication in a private value set in the connection: `auth0_state`.
### Example
state_signed = Phoenix.Token.sign(MyApp.Endpoint, "return_url", Phoenix.Controller.current_url(conn))
Routes.auth_path(conn, :request, "auth0", state: state_signed)
# authentication happens ...
# the state ends up in `conn.private.auth0_state` after the authentication process
{:ok, redirect_to} = Phoenix.Token.verify(MyApp.Endpoint, "return_url", conn.private.auth0_state, max_age: 900)
"""
use Ueberauth.Strategy,
uid_field: :sub,
default_scope: "openid profile email",
default_audience: "",
default_connection: "",
default_prompt: "",
default_screen_hint: "",
default_login_hint: "",
default_organization: "",
# See https://auth0.com/docs/manage-users/organizations/configure-organizations/invite-members#specify-route-behavior
default_invitation: "",
allowed_request_params: [
:scope,
:state,
:audience,
:connection,
:prompt,
:screen_hint,
:login_hint,
:organization,
:invitation
],
oauth2_module: Ueberauth.Strategy.Auth0.OAuth
alias OAuth2.{Client, Error, Response}
alias Plug.Conn
alias Ueberauth.Auth.{Credentials, Extra, Info}
@doc """
Handles the redirect to Auth0.
"""
def handle_request!(conn) do
allowed_params =
conn
|> option(:allowed_request_params)
|> Enum.map(&to_string/1)
opts =
conn.params
|> maybe_replace_param(conn, "scope", :default_scope)
|> maybe_replace_param(conn, "audience", :default_audience)
|> maybe_replace_param(conn, "connection", :default_connection)
|> maybe_replace_param(conn, "prompt", :default_prompt)
|> maybe_replace_param(conn, "screen_hint", :default_screen_hint)
|> maybe_replace_param(conn, "login_hint", :default_login_hint)
|> maybe_replace_param(conn, "organization", :default_organization)
|> maybe_replace_param(conn, "invitation", :default_invitation)
|> Map.put("state", conn.private[:ueberauth_state_param])
|> Enum.filter(fn {k, _} -> Enum.member?(allowed_params, k) end)
# Remove empty params
|> Enum.reject(fn {_, v} -> blank?(v) end)
|> Enum.map(fn {k, v} -> {String.to_existing_atom(k), v} end)
|> Keyword.put(:redirect_uri, callback_url(conn))
module = option(conn, :oauth2_module)
callback_url = module.authorize_url!(opts, [otp_app: option(conn, :otp_app)])
redirect!(conn, callback_url)
end
@doc """
Handles the callback from Auth0. When there is a failure from Auth0 the failure is included in the
`ueberauth_failure` struct. Otherwise the information returned from Auth0 is returned in the `Ueberauth.Auth` struct.
"""
def handle_callback!(%Conn{params: %{"code" => _}} = conn) do
{code, state} = parse_params(conn)
module = option(conn, :oauth2_module)
redirect_uri = callback_url(conn)
result = module.get_token!([code: code, redirect_uri: redirect_uri], [otp_app: option(conn, :otp_app)])
case result do
{:ok, client} ->
token = client.token
if token.access_token == nil do
set_errors!(conn, [
error(
token.other_params["error"],
token.other_params["error_description"]
)
])
else
fetch_user(conn, client, state)
end
{:error, client} ->
set_errors!(conn, [error(client.body["error"], client.body["error_description"])])
end
end
@doc false
def handle_callback!(conn) do
set_errors!(conn, [error("missing_code", "No code received")])
end
@doc """
Cleans up the private area of the connection used for passing the raw Auth0 response around during the callback.
"""
def handle_cleanup!(conn) do
conn
|> put_private(:auth0_user, nil)
|> put_private(:auth0_token, nil)
end
defp fetch_user(conn, %{token: token} = client, state) do
conn =
conn
|> put_private(:auth0_token, token)
|> put_private(:auth0_state, state)
case Client.get(client, "/userinfo") do
{:ok, %Response{status_code: 401, body: _body}} ->
set_errors!(conn, [error("token", "unauthorized")])
{:ok, %Response{status_code: status_code, body: user}}
when status_code in 200..399 ->
put_private(conn, :auth0_user, user)
{:error, %Response{body: body}} ->
set_errors!(conn, [error("OAuth2", body)])
{:error, %Error{reason: reason}} ->
set_errors!(conn, [error("OAuth2", reason)])
end
end
@doc """
Fetches the uid field from the Auth0 response.
"""
def uid(conn) do
conn.private.auth0_user[to_string(option(conn, :uid_field))]
end
@doc """
Includes the credentials from the Auth0 response.
"""
def credentials(conn) do
token = conn.private.auth0_token
scopes =
(token.other_params["scope"] || "")
|> String.split(",")
%Credentials{
token: token.access_token,
refresh_token: token.refresh_token,
token_type: token.token_type,
expires_at: token.expires_at,
expires: token_expired(token),
scopes: scopes,
other: token.other_params
}
end
defp token_expired(%{expires_at: nil}), do: false
defp token_expired(%{expires_at: _}), do: true
@doc """
Populates the extra section of the `Ueberauth.Auth` struct with auth0's
additional information from the `/userinfo` user profile and includes the
token received from Auth0 callback.
"""
def extra(conn) do
%Extra{
raw_info: %{
token: conn.private.auth0_token,
user: conn.private.auth0_user
}
}
end
@doc """
Fetches the fields to populate the info section of the `Ueberauth.Auth` struct.
This field has been changed from 0.5.0 to 0.6.0 to better reflect
fields of the OpenID standard claims. Extra fields provided by
auth0 are in the `Extra` struct.
"""
def info(conn) do
user = conn.private.auth0_user
%Info{
name: user["name"],
first_name: user["given_name"],
last_name: user["family_name"],
nickname: user["nickname"],
email: user["email"],
# The `locale` auth0 field has been moved to `Extra` to better follow OpenID standard specs.
# The `location` field of `Ueberauth.Auth.Info` is intended for location (city, country, ...)
# information while the `locale` information returned by auth0 is used for internationalization.
# There is no location field in the auth0 response, only an `address`.
location: nil,
description: nil,
image: user["picture"],
phone: user["phone_number"],
birthday: user["birthdate"],
urls: %{
profile: user["profile"],
website: user["website"]
}
}
end
defp parse_params(%Plug.Conn{params: %{"code" => code, "state" => state}}) do
{code, state}
end
defp parse_params(%Plug.Conn{params: %{"code" => code}}) do
{code, nil}
end
defp option(conn, key) do
default = Keyword.get(default_options(), key)
conn
|> options
|> Keyword.get(key, default)
end
defp option(nil, conn, key), do: option(conn, key)
defp option(value, _conn, _key), do: value
defp maybe_replace_param(params, conn, name, config_key) do
if params[name] do
params
else
Map.put(params, name, option(params[name], conn, config_key))
end
end
@compile {:inline, blank?: 1}
def blank?(""), do: true
def blank?([]), do: true
def blank?(nil), do: true
def blank?({}), do: true
def blank?(%{} = map) when map_size(map) == 0, do: true
def blank?(_), do: false
end