Skip to content

Commit

Permalink
FAPI 2.0 Compliance
Browse files Browse the repository at this point in the history
  • Loading branch information
maennchen committed Apr 30, 2024
1 parent 2275f7f commit 5931551
Show file tree
Hide file tree
Showing 5 changed files with 426 additions and 171 deletions.
114 changes: 90 additions & 24 deletions lib/oidcc/plug/authorization_callback.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ defmodule Oidcc.Plug.AuthorizationCallback do

@behaviour Plug

alias Oidcc.ClientContext
alias Oidcc.Plug.Authorize
alias Oidcc.ProviderConfiguration
alias Oidcc.Token
alias Oidcc.Userinfo

import Plug.Conn,
only: [get_session: 2, delete_session: 2, put_private: 3, get_peer_data: 1, get_req_header: 2]
Expand All @@ -95,6 +99,8 @@ defmodule Oidcc.Plug.AuthorizationCallback do
* `provider` - name of the `Oidcc.ProviderConfiguration.Worker`
* `client_id` - OAuth Client ID to use for the introspection
* `client_secret` - OAuth Client Secret to use for the introspection
* `client_context_opts` - Options for Client Context Initialization
* `client_profile_opts` - Options for Client Context Profiles
* `redirect_uri` - Where to redirect for callback
* `check_useragent` - check if useragent is the same as before the
authorization request
Expand All @@ -108,6 +114,8 @@ defmodule Oidcc.Plug.AuthorizationCallback do
provider: GenServer.name(),
client_id: String.t() | (-> String.t()),
client_secret: String.t() | (-> String.t()),
client_context_opts: :oidcc_client_context.opts() | (-> :oidcc_client_context.opts()),
client_profile_opts: :oidcc_profile.opts(),
redirect_uri: String.t() | (-> String.t()),
check_useragent: boolean(),
check_peer_ip: boolean(),
Expand All @@ -131,6 +139,8 @@ defmodule Oidcc.Plug.AuthorizationCallback do
:provider,
:client_id,
:client_secret,
:client_context_opts,
:client_profile_opts,
:redirect_uri,
:preferred_auth_methods,
check_useragent: true,
Expand All @@ -145,22 +155,50 @@ defmodule Oidcc.Plug.AuthorizationCallback do
client_id = opts |> Keyword.fetch!(:client_id) |> evaluate_config()
client_secret = opts |> Keyword.fetch!(:client_secret) |> evaluate_config()
redirect_uri = opts |> Keyword.fetch!(:redirect_uri) |> evaluate_config()
client_context_opts = opts |> Keyword.get(:client_context_opts, %{}) |> evaluate_config()
client_profile_opts = opts |> Keyword.get(:client_profile_opts, %{profiles: []})

params = Map.merge(params, body_params)

%{nonce: nonce, peer_ip: peer_ip, useragent: useragent, pkce_verifier: pkce_verifier} =
%{
nonce: nonce,
peer_ip: peer_ip,
useragent: useragent,
pkce_verifier: pkce_verifier,
state_verifier: state_verifier
} =
case get_session(conn, Authorize.get_session_name()) do
nil -> %{nonce: :any, peer_ip: nil, useragent: nil, pkce_verifier: :none}
%{} = session -> session
nil ->
%{
nonce: :any,
peer_ip: nil,
useragent: nil,
pkce_verifier: :none,
state_verifier: :none
}

%{} = session ->
session
end

check_peer_ip? = Keyword.fetch!(opts, :check_peer_ip)
check_useragent? = Keyword.fetch!(opts, :check_useragent)
retrieve_userinfo? = Keyword.fetch!(opts, :retrieve_userinfo)

