Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FAPI 2.0 Compliance #19

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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