Skip to content
Open
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
6 changes: 6 additions & 0 deletions lib/samly/application_config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
defmodule Samly.ApplicationConfig do
@behaviour Samly.ConfigBehaviour

@impl true
def get_idp(_conn, idp_id), do: Samly.Helper.get_idp(idp_id)
end
2 changes: 1 addition & 1 deletion lib/samly/auth_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Samly.AuthRouter do
import Samly.RouterUtil, only: [check_idp_id: 2, check_target_url: 2]

plug :fetch_session
plug Plug.CSRFProtection
plug Plug.CSRFProtection, with: :clear_session
plug :match
plug :check_idp_id
plug :check_target_url
Expand Down
3 changes: 3 additions & 0 deletions lib/samly/config_behaviour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Samly.ConfigBehaviour do
@callback get_idp(conn :: Plug.Conn.t(), idp_id :: binary) :: nil | Samly.IdpData.t()
end
1 change: 0 additions & 1 deletion lib/samly/helper.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defmodule Samly.Helper do
@moduledoc false

require Samly.Esaml
alias Samly.{Assertion, Esaml, IdpData}

Expand Down
7 changes: 7 additions & 0 deletions lib/samly/idp_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ defmodule Samly.IdpData do

@type id :: binary()

@spec load_providers(map(), map()) :: %IdpData{}
def from_config(sp_config, idp_config) do
service_providers = Samly.SpData.load_providers([sp_config])
config_map = load_providers([idp_config], service_providers)
config_map[idp_config.id]
end

@spec load_providers([map], %{required(id()) => %SpData{}}) ::
%{required(id()) => %IdpData{}} | no_return()
def load_providers(prov_config, service_providers) do
Expand Down
4 changes: 3 additions & 1 deletion lib/samly/router_util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ defmodule Samly.RouterUtil do
end
end

idp = idp_id && Helper.get_idp(idp_id)
config = Application.get_env(:samly, :config_provider, Samly.ApplicationConfig)

idp = idp_id && config.get_idp(conn, idp_id)

if idp do
conn |> Conn.put_private(:samly_idp, idp)
Expand Down
8 changes: 7 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Samly.Mixfile do
app: :samly,
version: @version,
description: @description,
elixirc_paths: elixirc_paths(Mix.env()),
docs: docs(),
package: package(),
elixir: "~> 1.10",
Expand All @@ -25,14 +26,19 @@ defmodule Samly.Mixfile do
]
end

defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

# Run "mix help deps" to learn about dependencies.
defp deps() do
[
{:plug, "~> 1.6"},
{:esaml, "~> 4.3"},
{:sweet_xml, "~> 0.6"},
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
{:ex_doc, "~> 0.19", only: :dev, runtime: false}
{:ex_doc, "~> 0.19", only: :dev, runtime: false},
{:mix_test_watch, "~> 1.1", only: [:dev, :test], runtime: false},
{:floki, "~> 0.36.2", only: [:dev, :test], runtime: false}
]
end

Expand Down
3 changes: 3 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"esaml": {:hex, :esaml, "4.6.0", "8fb5a3a0d56ccfce3e081a2f72b29058511047a2abbafb64cb6f595bf7465124", [:rebar3], [{:cowboy, "< 3.0.0", [hex: :cowboy, repo: "hexpm", optional: false]}], "hexpm", "d34d0b259cd8ac8215fd2c333fac9dbbb91b5f5da5a9304508612ff3ac0afa7a"},
"ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
Expand Down
36 changes: 36 additions & 0 deletions test/auth_router_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule Samly.AuthRouterTest do
use Samly.RouterCase

setup do
setup_providers([@sp_config], [@idp_config])
end

test "GET on signin uri returns saml html form" do
conn(:get, "/signin/idp1")
|> init_test_session(%{})
|> AuthRouter.call([])
|> assert_initial_saml_form("%2F")
end

test "GET on signin uri returns saml html form with the given target url" do
conn(:get, "/signin/idp1", target_url: "/Home")
|> init_test_session(%{})
|> AuthRouter.call([])
|> assert_initial_saml_form("%2FHome")
end