result =
with :ok <- check_peer_ip(conn, peer_ip, check_peer_ip?),
with {:ok, client_context} <-
ClientContext.from_configuration_worker(
provider,
client_id,
client_secret,
client_context_opts
),
{:ok, client_context, profile_opts} <-
apply_profile(client_context, client_profile_opts),
:ok <- check_peer_ip(conn, peer_ip, check_peer_ip?),
:ok <- check_useragent(conn, useragent, check_useragent?),
:ok <- check_state(params, state_verifier),
:ok <- check_issuer_request_param(params, client_context),
{:ok, code} <- fetch_request_param(params, "code"),
scope = Map.get(params, "scope", "openid"),
scopes = :oidcc_scope.parse(scope),
Expand All @@ -177,14 +215,12 @@ defmodule Oidcc.Plug.AuthorizationCallback do
{:ok, token} <-
retrieve_token(
code,
provider,
client_id,
client_secret,
client_context,
retrieve_userinfo?,
token_opts
Map.merge(profile_opts, token_opts)
),
{:ok, userinfo} <-
retrieve_userinfo(token, provider, client_id, client_secret, retrieve_userinfo?) do
retrieve_userinfo(token, client_context, retrieve_userinfo?) do
{:ok, {token, userinfo}}
end

Expand Down Expand Up @@ -234,16 +270,47 @@ defmodule Oidcc.Plug.AuthorizationCallback do
end
end

defp check_issuer_request_param(params, client_context)

defp check_issuer_request_param(params, %ClientContext{
provider_configuration: %ProviderConfiguration{
issuer: issuer,
authorization_response_iss_parameter_supported: true
}
}) do
with {:ok, given_issuer} <- fetch_request_param(params, "iss") do
if issuer == given_issuer do
:ok
else
{:error, {:invalid_issuer, given_issuer}}
end
end
end

defp check_issuer_request_param(_params, _client_context), do: :ok

defp check_state(params, state_verifier)
defp check_state(%{"state" => _state}, :none), do: {:error, :state_not_verified}
defp check_state(_params, :none), do: :ok

defp check_state(%{"state" => state}, state_verifier) do
if :erlang.phash2(state) == state_verifier do
:ok
else
{:error, :state_not_verified}
end
end

defp check_state(_params, _state), do: :ok

@spec retrieve_token(
code :: String.t(),
provider :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
client_context :: ClientContext.t(),
retrieve_userinfo? :: boolean(),
token_opts :: :oidcc_token.retrieve_opts()
) :: {:ok, Oidcc.Token.t()} | {:error, error()}
defp retrieve_token(code, provider, client_id, client_secret, retrieve_userinfo?, token_opts) do
case Oidcc.retrieve_token(code, provider, client_id, client_secret, token_opts) do
defp retrieve_token(code, client_context, retrieve_userinfo?, token_opts) do
case Token.retrieve(code, client_context, token_opts) do
{:ok, token} -> {:ok, token}
{:error, {:none_alg_used, token}} when retrieve_userinfo? -> {:ok, token}
{:error, reason} -> {:error, reason}
Expand All @@ -252,21 +319,20 @@ defmodule Oidcc.Plug.AuthorizationCallback do

@spec retrieve_userinfo(
token :: Oidcc.Token.t(),
provider :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
client_context :: ClientContext.t(),
retrieve_userinfo? :: true
) :: {:ok, :oidcc_jwt_util.claims()} | {:error, error()}
@spec retrieve_userinfo(
token :: Oidcc.Token.t(),
provider :: GenServer.name(),
client_id :: String.t(),
client_secret :: String.t(),
client_context :: ClientContext.t(),
retrieve_userinfo? :: false
) :: {:ok, nil} | {:error, error()}
defp retrieve_userinfo(token, provider, client_id, client_secret, retrieve_userinfo?)
defp retrieve_userinfo(_token, _provider, _client_id, _client_secret, false), do: {:ok, nil}
defp retrieve_userinfo(token, client_context, retrieve_userinfo?)
defp retrieve_userinfo(_token, _client_context, false), do: {:ok, nil}

defp retrieve_userinfo(token, client_context, true),
do: Userinfo.retrieve(token, client_context, %{})

