Skip to content

Commit

Permalink
Merge pull request #3 from kanes115/federation_logout_binding
Browse files Browse the repository at this point in the history
  • Loading branch information
khamilowicz authored Mar 3, 2020
2 parents 513525c + 7bcc21b commit 83c26bc
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 50 deletions.
1 change: 0 additions & 1 deletion lib/samly.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,4 @@ defmodule Samly do
conn
|> Conn.delete_session("samly_assertion_key")
end

end
45 changes: 33 additions & 12 deletions lib/samly/auth_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ defmodule Samly.AuthHandler do
import Plug.Conn
alias Samly.{Assertion, IdpData, Helper, State, Subject}

import Samly.RouterUtil, only: [ensure_sp_uris_set: 2, send_saml_request: 5, redirect: 3]
import Samly.RouterUtil,
only: [ensure_sp_uris_set: 2, send_saml_request: 5, send_saml_request: 6, redirect: 3]

@sso_init_resp_template """
<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"
Expand Down Expand Up @@ -41,6 +42,7 @@ defmodule Samly.AuthHandler do
import Plug.CSRFProtection, only: [get_csrf_token: 0]

target_url = conn.private[:samly_target_url] || "/"
target_url = conn.params["target_url"]

opts = [
nonce: conn.private[:samly_nonce],
Expand All @@ -55,7 +57,9 @@ defmodule Samly.AuthHandler do
end

def send_signin_req(%{host: host} = conn) do
%IdpData{id: idp_id} = idp = conn.private[:samly_idp]
%IdpData{id: idp_id, sso_post_url: sso_post, sso_redirect_url: sso_redirect} =
idp = conn.private[:samly_idp]

%IdpData{esaml_idp_rec: idp_rec, esaml_sp_rec: sp_rec} = idp
sp = ensure_sp_uris_set(sp_rec, conn)

Expand All @@ -81,7 +85,10 @@ defmodule Samly.AuthHandler do
idp_signin_url,
idp.use_redirect_for_req,
req_xml_frag,
relay_state
relay_state,
sp: sp,
sso_post: sso_post,
sso_redirect: sso_redirect
)
end

Expand All @@ -91,23 +98,31 @@ defmodule Samly.AuthHandler do
# conn |> send_resp(500, "request_failed")
end


def send_signout_req(conn) do
%IdpData{id: idp_id} = idp = conn.private[:samly_idp]
%IdpData{id: idp_id, slo_post_url: slo_post, slo_redirect_url: slo_redirect} =
idp = conn.private[:samly_idp]

%IdpData{esaml_idp_rec: idp_rec, esaml_sp_rec: sp_rec} = idp
%IdpData{pre_logout_pipeline: pipeline} = idp
sp = ensure_sp_uris_set(sp_rec, conn)

target_url = conn.private[:samly_target_url] || "/"
target_url = conn.params["target_url"] |> URI.decode_www_form()

assertion_key = get_session(conn, "samly_assertion_key")

case State.get_assertion(conn, assertion_key) do
%Assertion{idp_id: ^idp_id, authn: authn, subject: subject} ->
session_index = Map.get(authn, "session_index", "")
subject_rec = Subject.to_rec(subject)

{idp_signout_url, req_xml_frag} =
Helper.gen_idp_signout_req(sp, idp_rec, subject_rec, session_index)
{:ok, {idp_signout_url, req_xml_frag}} =
Helper.gen_idp_signout_req(sp, idp_rec, subject_rec, session_index,
slo_post: slo_post,
slo_redirect: slo_redirect,
use_redirect?: idp.use_redirect_for_logout_req
)

conn = pipethrough(conn, pipeline)
conn = State.delete_assertion(conn, assertion_key)
relay_state = State.gen_id()

Expand All @@ -118,9 +133,12 @@ defmodule Samly.AuthHandler do
|> delete_session("samly_assertion_key")
|> send_saml_request(
idp_signout_url,
idp.use_redirect_for_req,
idp.use_redirect_for_logout_req,
req_xml_frag,
relay_state
relay_state,
sp: sp,
slo_post: slo_post,
slo_redirect: slo_redirect
)

_ ->
Expand All @@ -132,10 +150,13 @@ defmodule Samly.AuthHandler do
# Logger.error("#{inspect error}")
# conn |> send_resp(500, "request_failed")
end


defp pipethrough(conn, nil), do: conn
defp pipethrough(conn, pipeline), do: pipeline.call(conn, [])

defp strip_subdomains(host, n_of_subdomains) do
host
|> String.split(".", parts: n_of_subdomains + 1)
|> List.last
|> List.last()
end
end
42 changes: 39 additions & 3 deletions lib/samly/helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,46 @@ defmodule Samly.Helper do
{idp_signin_url, xml_frag}
end

def gen_idp_signout_req(sp, idp_metadata, subject_rec, session_index) do
def gen_idp_signout_req(sp, idp_metadata, subject_rec, session_index, opts) do
idp_signout_url = Esaml.esaml_idp_metadata(idp_metadata, :logout_location)
xml_frag = :esaml_sp.generate_logout_request(idp_signout_url, session_index, subject_rec, sp)
{idp_signout_url, xml_frag}

case get_binding_type(opts) do
{:ok, binding_type} ->
xml_frag =
:esaml_sp.generate_logout_request(
idp_signout_url,
session_index,
subject_rec,
sp,
binding_type: binding_type
)

{:ok, {idp_signout_url, xml_frag}}

error ->
error
end
end

defp get_binding_type(opts) do
# TODO refactor
use_redirect? = Keyword.get(opts, :use_redirect?)
redirect = Keyword.get(opts, :slo_redirect)
post = Keyword.get(opts, :slo_post)

if use_redirect? do
if redirect do
{:ok, :redirect}
else
{:error, :no_binding}
end
else
if post do
{:ok, :post}
else
{:error, :no_binding}
end
end
end

def gen_idp_signout_resp(sp, idp_metadata, signout_status) do
Expand Down
60 changes: 35 additions & 25 deletions lib/samly/idp_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Samly.IdpData do
base_url: nil,
metadata_file: nil,
pre_session_create_pipeline: nil,
pre_logout_pipeline: nil,
use_redirect_for_req: false,
sign_requests: true,
sign_metadata: true,
Expand All @@ -41,6 +42,7 @@ defmodule Samly.IdpData do
base_url: nil | binary(),
metadata_file: nil | binary(),
pre_session_create_pipeline: nil | module(),
pre_logout_pipeline: nil | module(),
use_redirect_for_req: boolean(),
sign_requests: boolean(),
sign_metadata: boolean(),
Expand Down Expand Up @@ -77,13 +79,13 @@ defmodule Samly.IdpData do
@signing_keys_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l
@enc_keys_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@keydesc}[@use = 'encryption']"l


# These functions work on EntityDescriptor element
@sso_redirect_url_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@redirect}']/@Location"s
@sso_post_url_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@post}']/@Location"s
@slo_redirect_url_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s
@slo_post_url_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@post}']/@Location"s
@nameid_format_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@nameid}/text()[1]"s # TODO How to deal with multiple nameid formats?
# TODO How to deal with multiple nameid formats?
@nameid_format_selector ~x"/#{@entdesc}/#{@idpdesc}/#{@nameid}/text()[1]"s
@signing_keys_in_idp_selector ~x"./#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l
@cert_selector ~x"./ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s

Expand Down Expand Up @@ -116,9 +118,10 @@ defmodule Samly.IdpData do
when is_binary(id) and is_binary(sp_id) do
%IdpData{idp_data | id: id, sp_id: sp_id, base_url: Map.get(opts_map, :base_url)}
|> set_metadata_file(opts_map)
|> set_pipeline(opts_map)
|> set_pipelines(opts_map)
|> set_allowed_target_urls(opts_map)
|> set_boolean_attr(opts_map, :use_redirect_for_req)
|> set_boolean_attr(opts_map, :use_redirect_for_logout_req)
|> set_boolean_attr(opts_map, :sign_requests)
|> set_boolean_attr(opts_map, :sign_metadata)
|> set_boolean_attr(opts_map, :signed_assertion_in_resp)
Expand Down Expand Up @@ -193,10 +196,16 @@ defmodule Samly.IdpData do
%IdpData{idp_data | metadata_file: Map.get(opts_map, :metadata_file, @default_metadata_file)}
end

@spec set_pipeline(%IdpData{}, map()) :: %IdpData{}
defp set_pipeline(%IdpData{} = idp_data, %{} = opts_map) do
@spec set_pipelines(%IdpData{}, map()) :: %IdpData{}
defp set_pipelines(%IdpData{} = idp_data, %{} = opts_map) do
pipeline = Map.get(opts_map, :pre_session_create_pipeline)
%IdpData{idp_data | pre_session_create_pipeline: pipeline}
logout_pipeline = Map.get(opts_map, :pre_logout_pipeline)

%IdpData{
idp_data
| pre_session_create_pipeline: pipeline,
pre_logout_pipeline: logout_pipeline
}
end

defp set_allowed_target_urls(%IdpData{} = idp_data, %{} = opts_map) do
Expand Down Expand Up @@ -276,34 +285,35 @@ defmodule Samly.IdpData do

entity_md_xml = get_entity_descriptor(md_xml, entityID)



case entity_md_xml do
nil ->
Logger.warn("[Samly] Entity #{inspect(entityID)} not found")
{:ok, idp_data}

{:error, :entity_not_found} = err ->
Logger.warn("[Samly] Entity not found due to configuration error")
{:ok, idp_data}

{:error, reason} ->
Logger.warn("[Samly] Parsing error due to: #{inspect(reason)}")
{:ok, idp_data}

_ ->
signing_certs = get_signing_certs_in_idp(entity_md_xml)

{:ok,
%IdpData{
idp_data
| entity_id: entityID,
signed_requests: get_req_signed(md_xml),
certs: signing_certs,
fingerprints: idp_cert_fingerprints(signing_certs),
sso_redirect_url: get_sso_redirect_url(entity_md_xml),
sso_post_url: get_sso_post_url(entity_md_xml),
slo_redirect_url: get_slo_redirect_url(entity_md_xml),
slo_post_url: get_slo_post_url(entity_md_xml),
nameid_format: get_nameid_format(entity_md_xml)
}}
%IdpData{
idp_data
| entity_id: entityID,
signed_requests: get_req_signed(md_xml),
certs: signing_certs,
fingerprints: idp_cert_fingerprints(signing_certs),
sso_redirect_url: get_sso_redirect_url(entity_md_xml),
sso_post_url: get_sso_post_url(entity_md_xml),
slo_redirect_url: get_slo_redirect_url(entity_md_xml),
slo_post_url: get_slo_post_url(entity_md_xml),
nameid_format: get_nameid_format(entity_md_xml)
}}
end
end

Expand Down Expand Up @@ -388,7 +398,7 @@ defmodule Samly.IdpData do
)
end

@spec get_entity_id(:xmlElement) :: binary()
@spec get_entity_id(:xmlElement) :: binary()
def get_entity_id(md_elem) do
md_elem |> xpath(@entity_id_selector |> add_ns()) |> hd() |> String.trim()
end
Expand All @@ -404,8 +414,8 @@ defmodule Samly.IdpData do
@spec get_req_signed(:xmlElement) :: binary()
def get_req_signed(md_elem), do: get_data(md_elem, @req_signed_selector)

#@spec get_signing_certs(:xmlElement) :: certs()
#def get_signing_certs(md_elem), do: get_certs(md_elem, signing_keys_selector())
# @spec get_signing_certs(:xmlElement) :: certs()
# def get_signing_certs(md_elem), do: get_certs(md_elem, signing_keys_selector())

def get_signing_certs_in_idp(md_elem), do: get_certs(md_elem, @signing_keys_in_idp_selector)

Expand Down Expand Up @@ -459,11 +469,11 @@ defmodule Samly.IdpData do
@spec get_entity_descriptor(:xmlElement, entityID :: binary()) :: :xmlElement | nil
defp get_entity_descriptor(md_xml, entityID) do
selector = entity_by_id_selector(entityID) |> add_ns()

try do
SweetXml.xpath(md_xml, selector)
rescue
_ -> {:error, :entity_not_found}
end
end

end
end
14 changes: 11 additions & 3 deletions lib/samly/router_util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,23 @@ defmodule Samly.RouterUtil do
end
end

def send_saml_request(conn, idp_url, use_redirect?, signed_xml_payload, relay_state) do
def send_saml_request(conn, idp_url, use_redirect?, xml_payload, relay_state, opts \\ []) do
if use_redirect? do
redirect_opts = [key: opts |> Keyword.get(:sp) |> Esaml.esaml_sp(:key)]

url =
:esaml_binding.encode_http_redirect(idp_url, signed_xml_payload, :undefined, relay_state)
:esaml_binding.encode_http_redirect(
idp_url,
xml_payload,
:undefined,
relay_state,
redirect_opts
)

conn |> redirect(302, url)
else
nonce = conn.private[:samly_nonce]
resp_body = :esaml_binding.encode_http_post(idp_url, signed_xml_payload, relay_state, nonce)
resp_body = :esaml_binding.encode_http_post(idp_url, xml_payload, relay_state, nonce)

conn
|> Conn.put_resp_header("content-type", "text/html")
Expand Down
Loading

0 comments on commit 83c26bc

Please sign in to comment.