test "POST on signin uri returns form that will be submited to idp" do
assert ~c"urn:test:sp1" =
conn(:post, "/signin/idp1", %{RelayState: "OOhdIq-_PagPusisHCjYBZsYSwr-bVUs"})
|> put_private(:plug_skip_csrf_protection, true)
|> put_private(:samly_nonce, "1mv+7BUs8o1nkOxa6ufS6kJ")
|> init_test_session(%{})
|> AuthRouter.call([])
|> assert_form("POST", "http://samly.idp:8082/simplesaml/saml2/idp/SSOService.php")
|> Floki.attribute("input[name=SAMLRequest]", "value")
|> List.first()
|> Base.decode64!()
|> SweetXml.parse()
|> SweetXml.xpath(~x"//saml:Issuer/text()")
end
end
42 changes: 42 additions & 0 deletions test/config_behaviour_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule Samly.ConfigBehaviourTest do
use Samly.RouterCase
alias Samly.SPRouter

def get_idp(_conn, idp_id) do
assert idp_id == @idp_config.id
Samly.IdpData.from_config(@sp_config, @idp_config)
end

setup do
Application.put_env(:samly, :config_provider, Samly.ConfigBehaviourTest)
setup_providers([], [])
end

test "GET on signin uri returns saml html form" do
conn(:get, "/signin/idp1")
|> init_test_session(%{})
|> AuthRouter.call([])
|> assert_initial_saml_form("%2F")
end

test "POST consume saml assertion" do
assertion =
File.read!("./test/data/simplesaml_idp_assertion.xml")
|> Base.encode64()

conn =
conn(:post, "/consume/idp1", %{
SAMLResponse: assertion,
RelayState: "OOhdIq-_PagPusisHCjYBZsYSwr-bVUs"
})
|> init_test_session(%{
"relay_state" => "OOhdIq-_PagPusisHCjYBZsYSwr-bVUs",
"idp_id" => "idp1",
"target_url" => "/Home"
})
|> SPRouter.call([])

