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

Logout binding #3

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
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