Current section
Files
Jump to
Current section
Files
lib/plugoid/redirect_uri.ex
defmodule Plugoid.RedirectURI do
@moduledoc """
Plug to configure the application redirect URI
An OAuth2 / OpenID Connect redirect URI is a vanity, non-dynamic URI. The authorization
server redirects to this URI after authentication and authorization success or failure.
## Automatic configuration in a router
defmodule Myapp.Router do
use Plugoid.RedirectURI
end
installs a route to `/openid_connect_redirect_uri` in a Phoenix router.
## Determining the redirect URI
When using `Plugoid.RedirectURI`, an `plugoid_redirect_uri/2` function is automatically
installed in the router. It takes the endpoint as the first parameter and the issuer
as the second:
iex> PlugoidDemoWeb.Router.plugoid_redirect_uri(PlugoidDemoWeb.Endpoint, "https://issuer.example.com/auth")
"http://localhost:4000/openid_connect_redirect_uri?iss=https://issuer.example.com/auth"
It can be called without the endpoint, in which case it is inferred from the router's
module name:
iex> PlugoidDemoWeb.Router.plugoid_redirect_uri("https://issuer.example.com/auth")
"http://localhost:4000/openid_connect_redirect_uri?iss=https://issuer.example.com/auth"
## Options
- `:error_view`: the error view to be called in case of error. The `:"500"` template is
rendered in case of error (bascially, when the `state` parameter is missing from the response).
If not set, it will be automatically set to `MyApp.ErrorView` where `MyApp` is the
base module name of the application
- `:jti_register`: a module implementing the `JTIRegister` behaviour, to check the ID
Token against replay attack when a nonce is used (in the implicit and hybrid flows).
See also [`JTIRegister`](https://github.com/tanguilp/jti_register)
- `:path`: the path of the redirect URI. Defaults to `"openid_connect_redirect_uri"`
- `:token_callback`: a `t:token_callback/0` function to which are passed the received
tokens, for further use (for example, to store a refresh token)
Options of `t:OIDC.Auth.verify_opts/0` which will be passed to `OIDC.Auth.verify_response/3`.
"""
@behaviour Plug
alias OIDC.Auth.{
Challenge,
OPResponseSuccess
}
alias Plugoid.{
OIDCRequest,
Session.AuthSession,
Session.StateSession,
Utils
}
@type opts :: [opt() | OIDC.Auth.verify_opt()]
@type opt ::
{:error_view, module()}
| {:jti_register, module()}
| {:path, String.t()}
| {:token_callback, token_callback()}
@type token_callback :: (
OPResponseSuccess.t(),
issuer :: String.t(),
client_id :: String.t(),
opts()
-> any())
defmodule MissingStateParamaterError do
defexception message: "state parameter is missing from OP's response"
end
defmodule MissingRedirectQueryParamError do
defexception message: "a query param of the requested redirect URI is missing in the response"
end
defmacro __using__(opts) do
quote do
def plugoid_redirect_uri(endpoint \\ nil, issuer) do
endpoint =
if endpoint do
endpoint
else
Module.split(__MODULE__)
|> List.pop_at(-1)
|> elem(1)
|> Kernel.++([Endpoint])
|> Module.safe_concat()
end
base_redirect_uri =
apply(
Module.concat(__MODULE__, Helpers),
:openid_connect_redirect_uri_url,
[endpoint, :call]
)
base_redirect_uri <> "?iss=" <> URI.encode(issuer)
end
pipeline :oidc_redirect_pipeline do
plug :accepts, ["html"]
plug :fetch_query_params
plug Plug.Parsers, parsers: [:urlencoded], pass: ["*/*"]
end
scope unquote(opts[:path]) || "/openid_connect_redirect_uri", Plugoid do
pipe_through :oidc_redirect_pipeline
get "/", RedirectURI, :call,
as: :openid_connect_redirect_uri,
private: %{plugoid: unquote(opts)}
post "/", RedirectURI, :call,
as: :openid_connect_redirect_uri,
private: %{plugoid: unquote(opts)}
end
end
end
@impl true
def init(opts), do: opts
@impl true
def call(conn, _opts) do
opts = conn.private[:plugoid]
with {:ok, op_response} <- extract_params(conn),
%{"state" => state} = op_response,
{:ok, {conn, request}} <- StateSession.get_and_delete_oidc_request(conn, state),
:ok <- verify_redirect_uri_query_params(conn, request)
do
case OIDC.Auth.verify_response(op_response, request.challenge, opts) do
{:ok, %OPResponseSuccess{} = response} ->
maybe_register_nonce(response.id_token_claims, opts)
maybe_execute_token_callback(response, request.challenge, opts)
conn
|> AuthSession.set_authenticated(request.challenge.issuer, response)
|> Phoenix.Controller.redirect(to: request.initial_request_path <> "?redirected")
{:error, error} ->
# we compress to the maximum to avoid browser URL length limitations
error_token = Phoenix.Token.sign(
conn,
"plugoid error token",
:erlang.term_to_binary(error, compressed: 9)
)
redirect_to = request.initial_request_path <> "?oidc_error=" <> error_token
Phoenix.Controller.redirect(conn, to: redirect_to)
end
else
{:error, reason} ->
conn
|> Plug.Conn.put_status(:internal_server_error)
|> Phoenix.Controller.put_view(error_view(conn))
|> Phoenix.Controller.render(:"500", error: reason)
end
end
@spec extract_params(Plug.Conn.t()) :: {:ok, map()} | {:error, atom()}
defp extract_params(conn) do
case conn.method do
"GET" ->
conn.query_params
"POST" ->
conn.body_params
end
|> case do
%{"state" => _} = params ->
{:ok, params}
%{} ->
{:error, %MissingStateParamaterError{}}
end
end
@spec verify_redirect_uri_query_params(
Plug.Conn.t(), OIDCRequest.t()
) :: :ok | {:error, Exception.t()}
defp verify_redirect_uri_query_params(conn, oidc_request) do
req_query_params =
URI.parse(oidc_request.challenge.redirect_uri).query
|> URI.decode_query()
|> MapSet.new()
resp_query_params = conn.query_params |> MapSet.new()
if MapSet.subset?(req_query_params, resp_query_params) do
:ok
else
{:error, %MissingRedirectQueryParamError{}}
end
end
@spec error_view(Plug.Conn.t()) :: module()
defp error_view(conn),
do: conn.private[:plugoid][:error_view] || Utils.error_view_from_conn(conn)
@spec maybe_register_nonce(OIDC.id_token_claims(), Keyword.t()) :: any()
defp maybe_register_nonce(%{"nonce" => nonce, "exp" => exp}, opts) do
case opts[:jti_register] do
impl when is_atom(impl) ->
impl.register(nonce, exp)
_ ->
:ok
end
end
defp maybe_register_nonce(_, _) do
:ok
end
@spec maybe_execute_token_callback(
OPResponseSuccess.t(),
Challenge.t(),
opts()
) :: any()
defp maybe_execute_token_callback(response, challenge, opts) do
if opts[:token_callback] do
opts[:token_callback].(
response,
challenge.issuer,
challenge.client_id,
opts[:token_callback_opts] || []
)
end
end
end