assert conn.status == 302
assert "/Home" = get_resp_header(conn, "location") |> List.first()
end
end
86 changes: 86 additions & 0 deletions test/data/simplesaml_idp_assertion.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_b1f03f2438091a34a291e6e4906c6e6c09202b099c" Version="2.0" IssueInstant="2100-09-26T16:39:00Z"
Destination="http://samly.howto:4003/sso/sp/consume/idp1"
InResponseTo="id1727359569545809642114626">
<saml:Issuer>http://samly.idp:8082/simplesaml/saml2/idp/metadata.php</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<ds:Reference URI="#_b1f03f2438091a34a291e6e4906c6e6c09202b099c">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<ds:DigestValue>SsE1VSW1l4/Uff6lJCjEQDYYMI6t2AdnNq1CBLK1HXo=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
wLAVBA8yZpz1eMlR2oqiwAlAnLnjLuQ+kq1Rpn4cQkaTu34EePmWsroDOvEZBkM9vqTPyafCDeRS3SrF+13lv8FdnliO3AgwLGO9Kt2s0q8Oh/t1KJyZagkjmjh/kzEmL4hpgVnz4urGNp8gQMxI9R6gWh8sqGuqT3O9tH2/YG4ctMPghOEw1AKLv6Eq0VMUpNDGP6dUNdsM81UizD+zYc7S7wxbEP4nb1prev5T98fpouG82r5A/OwXBHrRx2QqTAocipIewo6U9OIbD3ctBC1kMvN4hPMnPEF8PVnxA8Fh9El0+E1qXJA0OQ5R9Tz7EfXFJlaQxmCvFeOYeWcPjQ==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIICmjCCAYICCQDX5sKPsYV3+jANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTE5MTIyMzA5MDI1MVoXDTIwMDEyMjA5MDI1MVowDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMdtDJ278DQTp84O5Nq5F8s5YOR34GFOGI2Swb/3pU7X7918lVljiKv7WVM65S59nJSyXV+fa15qoXLfsdRnq3yw0hTSTs2YDX+jl98kK3ksk3rROfYh1LIgByj4/4NeNpExgeB6rQk5Ay7YS+ARmMzEjXa0favHxu5BOdB2y6WvRQyjPS2lirT/PKWBZc04QZepsZ56+W7bd557tdedcYdY/nKI1qmSQClG2qgslzgqFOv1KCOw43a3mcK/TiiD8IXyLMJNC6OFW3xTL/BG6SOZ3dQ9rjQOBga+6GIaQsDjC4Xp7Kx+FkSvgaw0sJV8gt1mlZy+27Sza6d+hHD2pWECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAm2fk1+gd08FQxK7TL04O8EK1f0bzaGGUxWzlh98a3Dm8+OPhVQRi/KLsFHliLC86lsZQKunYdDB+qd0KUk2oqDG6tstG/htmRYD/S/jNmt8gyPAVi11dHUqW3IvQgJLwxZtoAv6PNs188hvT1WK3VWJ4YgFKYi5XQYnR5sv69Vsr91lYAxyrIlMKahjSW1jTD3ByRfAQghsSLk6fV0OyJHyhuF1TxOVBVf8XOdaqfmvD90JGIPGtfMLPUX4m35qaGAU48PwCL7L3cRHYs9wZWc0ifXZcBENLtHYCLi5txR8c5lyHB9d3AQHzKHMFNjLswn5HsckKg83RH7+eVqHqGw==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</samlp:Status>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_5568778a74c53067c93551e955f0f7820f6d5fe6ad"
Version="2.0" IssueInstant="2100-09-26T16:39:00Z">
<saml:Issuer>http://samly.idp:8082/simplesaml/saml2/idp/metadata.php</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<ds:Reference URI="#_5568778a74c53067c93551e955f0f7820f6d5fe6ad">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<ds:DigestValue>r1+smsBJ0NEKEazRJH8f6abeGOOhGXTOiW510t2mBmA=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
kbVcFaZ44aDvSt8GHC/RjI1XCGCFWPotO1MWvCUK+0AV2GhU5BTMqk5JNA3fVolHzSsnUh32Eup0CXxUgJL3zs3tvBN9o9zQmvXePQNprdvOd9Ileijyg/o66dTP9neBN3IzfAtRRzuIj3Ay2Ba7JlIfrLa5RQIr/uZ/QiXNtnl4ultaFWue58VjFcuF9Qei4NUbPxnB+zqpHhKsTsL/XYX/cEZ/1HqKNQ0a4YOjPBUJYfIjhMabaN6wN7v0nMrzQHxJtjhseXx+AmbALLCtWjPAoj9yhqGlRC6q2dmSSgJ2eEpDcSPaElmagu0ypwbSRGl8RgwFUsmgIT6fX8gruw==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIICmjCCAYICCQDX5sKPsYV3+jANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0MB4XDTE5MTIyMzA5MDI1MVoXDTIwMDEyMjA5MDI1MVowDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMdtDJ278DQTp84O5Nq5F8s5YOR34GFOGI2Swb/3pU7X7918lVljiKv7WVM65S59nJSyXV+fa15qoXLfsdRnq3yw0hTSTs2YDX+jl98kK3ksk3rROfYh1LIgByj4/4NeNpExgeB6rQk5Ay7YS+ARmMzEjXa0favHxu5BOdB2y6WvRQyjPS2lirT/PKWBZc04QZepsZ56+W7bd557tdedcYdY/nKI1qmSQClG2qgslzgqFOv1KCOw43a3mcK/TiiD8IXyLMJNC6OFW3xTL/BG6SOZ3dQ9rjQOBga+6GIaQsDjC4Xp7Kx+FkSvgaw0sJV8gt1mlZy+27Sza6d+hHD2pWECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAm2fk1+gd08FQxK7TL04O8EK1f0bzaGGUxWzlh98a3Dm8+OPhVQRi/KLsFHliLC86lsZQKunYdDB+qd0KUk2oqDG6tstG/htmRYD/S/jNmt8gyPAVi11dHUqW3IvQgJLwxZtoAv6PNs188hvT1WK3VWJ4YgFKYi5XQYnR5sv69Vsr91lYAxyrIlMKahjSW1jTD3ByRfAQghsSLk6fV0OyJHyhuF1TxOVBVf8XOdaqfmvD90JGIPGtfMLPUX4m35qaGAU48PwCL7L3cRHYs9wZWc0ifXZcBENLtHYCLi5txR8c5lyHB9d3AQHzKHMFNjLswn5HsckKg83RH7+eVqHqGw==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml:Subject>
<saml:NameID SPNameQualifier="urn:test:sp1"
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">
_7fa378b1b922a7e6793eb432b8ec9dfc0374b4e8a3</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2100-09-26T16:39:00Z"
Recipient="http://samly.howto:4003/sso/sp/consume/idp1"
InResponseTo="id1727359569545809642114626" />
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2100-09-26T14:05:40Z" NotOnOrAfter="2100-09-26T16:39:00Z">
<saml:AudienceRestriction>
<saml:Audience>urn:test:sp1</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2100-09-26T08:23:58Z"
SessionNotOnOrAfter="2100-09-26T16:39:00Z"
SessionIndex="_991ec1d9a2533365ac44ba4d610fbebbd614f4c550">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">[email protected]</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
Loading