defp retrieve_userinfo(token, provider, client_id, client_secret, true),
do: Oidcc.retrieve_userinfo(token, provider, client_id, client_secret, %{})
defp apply_profile(client_context, profile_opts),
do: ClientContext.apply_profiles(client_context, profile_opts)
end
57 changes: 43 additions & 14 deletions lib/oidcc/plug/authorize.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ defmodule Oidcc.Plug.Authorize do
## Query Params
* `state` - STate to relay to OpenID Provider. Commonly used for target redirect
* `state` - State to relay to OpenID Provider. Commonly used for target redirect
URL after authorization.
"""
@moduledoc since: "0.1.0"

@behaviour Plug

alias Oidcc.Authorization
alias Oidcc.ClientContext

import Plug.Conn,
only: [send_resp: 3, put_resp_header: 3, put_session: 3, get_peer_data: 1, get_req_header: 2]

Expand Down Expand Up @@ -57,6 +60,8 @@ defmodule Oidcc.Plug.Authorize do
* `provider` - name of the `Oidcc.ProviderConfiguration.Worker`
* `client_id` - OAuth Client ID to use for the introspection
* `client_secret` - OAuth Client Secret to use for the introspection
* `client_context_opts` - Options for Client Context Initialization
* `client_profile_opts` - Options for Client Context Profiles
"""
@typedoc since: "0.1.0"
@type opts :: [
Expand All @@ -65,7 +70,9 @@ defmodule Oidcc.Plug.Authorize do
url_extension: :oidcc_http_util.query_params(),
provider: GenServer.name(),
client_id: String.t() | (-> String.t()),
client_secret: String.t() | (-> String.t())
client_secret: String.t() | (-> String.t()),
client_context_opts: :oidcc_client_context.opts() | (-> :oidcc_client_context.opts()),
client_profile_opts: :oidcc_profile.opts()
]

@impl Plug
Expand All @@ -76,6 +83,8 @@ defmodule Oidcc.Plug.Authorize do
:client_id,
:client_secret,
:redirect_uri,
:client_context_opts,
:client_profile_opts,
url_extension: [],
scopes: ["openid"]
])
Expand All @@ -86,8 +95,12 @@ defmodule Oidcc.Plug.Authorize do
client_id = opts |> Keyword.fetch!(:client_id) |> evaluate_config()
client_secret = opts |> Keyword.fetch!(:client_secret) |> evaluate_config()
redirect_uri = opts |> Keyword.fetch!(:redirect_uri) |> evaluate_config()
client_context_opts = opts |> Keyword.get(:client_context_opts, %{}) |> evaluate_config()
client_profile_opts = opts |> Keyword.get(:client_profile_opts, %{profiles: []})

state = Map.get(params, "state", :undefined)
state_verifier = :erlang.phash2(state)

nonce = 31 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false)
pkce_verifier = 96 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false)

Expand All @@ -106,23 +119,39 @@ defmodule Oidcc.Plug.Authorize do
)
|> Map.new()

case Oidcc.create_redirect_url(provider, client_id, client_secret, authorization_opts) do
{:ok, redirect_uri} ->
conn
|> put_session(get_session_name(), %{
nonce: nonce,
peer_ip: peer_ip,
useragent: useragent,
pkce_verifier: pkce_verifier
})
|> put_resp_header("location", IO.iodata_to_binary(redirect_uri))
|> send_resp(302, "")

with {:ok, client_context} <-
ClientContext.from_configuration_worker(
provider,
client_id,
client_secret,
client_context_opts
),
{:ok, client_context, profile_opts} <-
apply_profile(client_context, client_profile_opts),
{:ok, redirect_uri} <-
Authorization.create_redirect_url(
client_context,
Map.merge(profile_opts, authorization_opts)
) do
conn
|> put_session(get_session_name(), %{
nonce: nonce,
peer_ip: peer_ip,
useragent: useragent,
pkce_verifier: pkce_verifier,
state_verifier: state_verifier
})
|> put_resp_header("location", IO.iodata_to_binary(redirect_uri))
|> send_resp(302, "")
else
{:error, reason} ->
raise Error, reason: reason
end
end

defp apply_profile(client_context, profile_opts),
do: ClientContext.apply_profiles(client_context, profile_opts)

@doc false
@spec get_session_name :: String.t()
def get_session_name, do: inspect(__MODULE__)
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ defmodule Oidcc.Plug.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:oidcc, "~> 3.0"},
{:oidcc, "~> 3.2.0"},
{:plug, "~> 1.14"},
{:ex_doc, "~> 0.29", only: :dev, runtime: false},
{:excoveralls, "~> 0.18.1", only: :test, runtime: false},
Expand Down
Loading

0 comments on commit 5931551

Please sign in to comment.