├── .formatter.exs
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config
└── config.exs
├── lib
├── samly.ex
└── samly
│ ├── assertion.ex
│ ├── auth_handler.ex
│ ├── auth_router.ex
│ ├── config_error.ex
│ ├── cspr_router.ex
│ ├── esaml.ex
│ ├── helper.ex
│ ├── idp_data.ex
│ ├── provider.ex
│ ├── router.ex
│ ├── router_util.ex
│ ├── sp_data.ex
│ ├── sp_handler.ex
│ ├── sp_router.ex
│ ├── state.ex
│ ├── state
│ ├── ets.ex
│ ├── session.ex
│ └── store.ex
│ └── subject.ex
├── mix.exs
├── mix.lock
└── test
├── data
├── azure_fed_metadata.xml
├── idp_metadata.xml
├── onelogin_idp_metadata.xml
├── shibboleth_idp_metadata.xml
├── simplesaml_idp_metadata.xml
├── test.crt
├── test.pem
└── testshib_metadata.xml
├── samly_idp_data_test.exs
├── samly_sp_data_test.exs
├── samly_state_test.exs
└── test_helper.exs
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | inputs: [
3 | "mix.exs",
4 | "{config,lib,test}/**/*.{ex,exs}"
5 | ],
6 |
7 | locals_without_parens: [
8 | plug: 1,
9 | plug: 2
10 | ]
11 | ]
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 | .sobelow
22 | /docs/
23 | /.elixir_ls/
24 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ### v1.0.0
4 |
5 | + `target_url` query parameter for the sign-in/sign-out requests must be
6 | `x-www-form-urlencoded`.
7 |
8 | + Redirect URLs are properly encoded.
9 |
10 | + Switched to `report-to` in content security policy.
11 |
12 | + `cache-control` header value updated.
13 |
14 | + Issue: #33 - Content Security Policy
15 | Enabled `Content-Security-Policy` in the HTTP response.
16 |
17 | + PR: #41 - Config support for nameid format
18 | `Samly` uses the nameid format from the IdP metadata XML file.
19 | It is possible now to override this using `nameid_fomat` config setting.
20 | If this format information is not present in the IdP metadata XML and not
21 | specified in the config setting, it defaults to `:transient`.
22 | Thanks to [calvinb](https://github.com/calvinb) for the PR.
23 |
24 | + Uptake `esaml 4.2` bringing in support for encrypted assertions.
25 | Check [Assertion Encryption](https://github.com/handnot2/esaml#assertion-encryption)
26 | for supported encryption algorithms. Use this information to enable assertion
27 | encryption on IdP. Thanks to [tcrossland](https://github.com/tcrossland)
28 | for the `esaml` PR.
29 |
30 | ### v0.10.1
31 |
32 | + Issues: #39, #40 - Downcase response header names
33 | (PR from [calvinb](https://github.com/calvinb))
34 |
35 | ### v0.10.0
36 |
37 | + Issue: #31 - Support for Cowboy 2.x
38 | Uptake `esaml` v4.0.0 which includes support for Cowboy 2.x.
39 | If support for Cowboy 1.x is needed, you need an override with
40 | `esaml` v3.6.x in your application `mix.exs` file.
41 |
42 | + Issue: #32 - Support for custom State Storage
43 | Includes support for ETS and Plug Sessions based authenticated SAML
44 | assertion storage. It is possible to create custom stores by
45 | implementing `Samly.State.Store`.
46 |
47 | + Issue: #34 - Included filename in error messages
48 | Include metadata/cert/key filenames when there is an error relevant to
49 | those files.
50 |
51 | ### v0.9.3
52 |
53 | + Uptake `esaml` v3.6.0 that includes fixes for schema validation errors.
54 |
55 | ### v0.9.2
56 |
57 | + PR merged fixing reopened Issue #16 (from @peterox)
58 |
59 | ### v0.9.1
60 |
61 | + Remove the need for supplying certicate and key files if the requests are
62 | not signed (Issue #16). Useful during development when the corresponding
63 | Identity Provider is setup for unsigned requests/responses. Use signing
64 | for production deployments. The defaults expect signed requests/responses.
65 |
66 | ### v0.9.0
67 |
68 | + Issue: #12. Support for IDP initiated SSO flow.
69 |
70 | + Original auth request ID when returned in auth response is made available
71 | in the assertion subject (SP initiated SSO flows). For IDP initiated
72 | SSO flows, this will be an empty string.
73 |
74 | + Issue: #14. Remove built-in referer check.
75 | Not specific to `Samly`. It is better handled by the consuming application.
76 |
77 | ### v0.8.4
78 |
79 | + Shibboleth Single Logout session match related fix. Uptake `esaml v3.3.0`.
80 |
81 | ### v0.8.3
82 |
83 | + Generates SP metadata XML that passes XSD validation
84 |
85 | ### v0.8.2
86 |
87 | + Handle namespaces in Identity Provider Metadata XML file
88 |
89 | ### v0.8.0
90 |
91 | + Added support for multiple Identity Providers. Check issue: #4.
92 | Instructions for migrating from v0.7.x available in github project wiki.
93 |
94 | ### v0.7.2
95 |
96 | + Added `use_redirect_for_idp_req` config parameter. By default `Samly` uses HTTP POST when sending requests to IdP. Set this config parameter to `true` if HTTP redirection should be used instead.
97 |
98 | ### v0.7.1
99 |
100 | + Added config option (`entity_id`). OOTB uses metadata URI as entity ID. Can be specified (`urn` entity ID for example) to override the default.
101 |
102 | ### v0.7.0
103 |
104 | + Added config options to control if requests and/or responses are signed or not
105 |
106 | ### v0.6.3
107 |
108 | + Added Inch CI
109 | + Corresponding doc updates
110 |
111 | ### v0.6.2
112 |
113 | + Doc updates
114 | + Config handling changes and corresponding tests
115 |
116 | ### v0.6.1
117 |
118 | + `target_url` query parameter form url encoded
119 |
120 | ### v0.6.0
121 |
122 | + Plug Pipeline config `:pre_session_create_pipeline`
123 | + Computed attributes available in `Samly.Assertion`
124 | + Updates to `Samly.Provider` `base_url` config handling
125 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 handnot2 (handnot2@gmail.com)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Samly
2 |
3 | A SAML 2.0 Service Provider Single-Sign-On Authentication library. This Plug library can be used to SAML enable a Plug/Phoenix application.
4 |
5 | This has been used in the wild with the following Identity Providers:
6 |
7 | + Okta
8 | + Ping Identity
9 | + OneLogin
10 | + ADFS
11 | + Nexus GO
12 | + Shibboleth
13 | + SimpleSAMLphp
14 |
15 | Please send a note by DM if you have successfully used `Samly` with other Identity Providers.
16 |
17 | [](http://inch-ci.org/github/handnot2/samly)
18 |
19 | This library uses Erlang [`esaml`](https://github.com/handnot2/esaml) to provide
20 | plug enabled routes.
21 |
22 | ## Setup
23 |
24 | ```elixir
25 | # mix.exs
26 |
27 | # v1.0.0 uses esaml v4.2 which in turn relies on cowboy 2.x
28 | # If you need to work with cowboy 1.x, you need the following override:
29 | # {:esaml, "~> 3.7", override: true}
30 |
31 | defp deps() do
32 | [
33 | # ...
34 | {:samly, "~> 1.0.0"},
35 | ]
36 | end
37 | ```
38 |
39 | ## Supervision Tree
40 |
41 | Add `Samly.Provider` to your application supervision tree.
42 |
43 | ```elixir
44 | # application.ex
45 |
46 | children = [
47 | # ...
48 | {Samly.Provider, []},
49 | ]
50 | ```
51 |
52 | ## Router Change
53 |
54 | Make the following change in your application router.
55 |
56 | ```elixir
57 | # router.ex
58 |
59 | # Add the following scope ahead of other routes
60 | # Keep this as a top-level scope and **do not** add
61 | # any plugs or pipelines explicitly to this scope.
62 | scope "/sso" do
63 | forward "/", Samly.Router
64 | end
65 | ```
66 |
67 | ## Certificate and Key for Samly
68 |
69 | `Samly` needs a private key and a corresponding certificate. These are used to
70 | sign the SAML requests when communicating with the Identity Provider. This certificate
71 | should be made available to `Samly` via config settings. It should also be made
72 | available to the Identity Provider so it can verify the SAML signed requests.
73 |
74 | You can create a self-signed certificate for this purpose. You can use `phx.gen.cert`
75 | mix task that is available as part of Phoenix 1.4 or use `openssl` directly to generate
76 | the key and corresponding certificate.
77 | (Check out [`samly_howto`](https://github.com/handnot2/samly_howto) `README.md` for this.)
78 |
79 | ## Identity Provider Metadata
80 |
81 | `Samly` expects information about the Identity Provider including information about
82 | its SAML endpoints in an XML file. Most Identity Providers have some way of
83 | exporting the IdP metadata in XML form. Some may provide a web UI to export/save
84 | the XML locally. Others may provide a URL that can be used to fetch the metadata.
85 |
86 | For example, `SimpleSAMLPhp` IdP provides a URL for the metadata. You can fetch
87 | it using `wget`.
88 |
89 | ```
90 | wget --no-check-certificate -O idp1_metadata.xml https://idp1.samly:9091/simplesaml/saml2/idp/metadata.php
91 | ```
92 |
93 | If you are using the `SimpleSAMLPhp` administrative Web UI, login with you
94 | admin credentials (`https://idp1.samly:9091/simplesaml`). Go to the `Federation`
95 | tab. At the top there will be a section titled "SAML 2.0 IdP Metadata". Click
96 | on the `Show metadata` link. Copy the metadata XML from this page and save it
97 | in a local file (`idp1_metadata.xml` for example).
98 |
99 | Make sure to save this XML file and provide the path to the saved file in
100 | `Samly` configuration.
101 |
102 | ## Identity Provider ID in Samly
103 |
104 | `Samly` has the ability to support multiple Identity Providers. All IdPs that
105 | `Samly` needs to talk to must have an identifier (idp_id). This IdP id will be
106 | used in the service provider URLs. This is how `Samly` figures out which SAML
107 | request corresponds to what IdP so that it can perform relevant validation checks
108 | and process the requests/responses.
109 |
110 | There are two options when it comes to how the idp_id is represented in the
111 | Service Provider SAML URLs.
112 |
113 | #### URL Path Segment
114 |
115 | In this model, the idp_id is present as a URL path segment. Here is an
116 | example URL: `https://do-good.org/sso/auth/signin/affiliates`. The idp_id
117 | in this URL is "affiliates". If you have more than one IdP, only this last
118 | part changes. The URLs for this model are:
119 |
120 | | Description | URL |
121 | |:----|:----|
122 | | Sign-in button/link in Web UI | `/sso/auth/signin/affiliates` |
123 | | Sign-out button/link in Web UI | `/sso/auth/signout/affiliates` |
124 | | SP Metadata URL | `https://do-good.org/sso/sp/metadata/affiliates` |
125 | | SAML Assertion Consumer Service | `https://do-good.org/sso/sp/consume/affiliates` |
126 | | SAML SingleLogout Service | `https://do-good.org/sso/sp/logout/affiliates` |
127 |
128 | The path segment model is the default one in `Samly`. If there is only one Identity Provider, use this mode.
129 |
130 | > These URL routes are automatically created based on the configuration information and
131 | > the above mentioned router scope definition.
132 | >
133 | > Use the Sign-in and Sign-out URLs shown above in your application's Web UI buttons/links.
134 | > When the end-user clicks on these buttons/links, the HTTP `GET` request is handled by `Samly`
135 | > which internally does a `POST` that in turn sends the appropriate SAML request to the IdP.
136 |
137 | #### Subdomain in Host Name
138 |
139 | In this model, the subdomain name is used as the idp_id. Here is an example URL: `https://ngo.do-good.org/sso/auth/signin`. Here `ngo` is the idp_id. The URLs supported by `Samly`
140 | in this model look different.
141 |
142 | | Description | URL |
143 | |:----|:----|
144 | | Sign-in button/link in Web UI | `/sso/auth/signin` |
145 | | Sign-out button/link in Web UI | `/sso/auth/signout` |
146 | | SP Metadata URL | `https://ngo.do-good.org/sso/sp/metadata` |
147 | | SAML Assertion Consumer Service | `https://ngo.do-good.org/sso/sp/consume` |
148 | | SAML SingleLogout Service | `https://ngo.do-good.org/sso/sp/logout` |
149 |
150 | > Take a look at [`samly_howto`](https://github.com/handnot2/samly_howto) - a reference/demo
151 | > application on how to use this library.
152 | >
153 | > Make sure to use HTTPS URLs in production deployments.
154 |
155 | #### Target URL for Sign-In and Sign-Out Actions
156 |
157 | The sign-in and sign-out URLs (HTTP GET) mentioned above optionally take a `target_url`
158 | query parameter. `Samly` will redirect the browser to these URLs upon successfuly
159 | completing the sign-in/sign-out operations initiated from your application.
160 |
161 | > This `target_url` query parameter value must be `x-www-form-urlencoded`.
162 |
163 | ## Samly Configuration
164 |
165 | ```elixir
166 | # config/dev.exs
167 |
168 | config :samly, Samly.Provider,
169 | idp_id_from: :path_segment,
170 | service_providers: [
171 | %{
172 | id: "do-good-affiliates-sp",
173 | entity_id: "urn:do-good.org:affiliates-app",
174 | certfile: "path/to/samly/certfile.pem",
175 | keyfile: "path/to/samly/keyfile.pem",
176 | #contact_name: "Affiliates Admin",
177 | #contact_email: "affiliates-admin@do-good.org",
178 | #org_name: "Do Good",
179 | #org_displayname: "Goodly, No evil!",
180 | #org_url: "https://do-good.org"
181 | }
182 | ],
183 | identity_providers: [
184 | %{
185 | id: "affiliates",
186 | sp_id: "do-good-affiliates-sp",
187 | base_url: "https://do-good.org/sso",
188 | metadata_file: "idp1_metadata.xml",
189 | #pre_session_create_pipeline: MySamlyPipeline,
190 | #use_redirect_for_req: false,
191 | #sign_requests: true,
192 | #sign_metadata: true,
193 | #signed_assertion_in_resp: true,
194 | #signed_envelopes_in_resp: true,
195 | #allow_idp_initiated_flow: false,
196 | #allowed_target_urls: ["https://do-good.org"],
197 | #nameid_format: :transient
198 | }
199 | ]
200 | ```
201 |
202 | | Parameters | Description |
203 | |:------------|:-----------|
204 | | `idp_id_from` | _(optional)_`:path_segment` or `:subdomain`. Default is `:path_segment`. |
205 | | **Service Provider Parameters** | |
206 | | `id` | _(mandatory)_ |
207 | | `identity_id` | _(optional)_ If omitted, the metadata URL will be used |
208 | | `certfile` | _(optional)_ This is needed when SAML requests/responses from `Samly` need to be signed. Make sure to **set this in a production deployment**. Could be omitted during development if your IDP is setup to not require signing. If that is the case, the following **Identity Provider Parameters** must be explicitly set to false: `sign_requests`, `sign_metadata`|
209 | | `keyfile` | _(optional)_ Similar to `certfile` |
210 | | `contact_name` | _(optional)_ Technical contact name for the Service Provider |
211 | | `contact_email` | _(optional)_ Technical contact email address |
212 | | `org_name` | _(optional)_ SAML Service Provider (your app) Organization name |
213 | | `org_displayname` | _(optional)_ SAML SP Organization displayname |
214 | | `org_url` | _(optional)_ Service Provider Organization web site URL |
215 | | **Identity Provider Parameters** | |
216 | | `id` | _(mandatory)_ This will be the idp_id in the URLs |
217 | | `sp_id` | _(mandatory)_ The service provider definition to be used with this Identity Provider definition |
218 | | `base_url` | _(optional)_ If missing `Samly` will use the current URL to derive this. It is better to define this in production deployment. |
219 | | `metadata_file` | _(mandatory)_ Path to the IdP metadata XML file obtained from the Identity Provider. |
220 | | `pre_session_create_pipeline` | _(optional)_ Check the customization section. |
221 | | `use_redirect_for_req` | _(optional)_ Default is `false`. When this is `false`, `Samly` will POST to the IdP SAML endpoints. |
222 | | `sign_requests`, `sign_metadata` | _(optional)_ Default is `true`. |
223 | | `signed_assertion_in_resp`, `signed_envelopes_in_resp` | _(optional)_ Default is `true`. When `true`, `Samly` expects the requests and responses from IdP to be signed. |
224 | | `allow_idp_initiated_flow` | _(optional)_ Default is `false`. IDP initiated SSO is allowed only when this is set to `true`. |
225 | | `allowed_target_urls` | _(optional)_ Default is `[]`. `Samly` uses this **only** when `allow_idp_initiated_flow` parameter is set to `true`. Make sure to set this to one or more exact URLs you want to allow (whitelist). The URL to redirect the user after completing the SSO flow is sent from IDP in auth response as `relay_state`. This `relay_state` target URL is matched against this URL list. Set the value to `nil` if you do not want this whitelist capability. |
226 | | `nameid_format` | _(optional)_ When specified, `Samly` includes the value as the `NameIDPolicy` element's `Format` attribute in the login request. Value must either be a string or one of the following atoms: `:email`, `:x509`, `:windows`, `:krb`, `:persistent`, `:transient`. Use the string value when you need to specify a non-standard/custom nameid format supported by your IdP. |
227 |
228 | #### Authenticated SAML Assertion State Store
229 |
230 | `Samly` internally maintains the authenticated SAML assertions (from `LoginResponse` SAML requests).
231 | There are two built-in state store options available - one based on ETS and the other on Plug Sessions.
232 | The ETS store can be setup using the following configuration:
233 |
234 | ```elixir
235 | config :samly, Samly.State,
236 | store: Samly.State.ETS,
237 | opts: [table: :my_ets_table]
238 | ```
239 |
240 | This state configuration is optional. If omitted, `Samly` uses `Samly.State.ETS` provider by default.
241 |
242 | | Options | Description |
243 | |:------------|:-----------|
244 | | `opts` | _(optional)_ The `:table` option is the ETS table name for storing the assertions. This ETS table is created during the store provider initialization if it is not already present. Default is `samly_assertions_table`. |
245 |
246 | > Use `Samly.State.Session` provider in a clustered deployment. This provider uses
247 | > the Plug Sessions to keep the authenticated SAML assertions.
248 |
249 | This session based provider can be enabled using the following:
250 |
251 | ```elixir
252 | config :samly, Samly.State,
253 | store: Samly.State.Session,
254 | opts: [key: :my_assertion_key]
255 | ```
256 |
257 | | Options | Description |
258 | |:------------|:-----------|
259 | | `opts` | _(optional)_ The `:key` is the name of the session key where assertion is stored. Default is `:samly_assertion`. |
260 |
261 | ## SAML Assertion
262 |
263 | Once authentication is completed successfully, IdP sends a "consume" SAML
264 | request to `Samly`. `Samly` in-turn performs its own checks (including checking
265 | the integrity of the "consume" request). At this point, the SAML assertion
266 | with the authenticated user subject and attributes is available.
267 |
268 | The subject in the SAML assertion is tracked by `Samly` so that subsequent
269 | logout/signout request, either service provider initiated or IdP initiated
270 | would result in proper removal of the corresponding SAML assertion.
271 |
272 | Use the `Samly.get_active_assertion` function to get the SAML assertion
273 | for the currently authenticated user. This function will return `nil` if
274 | the user is not authenticated.
275 |
276 | > Avoid using the subject in the SAML assertion in UI. Depending on how the
277 | > IdP is setup, this might be a randomly generated id.
278 | >
279 | > You should only rely on the user attributes in the assertion.
280 | > As an application working with an IdP, you should know which attributes
281 | > will be made available to your application and out of
282 | > those attributes which one should be treated as the logged in userid/name.
283 | > For example it could be "uid" or "email" depending on how the authentication
284 | > source is setup in the IdP.
285 |
286 | ## Customization
287 |
288 | #### Pipeline
289 |
290 | `Samly` allows you to specify a Plug Pipeline if you need more control over
291 | the authenticated user's attributes and/or do a Just-in-time user creation.
292 | The Plug Pipeline is invoked after the user has successfully authenticated
293 | with the IdP but before a session is created.
294 |
295 | This is just a vanilla Plug Pipeline. The SAML assertion from
296 | the IdP is made available in the Plug connection as a "private".
297 | (The pipeline plugs have access to the `idp_id` in this assertion.)
298 | If you want to derive new attributes, create an Elixir map data (`%{}`)
299 | and update the `computed` field of the SAML assertion and put it back
300 | in the Plug connection private with `Conn.put_private` call.
301 |
302 | Here is a sample pipeline that shows this:
303 |
304 | ```elixir
305 | defmodule MySamlyPipeline do
306 | use Plug.Builder
307 | alias Samly.{Assertion}
308 |
309 | plug :compute_attributes
310 | plug :jit_provision_user
311 |
312 | def compute_attributes(conn, _opts) do
313 | assertion = conn.private[:samly_assertion]
314 |
315 | # This assertion has the idp_id
316 | # %Assertion{idp_id: idp_id} = assertion
317 |
318 | first_name = Map.get(assertion.attributes, "first_name")
319 | last_name = Map.get(assertion.attributes, "last_name")
320 |
321 | computed = %{"full_name" => "#{first_name} #{last_name}"}
322 |
323 | assertion = %Assertion{assertion | computed: computed}
324 |
325 | conn
326 | |> put_private(:samly_assertion, assertion)
327 |
328 | # If you have an error condition:
329 | # conn
330 | # |> send_resp(404, "attribute mapping failed")
331 | # |> halt()
332 | end
333 |
334 | def jit_provision_user(conn, _opts) do
335 | # your user creation here ...
336 | conn
337 | end
338 | end
339 | ```
340 |
341 | Make this pipeline available in your config:
342 |
343 | ```elixir
344 | config :samly, Samly.Provider,
345 | identity_providers: [
346 | %{
347 | # ...
348 | pre_session_create_pipeline: MySamlyPipeline,
349 | # ...
350 | }
351 | ]
352 | ```
353 |
354 | #### State Store
355 |
356 | Take a look at the implementation of `Samly.State.ETS` or `Samly.State.Session` and use those as examples showing how to create your own state store (based on redis, memcached, database etc.).
357 |
358 | ## Security Related
359 |
360 | + `Samly` initiated sign-in/sign-out requests send `RelayState` to IdP and expect to get that back. Mismatched or missing `RelayState` in IdP responses to SP initiated requests will fail (with HTTP `403 access_denied`).
361 | + Besides the `RelayState`, the request and response `idp_id`s must match. Reponse is rejected if they don't.
362 | + `Samly` makes the original request ID that an auth response corresponds to
363 | in `Samly.Subject.in_response_to` field. It is the responsibility of the consuming application to use this information along with the validity period in the assertion to check for **replay attacks**. The consuming application should use the `pre_session_create_pipeline` to perform this check. You may need a database or a distributed cache such as memcache in a clustered setup to keep track of these request IDs for their validity period to perform this check. Be aware that `in_response_to` field is **not** set when IDP initialized authorization flow is used.
364 | + OOTB SAML requests and responses are signed.
365 | + Signature digest method supported: `SHA256`.
366 | > Some Identity Providers may be using `SHA1` by default.
367 | > Make sure to configure the IdP to use `SHA256`. `Samly`
368 | > will reject (`access_denied`) IdP responses using `SHA1`.
369 | + `esaml` provides additional checks such as trusted certificate verification, recipient verification among others.
370 | + By default, `Samly` signs the SAML requests it sends to the Identity Provider. It also
371 | expects the SAML reqsponses to be signed (both assertion and envelopes). If your IdP is
372 | not configured to sign, you will have to explicitly turn them off in the configuration.
373 | It is highly recommended to turn signing on in production deployments.
374 | + Encypted Assertions are supported in `Samly`. There are no explicit config settings for this. Decryption happens automatically when encrypted assertions are detected in the SAML response.
375 | > [Supported Encryption algorithms](https://github.com/handnot2/esaml#assertion-encryption)
376 | + Make sure to use HTTPS URLs in production deployments.
377 |
378 | ## FAQ
379 |
380 | #### How to setup a SAML 2.0 IdP for development purposes?
381 |
382 | Docker based setup of [`SimpleSAMLPhp`](https://simplesamlphp.org) is made available
383 | at [`samly_simplesaml`](https://github.com/handnot2/samly_simplesaml) Git Repo.
384 | Check out the `README.md` file of this repo.
385 |
386 | There is also a Docker based setup of [`Shibboleth`](https://www.shibboleth.net/).
387 | Checkout the corresponding `README.md` file in [`samly_shibboleth`](https://github.com/handnot2/samly_shibboleth) Git Repo.
388 |
389 | #### Any sample Phoenix application that shows how to use Samly?
390 |
391 | Clone the [`samly_howto`](https://github.com/handnot2/samly_howto) Git Repo.
392 | Detailed instructions on how to setup and run this application are available
393 | in the `README.md` file in this repo.
394 |
395 | > It is recommended that you use the `SamlyHowto` application to
396 | > sort out any configuration issues by making that demo application work
397 | > successfully with your Identity Provider (IdP) before attempting your
398 | > application.
399 | >
400 | > This demo application supports experimentation with multiple IdPs.
401 |
402 | #### How to register the service provider with IdP
403 |
404 | If you are using `samly_simplesaml` or `samly_shibboleth`, the instructions
405 | you followed there would take care of registering your Phoenix SAML Service provider
406 | appliccation. For any other IdP, follow the instructions from the respective
407 | IdP vendor.
408 |
409 | #### Common Errors
410 |
411 | `access_denied {:error, :bad_recipient}` - Check the `base_url` in your `Samly`
412 | config setting under `indentity_providers`.
413 |
414 | `access_denied {:error, :bad_audience}` - Make sure that the `entity_id` in
415 | the `Samly` config setting is correct.
416 |
417 | `access_denied {:envelope, {:error, :cert_no_accepted}}` - Make sure the
418 | Identity Provider metadata XML file you are using in the `Samly` config setting
419 | is correct and corresponds to the IdP you are attempting to talk to. You get
420 | this error if the certificate used by the IdP to sign the SAML responses
421 | has changed and you don't have the updated IdP metadata XML file on the `Samly` end.
422 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for
9 | # 3rd-party users, it should be done in your "mix.exs" file.
10 |
11 | # You can configure your application as:
12 | #
13 | # config :samly, key: :value
14 | #
15 | # and access this configuration in your application as:
16 | #
17 | # Application.get_env(:samly, :key)
18 | #
19 | # You can also configure a 3rd-party app:
20 | #
21 | # config :logger, level: :info
22 | #
23 |
24 | # It is also possible to import configuration files, relative to this
25 | # directory. For example, you can emulate configuration per environment
26 | # by uncommenting the line below and defining dev.exs, test.exs and such.
27 | # Configuration from the imported file will override the ones defined
28 | # here (which is why it is important to import them last).
29 | #
30 | # import_config "#{Mix.env}.exs"
31 |
--------------------------------------------------------------------------------
/lib/samly.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly do
2 | @moduledoc """
3 | Elixir library used to enable SAML SP SSO to a Phoenix/Plug based application.
4 | """
5 |
6 | alias Plug.Conn
7 | alias Samly.{Assertion, State}
8 |
9 | @doc """
10 | Returns authenticated user SAML Assertion.
11 |
12 | The struct includes the attributes sent from IdP as well as any corresponding locally
13 | computed/derived attributes. Returns `nil` if the current Plug session
14 | is not authenticated.
15 |
16 | ## Parameters
17 |
18 | + `conn` - Plug connection
19 |
20 | ## Examples
21 |
22 | # When there is an authenticated SAML assertion
23 | %Assertion{} = Samly.get_active_assertion()
24 | """
25 | @spec get_active_assertion(Conn.t()) :: nil | Assertion.t()
26 | def get_active_assertion(conn) do
27 | case Conn.get_session(conn, "samly_assertion_key") do
28 | {_idp_id, _nameid} = assertion_key ->
29 | State.get_assertion(conn, assertion_key)
30 |
31 | _ ->
32 | nil
33 | end
34 | end
35 |
36 | @doc """
37 | Returns value of the specified attribute name in the given SAML Assertion.
38 |
39 | Checks for the attribute in `computed` map first and `attributes` map next.
40 | Returns a UTF-8 binary or a list of UTF-8 binaries (in case of multi-valued)
41 | if the given attribute is present. Returns `nil` if attribute is not present.
42 |
43 | ## Parameters
44 |
45 | + `assertion` - SAML assertion obtained by calling `get_active_assertion/1`
46 | + `name`: Attribute name
47 |
48 | ## Examples
49 |
50 | assertion = Samly.get_active_assertion()
51 | # returns a list if the attribute is multi-valued
52 | roles = Samly.get_attribute(assertion, "roles")
53 | computed_fullname = Samly.get_attribute(assertion, "fullname")
54 | """
55 | @spec get_attribute(nil | Assertion.t(), Assertion.attr_name_t()) ::
56 | nil | Assertion.attr_value_t()
57 | def get_attribute(nil, _name), do: nil
58 |
59 | def get_attribute(%Assertion{} = assertion, name) do
60 | Map.get(assertion.computed, name) || Map.get(assertion.attributes, name)
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/samly/assertion.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.Assertion do
2 | @moduledoc """
3 | SAML assertion returned from IDP upon successful user authentication.
4 |
5 | The assertion attributes returned by the IdP are available in `attributes` field
6 | as a map. Any computed attributes (using a Plug Pipeline by way of configuration)
7 | are available in `computed` field as map.
8 |
9 | The attributes can be accessed directly from `attributes` or `computed` maps.
10 | The `Samly.get_attribute/2` function can be used as well. This function will
11 | first look at the `computed` attributes. If the request attribute is not present there,
12 | it will check in `attributes` next.
13 | """
14 |
15 | require Samly.Esaml
16 | alias Samly.{Esaml, Subject}
17 |
18 | @type attr_name_t :: String.t()
19 | @type attr_value_t :: String.t() | [String.t()]
20 |
21 | defstruct version: "2.0",
22 | issue_instant: "",
23 | recipient: "",
24 | issuer: "",
25 | subject: %Subject{},
26 | conditions: %{},
27 | attributes: %{},
28 | authn: %{},
29 | computed: %{},
30 | idp_id: ""
31 |
32 | @type t :: %__MODULE__{
33 | version: String.t(),
34 | issue_instant: String.t(),
35 | recipient: String.t(),
36 | issuer: String.t(),
37 | subject: Subject.t(),
38 | conditions: map,
39 | attributes: %{required(attr_name_t()) => attr_value_t()},
40 | authn: map,
41 | computed: %{required(attr_name_t()) => attr_value_t()},
42 | idp_id: String.t()
43 | }
44 |
45 | @doc false
46 | def from_rec(assertion_rec) do
47 | Esaml.esaml_assertion(
48 | version: version,
49 | issue_instant: issue_instant,
50 | recipient: recipient,
51 | issuer: issuer,
52 | subject: subject_rec,
53 | conditions: conditions,
54 | attributes: attributes,
55 | authn: authn
56 | ) = assertion_rec
57 |
58 | %__MODULE__{
59 | version: List.to_string(version),
60 | issue_instant: List.to_string(issue_instant),
61 | recipient: List.to_string(recipient),
62 | issuer: List.to_string(issuer),
63 | subject: Subject.from_rec(subject_rec),
64 | conditions: conditions |> stringize(),
65 | attributes: attributes |> stringize(),
66 | authn: authn |> stringize()
67 | }
68 | end
69 |
70 | defp stringize(proplist) do
71 | proplist
72 | |> Enum.map(fn
73 | {k, []} ->
74 | {to_string(k), ""}
75 |
76 | {k, values} when is_list(values) and is_list(hd(values)) ->
77 | {to_string(k), Enum.map(values, fn v -> List.to_string(v) end)}
78 |
79 | {k, v} when is_list(v) ->
80 | {to_string(k), List.to_string(v)}
81 | end)
82 | |> Enum.into(%{})
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/samly/auth_handler.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.AuthHandler do
2 | @moduledoc false
3 |
4 | require Logger
5 | import Plug.Conn
6 | alias Samly.{Assertion, IdpData, Helper, State, Subject}
7 |
8 | import Samly.RouterUtil, only: [ensure_sp_uris_set: 2, send_saml_request: 5, redirect: 3]
9 |
10 | @sso_init_resp_template """
11 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 | Note:
25 | Since your browser does not support JavaScript, you must press
26 | the button below to proceed.
27 |
28 |
29 |
36 |
37 |
38 | """
39 |
40 | def initiate_sso_req(conn) do
41 | import Plug.CSRFProtection, only: [get_csrf_token: 0]
42 |
43 | target_url = conn.private[:samly_target_url] || "/"
44 |
45 | opts = [
46 | nonce: conn.private[:samly_nonce],
47 | action: URI.encode(conn.request_path),
48 | target_url: URI.encode_www_form(target_url),
49 | csrf_token: get_csrf_token()
50 | ]
51 |
52 | conn
53 | |> put_resp_header("content-type", "text/html")
54 | |> send_resp(200, EEx.eval_string(@sso_init_resp_template, opts))
55 | end
56 |
57 | def send_signin_req(conn) do
58 | %IdpData{id: idp_id} = idp = conn.private[:samly_idp]
59 | %IdpData{esaml_idp_rec: idp_rec, esaml_sp_rec: sp_rec} = idp
60 | sp = ensure_sp_uris_set(sp_rec, conn)
61 |
62 | target_url = conn.private[:samly_target_url] || "/"
63 | assertion_key = get_session(conn, "samly_assertion_key")
64 |
65 | case State.get_assertion(conn, assertion_key) do
66 | %Assertion{idp_id: ^idp_id} ->
67 | conn |> redirect(302, target_url)
68 |
69 | _ ->
70 | relay_state = State.gen_id()
71 |
72 | {idp_signin_url, req_xml_frag} =
73 | Helper.gen_idp_signin_req(sp, idp_rec, Map.get(idp, :nameid_format))
74 |
75 | conn
76 | |> configure_session(renew: true)
77 | |> put_session("relay_state", relay_state)
78 | |> put_session("idp_id", idp_id)
79 | |> put_session("target_url", target_url)
80 | |> send_saml_request(
81 | idp_signin_url,
82 | idp.use_redirect_for_req,
83 | req_xml_frag,
84 | relay_state
85 | )
86 | end
87 |
88 | # rescue
89 | # error ->
90 | # Logger.error("#{inspect error}")
91 | # conn |> send_resp(500, "request_failed")
92 | end
93 |
94 | def send_signout_req(conn) do
95 | %IdpData{id: idp_id} = idp = conn.private[:samly_idp]
96 | %IdpData{esaml_idp_rec: idp_rec, esaml_sp_rec: sp_rec} = idp
97 | sp = ensure_sp_uris_set(sp_rec, conn)
98 |
99 | target_url = conn.private[:samly_target_url] || "/"
100 | assertion_key = get_session(conn, "samly_assertion_key")
101 |
102 | case State.get_assertion(conn, assertion_key) do
103 | %Assertion{idp_id: ^idp_id, authn: authn, subject: subject} ->
104 | session_index = Map.get(authn, "session_index", "")
105 | subject_rec = Subject.to_rec(subject)
106 |
107 | {idp_signout_url, req_xml_frag} =
108 | Helper.gen_idp_signout_req(sp, idp_rec, subject_rec, session_index)
109 |
110 | conn = State.delete_assertion(conn, assertion_key)
111 | relay_state = State.gen_id()
112 |
113 | conn
114 | |> put_session("target_url", target_url)
115 | |> put_session("relay_state", relay_state)
116 | |> put_session("idp_id", idp_id)
117 | |> delete_session("samly_assertion_key")
118 | |> send_saml_request(
119 | idp_signout_url,
120 | idp.use_redirect_for_req,
121 | req_xml_frag,
122 | relay_state
123 | )
124 |
125 | _ ->
126 | conn |> send_resp(403, "access_denied")
127 | end
128 |
129 | # rescue
130 | # error ->
131 | # Logger.error("#{inspect error}")
132 | # conn |> send_resp(500, "request_failed")
133 | end
134 | end
135 |
--------------------------------------------------------------------------------
/lib/samly/auth_router.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.AuthRouter do
2 | @moduledoc false
3 |
4 | use Plug.Router
5 | import Plug.Conn
6 | import Samly.RouterUtil, only: [check_idp_id: 2, check_target_url: 2]
7 |
8 | plug :fetch_session
9 | plug Plug.CSRFProtection
10 | plug :match
11 | plug :check_idp_id
12 | plug :check_target_url
13 | plug :dispatch
14 |
15 | get "/signin/*idp_id_seg" do
16 | conn |> Samly.AuthHandler.initiate_sso_req()
17 | end
18 |
19 | post "/signin/*idp_id_seg" do
20 | conn |> Samly.AuthHandler.send_signin_req()
21 | end
22 |
23 | get "/signout/*idp_id_seg" do
24 | conn |> Samly.AuthHandler.initiate_sso_req()
25 | end
26 |
27 | post "/signout/*idp_id_seg" do
28 | conn |> Samly.AuthHandler.send_signout_req()
29 | end
30 |
31 | match _ do
32 | conn |> send_resp(404, "not_found")
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/samly/config_error.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.ConfigError do
2 | @moduledoc false
3 |
4 | defexception [:message]
5 |
6 | @spec exception(map) :: Exception.t()
7 | def exception(data) when is_map(data) do
8 | %__MODULE__{message: "invalid_config: #{inspect(data)}"}
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/samly/cspr_router.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.CsprRouter do
2 | @moduledoc false
3 |
4 | use Plug.Router
5 | import Plug.Conn
6 |
7 | require Logger
8 |
9 | plug :match
10 | plug :dispatch
11 |
12 | match _ do
13 | {:ok, body, conn} = Plug.Conn.read_body(conn)
14 | Logger.error(body)
15 | conn |> send_resp(200, "OK")
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/samly/esaml.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.Esaml do
2 | @moduledoc false
3 |
4 | require Record
5 | import Record, only: [defrecord: 2, extract: 2]
6 |
7 | @esaml_hrl "esaml/include/esaml.hrl"
8 | @public_key_hrl "public_key/include/OTP-PUB-KEY.hrl"
9 |
10 | defrecord :esaml_org, extract(:esaml_org, from_lib: @esaml_hrl)
11 | defrecord :esaml_contact, extract(:esaml_contact, from_lib: @esaml_hrl)
12 | defrecord :esaml_sp_metadata, extract(:esaml_sp_metadata, from_lib: @esaml_hrl)
13 | defrecord :esaml_idp_metadata, extract(:esaml_idp_metadata, from_lib: @esaml_hrl)
14 | defrecord :esaml_authnreq, extract(:esaml_authnreq, from_lib: @esaml_hrl)
15 | defrecord :esaml_subject, extract(:esaml_subject, from_lib: @esaml_hrl)
16 | defrecord :esaml_assertion, extract(:esaml_assertion, from_lib: @esaml_hrl)
17 | defrecord :esaml_logoutreq, extract(:esaml_logoutreq, from_lib: @esaml_hrl)
18 | defrecord :esaml_logoutresp, extract(:esaml_logoutresp, from_lib: @esaml_hrl)
19 | defrecord :esaml_response, extract(:esaml_response, from_lib: @esaml_hrl)
20 | defrecord :esaml_sp, extract(:esaml_sp, from_lib: @esaml_hrl)
21 | defrecord :RSAPrivateKey, extract(:RSAPrivateKey, from_lib: @public_key_hrl)
22 | end
23 |
--------------------------------------------------------------------------------
/lib/samly/helper.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.Helper do
2 | @moduledoc false
3 |
4 | require Samly.Esaml
5 | alias Samly.{Assertion, Esaml, IdpData}
6 |
7 | @spec get_idp(binary) :: nil | IdpData.t()
8 | def get_idp(idp_id) do
9 | idps = Application.get_env(:samly, :identity_providers, %{})
10 | Map.get(idps, idp_id)
11 | end
12 |
13 | @spec get_metadata_uri(nil | binary, binary) :: nil | charlist
14 | def get_metadata_uri(nil, _idp_id), do: nil
15 |
16 | def get_metadata_uri(sp_base_url, nil) when is_binary(sp_base_url) do
17 | "#{sp_base_url}/sp/metadata" |> String.to_charlist()
18 | end
19 |
20 | def get_metadata_uri(sp_base_url, idp_id) when is_binary(sp_base_url) do
21 | "#{sp_base_url}/sp/metadata/#{idp_id}" |> String.to_charlist()
22 | end
23 |
24 | @spec get_consume_uri(nil | binary, binary) :: nil | charlist
25 | def get_consume_uri(nil, _idp_id), do: nil
26 |
27 | def get_consume_uri(sp_base_url, nil) when is_binary(sp_base_url) do
28 | "#{sp_base_url}/sp/consume" |> String.to_charlist()
29 | end
30 |
31 | def get_consume_uri(sp_base_url, idp_id) when is_binary(sp_base_url) do
32 | "#{sp_base_url}/sp/consume/#{idp_id}" |> String.to_charlist()
33 | end
34 |
35 | @spec get_logout_uri(nil | binary, binary) :: nil | charlist
36 | def get_logout_uri(nil, _idp_id), do: nil
37 |
38 | def get_logout_uri(sp_base_url, nil) when is_binary(sp_base_url) do
39 | "#{sp_base_url}/sp/logout" |> String.to_charlist()
40 | end
41 |
42 | def get_logout_uri(sp_base_url, idp_id) when is_binary(sp_base_url) do
43 | "#{sp_base_url}/sp/logout/#{idp_id}" |> String.to_charlist()
44 | end
45 |
46 | def sp_metadata(sp) do
47 | :xmerl.export([:esaml_sp.generate_metadata(sp)], :xmerl_xml)
48 | end
49 |
50 | def gen_idp_signin_req(sp, idp_metadata, nameid_format) do
51 | idp_signin_url = Esaml.esaml_idp_metadata(idp_metadata, :login_location)
52 |
53 | xml_frag = :esaml_sp.generate_authn_request(idp_signin_url, sp, nameid_format)
54 |
55 | {idp_signin_url, xml_frag}
56 | end
57 |
58 | def gen_idp_signout_req(sp, idp_metadata, subject_rec, session_index) do
59 | idp_signout_url = Esaml.esaml_idp_metadata(idp_metadata, :logout_location)
60 | xml_frag = :esaml_sp.generate_logout_request(idp_signout_url, session_index, subject_rec, sp)
61 | {idp_signout_url, xml_frag}
62 | end
63 |
64 | def gen_idp_signout_resp(sp, idp_metadata, signout_status) do
65 | idp_signout_url = Esaml.esaml_idp_metadata(idp_metadata, :logout_location)
66 | xml_frag = :esaml_sp.generate_logout_response(idp_signout_url, signout_status, sp)
67 | {idp_signout_url, xml_frag}
68 | end
69 |
70 | def decode_idp_auth_resp(sp, saml_encoding, saml_response) do
71 | with {:ok, xml_frag} <- decode_saml_payload(saml_encoding, saml_response),
72 | {:ok, assertion_rec} <- :esaml_sp.validate_assertion(xml_frag, sp) do
73 | {:ok, Assertion.from_rec(assertion_rec)}
74 | else
75 | {:error, reason} -> {:error, reason}
76 | error -> {:error, {:invalid_request, "#{inspect(error)}"}}
77 | end
78 | end
79 |
80 | def decode_idp_signout_resp(sp, saml_encoding, saml_response) do
81 | resp_ns = [
82 | {'samlp', 'urn:oasis:names:tc:SAML:2.0:protocol'},
83 | {'saml', 'urn:oasis:names:tc:SAML:2.0:assertion'},
84 | {'ds', 'http://www.w3.org/2000/09/xmldsig#'}
85 | ]
86 |
87 | with {:ok, xml_frag} <- decode_saml_payload(saml_encoding, saml_response),
88 | nodes when is_list(nodes) and length(nodes) == 1 <-
89 | :xmerl_xpath.string('/samlp:LogoutResponse', xml_frag, [{:namespace, resp_ns}]) do
90 | :esaml_sp.validate_logout_response(xml_frag, sp)
91 | else
92 | _ -> {:error, :invalid_request}
93 | end
94 | end
95 |
96 | def decode_idp_signout_req(sp, saml_encoding, saml_request) do
97 | req_ns = [
98 | {'samlp', 'urn:oasis:names:tc:SAML:2.0:protocol'},
99 | {'saml', 'urn:oasis:names:tc:SAML:2.0:assertion'}
100 | ]
101 |
102 | with {:ok, xml_frag} <- decode_saml_payload(saml_encoding, saml_request),
103 | nodes when is_list(nodes) and length(nodes) == 1 <-
104 | :xmerl_xpath.string('/samlp:LogoutRequest', xml_frag, [{:namespace, req_ns}]) do
105 | :esaml_sp.validate_logout_request(xml_frag, sp)
106 | else
107 | _ -> {:error, :invalid_request}
108 | end
109 | end
110 |
111 | defp decode_saml_payload(saml_encoding, saml_payload) do
112 | try do
113 | xml = :esaml_binding.decode_response(saml_encoding, saml_payload)
114 | {:ok, xml}
115 | rescue
116 | error -> {:error, {:invalid_response, "#{inspect(error)}"}}
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/lib/samly/idp_data.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.IdpData do
2 | @moduledoc false
3 |
4 | import SweetXml
5 | require Logger
6 | require Samly.Esaml
7 | alias Samly.{Esaml, Helper, IdpData, SpData}
8 |
9 | @type nameid_format :: :unknown | charlist()
10 | @type certs :: [binary()]
11 | @type url :: nil | binary()
12 |
13 | defstruct id: "",
14 | sp_id: "",
15 | base_url: nil,
16 | metadata_file: nil,
17 | pre_session_create_pipeline: nil,
18 | use_redirect_for_req: false,
19 | sign_requests: true,
20 | sign_metadata: true,
21 | signed_assertion_in_resp: true,
22 | signed_envelopes_in_resp: true,
23 | allow_idp_initiated_flow: false,
24 | allowed_target_urls: [],
25 | entity_id: "",
26 | signed_requests: "",
27 | certs: [],
28 | sso_redirect_url: nil,
29 | sso_post_url: nil,
30 | slo_redirect_url: nil,
31 | slo_post_url: nil,
32 | nameid_format: :unknown,
33 | fingerprints: [],
34 | esaml_idp_rec: Esaml.esaml_idp_metadata(),
35 | esaml_sp_rec: Esaml.esaml_sp(),
36 | valid?: false
37 |
38 | @type t :: %__MODULE__{
39 | id: binary(),
40 | sp_id: binary(),
41 | base_url: nil | binary(),
42 | metadata_file: nil | binary(),
43 | pre_session_create_pipeline: nil | module(),
44 | use_redirect_for_req: boolean(),
45 | sign_requests: boolean(),
46 | sign_metadata: boolean(),
47 | signed_assertion_in_resp: boolean(),
48 | signed_envelopes_in_resp: boolean(),
49 | allow_idp_initiated_flow: boolean(),
50 | allowed_target_urls: nil | [binary()],
51 | entity_id: binary(),
52 | signed_requests: binary(),
53 | certs: certs(),
54 | sso_redirect_url: url(),
55 | sso_post_url: url(),
56 | slo_redirect_url: url(),
57 | slo_post_url: url(),
58 | nameid_format: nameid_format(),
59 | fingerprints: [binary()],
60 | esaml_idp_rec: :esaml_idp_metadata,
61 | esaml_sp_rec: :esaml_sp,
62 | valid?: boolean()
63 | }
64 |
65 | @entdesc "md:EntityDescriptor"
66 | @idpdesc "md:IDPSSODescriptor"
67 | @signedreq "WantAuthnRequestsSigned"
68 | @nameid "md:NameIDFormat"
69 | @keydesc "md:KeyDescriptor"
70 | @ssos "md:SingleSignOnService"
71 | @slos "md:SingleLogoutService"
72 | @redirect "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
73 | @post "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
74 |
75 | @entity_id_selector ~x"//#{@entdesc}/@entityID"sl
76 | @nameid_format_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@nameid}/text()"s
77 | @req_signed_selector ~x"//#{@entdesc}/#{@idpdesc}/@#{@signedreq}"s
78 | @sso_redirect_url_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@redirect}']/@Location"s
79 | @sso_post_url_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@ssos}[@Binding = '#{@post}']/@Location"s
80 | @slo_redirect_url_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@redirect}']/@Location"s
81 | @slo_post_url_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@slos}[@Binding = '#{@post}']/@Location"s
82 | @signing_keys_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@keydesc}[@use != 'encryption']"l
83 | @enc_keys_selector ~x"//#{@entdesc}/#{@idpdesc}/#{@keydesc}[@use = 'encryption']"l
84 | @cert_selector ~x"./ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()"s
85 |
86 | @type id :: binary()
87 |
88 | @spec load_providers([map], %{required(id()) => %SpData{}}) ::
89 | %{required(id()) => %IdpData{}} | no_return()
90 | def load_providers(prov_config, service_providers) do
91 | prov_config
92 | |> Enum.map(fn idp_config -> load_provider(idp_config, service_providers) end)
93 | |> Enum.filter(fn idp_data -> idp_data.valid? end)
94 | |> Enum.map(fn idp_data -> {idp_data.id, idp_data} end)
95 | |> Enum.into(%{})
96 | end
97 |
98 | @spec load_provider(map(), %{required(id()) => %SpData{}}) :: %IdpData{} | no_return
99 | def load_provider(idp_config, service_providers) do
100 | %IdpData{}
101 | |> save_idp_config(idp_config)
102 | |> load_metadata(idp_config)
103 | |> override_nameid_format(idp_config)
104 | |> update_esaml_recs(service_providers, idp_config)
105 | |> verify_slo_url()
106 | end
107 |
108 | @spec save_idp_config(%IdpData{}, map()) :: %IdpData{}
109 | defp save_idp_config(idp_data, %{id: id, sp_id: sp_id} = opts_map)
110 | when is_binary(id) and is_binary(sp_id) do
111 | %IdpData{idp_data | id: id, sp_id: sp_id, base_url: Map.get(opts_map, :base_url)}
112 | |> set_metadata_file(opts_map)
113 | |> set_pipeline(opts_map)
114 | |> set_allowed_target_urls(opts_map)
115 | |> set_boolean_attr(opts_map, :use_redirect_for_req)
116 | |> set_boolean_attr(opts_map, :sign_requests)
117 | |> set_boolean_attr(opts_map, :sign_metadata)
118 | |> set_boolean_attr(opts_map, :signed_assertion_in_resp)
119 | |> set_boolean_attr(opts_map, :signed_envelopes_in_resp)
120 | |> set_boolean_attr(opts_map, :allow_idp_initiated_flow)
121 | end
122 |
123 | @spec load_metadata(%IdpData{}, map()) :: %IdpData{}
124 | defp load_metadata(idp_data, _opts_map) do
125 | with {:reading, {:ok, raw_xml}} <- {:reading, File.read(idp_data.metadata_file)},
126 | {:parsing, {:ok, idp_data}} <- {:parsing, from_xml(raw_xml, idp_data)} do
127 | idp_data
128 | else
129 | {:reading, {:error, reason}} ->
130 | Logger.error(
131 | "[Samly] Failed to read metadata_file [#{inspect(idp_data.metadata_file)}]: #{
132 | inspect(reason)
133 | }"
134 | )
135 |
136 | idp_data
137 |
138 | {:parsing, {:error, reason}} ->
139 | Logger.error(
140 | "[Samly] Invalid metadata_file content [#{inspect(idp_data.metadata_file)}]: #{
141 | inspect(reason)
142 | }"
143 | )
144 |
145 | idp_data
146 | end
147 | end
148 |
149 | @spec update_esaml_recs(%IdpData{}, %{required(id()) => %SpData{}}, map()) :: %IdpData{}
150 | defp update_esaml_recs(idp_data, service_providers, opts_map) do
151 | case Map.get(service_providers, idp_data.sp_id) do
152 | %SpData{} = sp ->
153 | idp_data = %IdpData{idp_data | esaml_idp_rec: to_esaml_idp_metadata(idp_data, opts_map)}
154 | idp_data = %IdpData{idp_data | esaml_sp_rec: get_esaml_sp(sp, idp_data)}
155 | %IdpData{idp_data | valid?: cert_config_ok?(idp_data, sp)}
156 |
157 | _ ->
158 | Logger.error("[Samly] Unknown/invalid sp_id: #{idp_data.sp_id}")
159 | idp_data
160 | end
161 | end
162 |
163 | @spec cert_config_ok?(%IdpData{}, %SpData{}) :: boolean
164 | defp cert_config_ok?(%IdpData{} = idp_data, %SpData{} = sp_data) do
165 | if (idp_data.sign_metadata || idp_data.sign_requests) &&
166 | (sp_data.cert == :undefined || sp_data.key == :undefined) do
167 | Logger.error("[Samly] SP cert or key missing - Skipping identity provider: #{idp_data.id}")
168 | false
169 | else
170 | true
171 | end
172 | end
173 |
174 | @spec verify_slo_url(%IdpData{}) :: %IdpData{}
175 | defp verify_slo_url(%IdpData{} = idp_data) do
176 | if idp_data.valid? && idp_data.slo_redirect_url == nil && idp_data.slo_post_url == nil do
177 | Logger.warn("[Samly] SLO Endpoint missing in [#{inspect(idp_data.metadata_file)}]")
178 | end
179 |
180 | idp_data
181 | end
182 |
183 | @default_metadata_file "idp_metadata.xml"
184 |
185 | @spec set_metadata_file(%IdpData{}, map()) :: %IdpData{}
186 | defp set_metadata_file(%IdpData{} = idp_data, %{} = opts_map) do
187 | %IdpData{idp_data | metadata_file: Map.get(opts_map, :metadata_file, @default_metadata_file)}
188 | end
189 |
190 | @spec set_pipeline(%IdpData{}, map()) :: %IdpData{}
191 | defp set_pipeline(%IdpData{} = idp_data, %{} = opts_map) do
192 | pipeline = Map.get(opts_map, :pre_session_create_pipeline)
193 | %IdpData{idp_data | pre_session_create_pipeline: pipeline}
194 | end
195 |
196 | defp set_allowed_target_urls(%IdpData{} = idp_data, %{} = opts_map) do
197 | target_urls =
198 | case Map.get(opts_map, :allowed_target_urls, nil) do
199 | nil -> nil
200 | urls when is_list(urls) -> Enum.filter(urls, &is_binary/1)
201 | end
202 |
203 | %IdpData{idp_data | allowed_target_urls: target_urls}
204 | end
205 |
206 | @spec override_nameid_format(%IdpData{}, map()) :: %IdpData{}
207 | defp override_nameid_format(%IdpData{} = idp_data, idp_config) do
208 | nameid_format =
209 | case Map.get(idp_config, :nameid_format, "") do
210 | "" ->
211 | idp_data.nameid_format
212 |
213 | format when is_binary(format) ->
214 | to_charlist(format)
215 |
216 | :email ->
217 | 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
218 |
219 | :x509 ->
220 | 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName'
221 |
222 | :windows ->
223 | 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName'
224 |
225 | :krb ->
226 | 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos'
227 |
228 | :persistent ->
229 | 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
230 |
231 | :transient ->
232 | 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
233 |
234 | invalid_nameid_format ->
235 | Logger.error(
236 | "[Samly] invalid nameid_format [#{inspect(idp_data.metadata_file)}]: #{
237 | inspect(invalid_nameid_format)
238 | }"
239 | )
240 |
241 | idp_data.nameid_format
242 | end
243 |
244 | %IdpData{idp_data | nameid_format: nameid_format}
245 | end
246 |
247 | @spec set_boolean_attr(%IdpData{}, map(), atom()) :: %IdpData{}
248 | defp set_boolean_attr(%IdpData{} = idp_data, %{} = opts_map, attr_name)
249 | when is_atom(attr_name) do
250 | v = Map.get(opts_map, attr_name)
251 | if is_boolean(v), do: Map.put(idp_data, attr_name, v), else: idp_data
252 | end
253 |
254 | @spec from_xml(binary, %IdpData{}) :: {:ok, %IdpData{}}
255 | def from_xml(metadata_xml, idp_data) when is_binary(metadata_xml) do
256 | xml_opts = [
257 | space: :normalize,
258 | namespace_conformant: true,
259 | comments: false,
260 | default_attrs: true
261 | ]
262 |
263 | md_xml = SweetXml.parse(metadata_xml, xml_opts)
264 | signing_certs = get_signing_certs(md_xml)
265 |
266 | {:ok,
267 | %IdpData{
268 | idp_data
269 | | entity_id: get_entity_id(md_xml),
270 | signed_requests: get_req_signed(md_xml),
271 | certs: signing_certs,
272 | fingerprints: idp_cert_fingerprints(signing_certs),
273 | sso_redirect_url: get_sso_redirect_url(md_xml),
274 | sso_post_url: get_sso_post_url(md_xml),
275 | slo_redirect_url: get_slo_redirect_url(md_xml),
276 | slo_post_url: get_slo_post_url(md_xml),
277 | nameid_format: get_nameid_format(md_xml)
278 | }}
279 | end
280 |
281 | # @spec to_esaml_idp_metadata(IdpData.t(), map()) :: :esaml_idp_metadata
282 | defp to_esaml_idp_metadata(%IdpData{} = idp_data, %{} = idp_config) do
283 | {sso_url, slo_url} = get_sso_slo_urls(idp_data, idp_config)
284 | sso_url = if sso_url, do: String.to_charlist(sso_url), else: []
285 | slo_url = if slo_url, do: String.to_charlist(slo_url), else: :undefined
286 |
287 | Esaml.esaml_idp_metadata(
288 | entity_id: String.to_charlist(idp_data.entity_id),
289 | login_location: sso_url,
290 | logout_location: slo_url,
291 | name_format: idp_data.nameid_format
292 | )
293 | end
294 |
295 | defp get_sso_slo_urls(%IdpData{} = idp_data, %{use_redirect_for_req: true}) do
296 | {idp_data.sso_redirect_url, idp_data.slo_redirect_url}
297 | end
298 |
299 | defp get_sso_slo_urls(%IdpData{} = idp_data, %{use_redirect_for_req: false}) do
300 | {idp_data.sso_post_url, idp_data.slo_post_url}
301 | end
302 |
303 | defp get_sso_slo_urls(%IdpData{} = idp_data, _opts_map) do
304 | {
305 | idp_data.sso_post_url || idp_data.sso_redirect_url,
306 | idp_data.slo_post_url || idp_data.slo_redirect_url
307 | }
308 | end
309 |
310 | @spec idp_cert_fingerprints(certs()) :: [binary()]
311 | defp idp_cert_fingerprints(certs) when is_list(certs) do
312 | certs
313 | |> Enum.map(&Base.decode64!/1)
314 | |> Enum.map(&cert_fingerprint/1)
315 | |> Enum.map(&String.to_charlist/1)
316 | |> :esaml_util.convert_fingerprints()
317 | end
318 |
319 | defp cert_fingerprint(dercert) do
320 | "sha256:" <> (:sha256 |> :crypto.hash(dercert) |> Base.encode64())
321 | end
322 |
323 | # @spec get_esaml_sp(%SpData{}, %IdpData{}) :: :esaml_sp
324 | defp get_esaml_sp(%SpData{} = sp_data, %IdpData{} = idp_data) do
325 | idp_id_from = Application.get_env(:samly, :idp_id_from)
326 | path_segment_idp_id = if idp_id_from == :subdomain, do: nil, else: idp_data.id
327 |
328 | sp_entity_id =
329 | case sp_data.entity_id do
330 | "" -> :undefined
331 | id -> String.to_charlist(id)
332 | end
333 |
334 | Esaml.esaml_sp(
335 | org:
336 | Esaml.esaml_org(
337 | name: String.to_charlist(sp_data.org_name),
338 | displayname: String.to_charlist(sp_data.org_displayname),
339 | url: String.to_charlist(sp_data.org_url)
340 | ),
341 | tech:
342 | Esaml.esaml_contact(
343 | name: String.to_charlist(sp_data.contact_name),
344 | email: String.to_charlist(sp_data.contact_email)
345 | ),
346 | key: sp_data.key,
347 | certificate: sp_data.cert,
348 | sp_sign_requests: idp_data.sign_requests,
349 | sp_sign_metadata: idp_data.sign_metadata,
350 | idp_signs_envelopes: idp_data.signed_envelopes_in_resp,
351 | idp_signs_assertions: idp_data.signed_assertion_in_resp,
352 | trusted_fingerprints: idp_data.fingerprints,
353 | metadata_uri: Helper.get_metadata_uri(idp_data.base_url, path_segment_idp_id),
354 | consume_uri: Helper.get_consume_uri(idp_data.base_url, path_segment_idp_id),
355 | logout_uri: Helper.get_logout_uri(idp_data.base_url, path_segment_idp_id),
356 | entity_id: sp_entity_id
357 | )
358 | end
359 |
360 | @spec get_entity_id(:xmlElement) :: binary()
361 | def get_entity_id(md_elem) do
362 | md_elem |> xpath(@entity_id_selector |> add_ns()) |> hd() |> String.trim()
363 | end
364 |
365 | @spec get_nameid_format(:xmlElement) :: nameid_format()
366 | def get_nameid_format(md_elem) do
367 | case get_data(md_elem, @nameid_format_selector) do
368 | "" -> :unknown
369 | nameid_format -> to_charlist(nameid_format)
370 | end
371 | end
372 |
373 | @spec get_req_signed(:xmlElement) :: binary()
374 | def get_req_signed(md_elem), do: get_data(md_elem, @req_signed_selector)
375 |
376 | @spec get_signing_certs(:xmlElement) :: certs()
377 | def get_signing_certs(md_elem), do: get_certs(md_elem, @signing_keys_selector)
378 |
379 | @spec get_enc_certs(:xmlElement) :: certs()
380 | def get_enc_certs(md_elem), do: get_certs(md_elem, @enc_keys_selector)
381 |
382 | @spec get_certs(:xmlElement, %SweetXpath{}) :: certs()
383 | defp get_certs(md_elem, key_selector) do
384 | md_elem
385 | |> xpath(key_selector |> add_ns())
386 | |> Enum.map(fn e ->
387 | # Extract base64 encoded cert from XML (strip away any whitespace)
388 | cert = xpath(e, @cert_selector |> add_ns())
389 |
390 | cert
391 | |> String.split()
392 | |> Enum.map(&String.trim/1)
393 | |> Enum.join()
394 | end)
395 | end
396 |
397 | @spec get_sso_redirect_url(:xmlElement) :: url()
398 | def get_sso_redirect_url(md_elem), do: get_url(md_elem, @sso_redirect_url_selector)
399 |
400 | @spec get_sso_post_url(:xmlElement) :: url()
401 | def get_sso_post_url(md_elem), do: get_url(md_elem, @sso_post_url_selector)
402 |
403 | @spec get_slo_redirect_url(:xmlElement) :: url()
404 | def get_slo_redirect_url(md_elem), do: get_url(md_elem, @slo_redirect_url_selector)
405 |
406 | @spec get_slo_post_url(:xmlElement) :: url()
407 | def get_slo_post_url(md_elem), do: get_url(md_elem, @slo_post_url_selector)
408 |
409 | @spec get_url(:xmlElement, %SweetXpath{}) :: url()
410 | defp get_url(md_elem, selector) do
411 | case get_data(md_elem, selector) do
412 | "" -> nil
413 | url -> url
414 | end
415 | end
416 |
417 | @spec get_data(:xmlElement, %SweetXpath{}) :: binary()
418 | def get_data(md_elem, selector) do
419 | md_elem |> xpath(selector |> add_ns()) |> String.trim()
420 | end
421 |
422 | @spec add_ns(%SweetXpath{}) :: %SweetXpath{}
423 | defp add_ns(xpath) do
424 | xpath
425 | |> SweetXml.add_namespace("md", "urn:oasis:names:tc:SAML:2.0:metadata")
426 | |> SweetXml.add_namespace("ds", "http://www.w3.org/2000/09/xmldsig#")
427 | end
428 | end
429 |
--------------------------------------------------------------------------------
/lib/samly/provider.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.Provider do
2 | @moduledoc """
3 | SAML 2.0 Service Provider
4 |
5 | This should be added to the hosting Phoenix/Plug application's supervision tree.
6 | This GenServer initializes the SP configuration and loads the IDP medata XML
7 | containing information on how to communicate with the IDP.
8 |
9 | ```elixir
10 | # application.ex
11 |
12 | children = [
13 | # ...
14 | worker(Samly.Provider, []),
15 | ]
16 | ```
17 |
18 | Check README.md `Configuration` section.
19 | """
20 |
21 | use GenServer
22 | require Logger
23 |
24 | require Samly.Esaml
25 | alias Samly.{State}
26 |
27 | @doc false
28 | def start_link(gs_opts \\ []) do
29 | GenServer.start_link(__MODULE__, [], gs_opts)
30 | end
31 |
32 | @doc false
33 | def init([]) do
34 | store_env = Application.get_env(:samly, Samly.State, [])
35 | store_provider = store_env[:store] || Samly.State.ETS
36 | store_opts = store_env[:opts] || []
37 | State.init(store_provider, store_opts)
38 |
39 | opts = Application.get_env(:samly, Samly.Provider, [])
40 |
41 | # must be done prior to loading the providers
42 | idp_id_from =
43 | case opts[:idp_id_from] do
44 | nil ->
45 | :path_segment
46 |
47 | value when value in [:subdomain, :path_segment] ->
48 | value
49 |
50 | unknown ->
51 | Logger.warn(
52 | "[Samly] invalid_data idp_id_from: #{inspect(unknown)}. Using :path_segment"
53 | )
54 |
55 | :path_segment
56 | end
57 |
58 | Application.put_env(:samly, :idp_id_from, idp_id_from)
59 |
60 | service_providers = Samly.SpData.load_providers(opts[:service_providers] || [])
61 |
62 | identity_providers =
63 | Samly.IdpData.load_providers(opts[:identity_providers] || [], service_providers)
64 |
65 | Application.put_env(:samly, :service_providers, service_providers)
66 | Application.put_env(:samly, :identity_providers, identity_providers)
67 |
68 | {:ok, %{}}
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/samly/router.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.Router do
2 | @moduledoc false
3 |
4 | use Plug.Router
5 |
6 | plug :secure_samly
7 | plug :match
8 | plug :dispatch
9 |
10 | forward("/auth", to: Samly.AuthRouter)
11 | forward("/sp", to: Samly.SPRouter)
12 | forward("/csp-report", to: Samly.CsprRouter)
13 |
14 | match _ do
15 | conn |> send_resp(404, "not_found")
16 | end
17 |
18 | @csp """
19 | default-src 'none';
20 | script-src 'self' 'nonce-<%= nonce %>' 'report-sample';
21 | img-src 'self' 'report-sample';
22 | report-to /sso/csp-report;
23 | """
24 | |> String.replace("\n", " ")
25 |
26 | defp secure_samly(conn, _opts) do
27 | conn
28 | |> put_private(:samly_nonce, :crypto.strong_rand_bytes(18) |> Base.encode64())
29 | |> register_before_send(fn connection ->
30 | nonce = connection.private[:samly_nonce]
31 |
32 | connection
33 | |> put_resp_header("cache-control", "no-cache, no-store, must-revalidate")
34 | |> put_resp_header("pragma", "no-cache")
35 | |> put_resp_header("x-frame-options", "SAMEORIGIN")
36 | |> put_resp_header("content-security-policy", EEx.eval_string(@csp, nonce: nonce))
37 | |> put_resp_header("x-xss-protection", "1; mode=block")
38 | |> put_resp_header("x-content-type-options", "nosniff")
39 | end)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/samly/router_util.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.RouterUtil do
2 | @moduledoc false
3 |
4 | alias Plug.Conn
5 | require Logger
6 | require Samly.Esaml
7 | alias Samly.{Esaml, IdpData, Helper}
8 |
9 | @subdomain_re ~r/^(?([^.]+))?\./
10 |
11 | def check_idp_id(conn, _opts) do
12 | idp_id_from = Application.get_env(:samly, :idp_id_from)
13 |
14 | idp_id =
15 | if idp_id_from == :subdomain do
16 | case Regex.named_captures(@subdomain_re, conn.host) do
17 | %{"subdomain" => idp_id} -> idp_id
18 | _ -> nil
19 | end
20 | else
21 | case conn.params["idp_id_seg"] do
22 | [idp_id] -> idp_id
23 | _ -> nil
24 | end
25 | end
26 |
27 | idp = idp_id && Helper.get_idp(idp_id)
28 |
29 | if idp do
30 | conn |> Conn.put_private(:samly_idp, idp)
31 | else
32 | conn |> Conn.send_resp(403, "invalid_request unknown IdP") |> Conn.halt()
33 | end
34 | end
35 |
36 | def check_target_url(conn, _opts) do
37 | try do
38 | target_url = conn.params["target_url"] && URI.decode_www_form(conn.params["target_url"])
39 | conn |> Conn.put_private(:samly_target_url, target_url)
40 | rescue
41 | ArgumentError ->
42 | Logger.error(
43 | "[Samly] target_url must be x-www-form-urlencoded: #{inspect(conn.params["target_url"])}"
44 | )
45 |
46 | conn |> Conn.send_resp(400, "target_url must be x-www-form-urlencoded") |> Conn.halt()
47 | end
48 | end
49 |
50 | # generate URIs using the idp_id
51 | @spec ensure_sp_uris_set(tuple, Conn.t()) :: tuple
52 | def ensure_sp_uris_set(sp, conn) do
53 | case Esaml.esaml_sp(sp, :metadata_uri) do
54 | [?/ | _] ->
55 | uri = %URI{
56 | scheme: Atom.to_string(conn.scheme),
57 | host: conn.host,
58 | port: conn.port,
59 | path: "/sso"
60 | }
61 |
62 | base_url = URI.to_string(uri)
63 | idp_id_from = Application.get_env(:samly, :idp_id_from)
64 |
65 | path_segment_idp_id =
66 | if idp_id_from == :subdomain do
67 | nil
68 | else
69 | %IdpData{id: idp_id} = conn.private[:samly_idp]
70 | idp_id
71 | end
72 |
73 | Esaml.esaml_sp(
74 | sp,
75 | metadata_uri: Helper.get_metadata_uri(base_url, path_segment_idp_id),
76 | consume_uri: Helper.get_consume_uri(base_url, path_segment_idp_id),
77 | logout_uri: Helper.get_logout_uri(base_url, path_segment_idp_id)
78 | )
79 |
80 | _ ->
81 | sp
82 | end
83 | end
84 |
85 | def send_saml_request(conn, idp_url, use_redirect?, signed_xml_payload, relay_state) do
86 | if use_redirect? do
87 | url =
88 | :esaml_binding.encode_http_redirect(idp_url, signed_xml_payload, :undefined, relay_state)
89 |
90 | conn |> redirect(302, url)
91 | else
92 | nonce = conn.private[:samly_nonce]
93 | resp_body = :esaml_binding.encode_http_post(idp_url, signed_xml_payload, relay_state, nonce)
94 |
95 | conn
96 | |> Conn.put_resp_header("content-type", "text/html")
97 | |> Conn.send_resp(200, resp_body)
98 | end
99 | end
100 |
101 | def redirect(conn, status_code, dest) do
102 | conn
103 | |> Conn.put_resp_header("location", URI.encode(dest))
104 | |> Conn.send_resp(status_code, "")
105 | |> Conn.halt()
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/lib/samly/sp_data.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.SpData do
2 | @moduledoc false
3 |
4 | require Logger
5 | require Samly.Esaml
6 | alias Samly.SpData
7 |
8 | defstruct id: "",
9 | entity_id: "",
10 | certfile: "",
11 | keyfile: "",
12 | contact_name: "",
13 | contact_email: "",
14 | org_name: "",
15 | org_displayname: "",
16 | org_url: "",
17 | cert: :undefined,
18 | key: :undefined,
19 | valid?: true
20 |
21 | @type t :: %__MODULE__{
22 | id: binary(),
23 | entity_id: binary(),
24 | certfile: binary(),
25 | keyfile: binary(),
26 | contact_name: binary(),
27 | contact_email: binary(),
28 | org_name: binary(),
29 | org_displayname: binary(),
30 | org_url: binary(),
31 | cert: :undefined | binary(),
32 | key: :undefined | :RSAPrivateKey,
33 | valid?: boolean()
34 | }
35 |
36 | @type id :: binary
37 |
38 | @default_contact_name "Samly SP Admin"
39 | @default_contact_email "admin@samly"
40 | @default_org_name "Samly SP"
41 | @default_org_displayname "SAML SP built with Samly"
42 | @default_org_url "https://github.com/handnot2/samly"
43 |
44 | @spec load_providers(list(map)) :: %{required(id) => t}
45 | def load_providers(prov_configs) do
46 | prov_configs
47 | |> Enum.map(&load_provider/1)
48 | |> Enum.filter(fn sp_data -> sp_data.valid? end)
49 | |> Enum.map(fn sp_data -> {sp_data.id, sp_data} end)
50 | |> Enum.into(%{})
51 | end
52 |
53 | @spec load_provider(map) :: %SpData{} | no_return
54 | def load_provider(%{} = opts_map) do
55 | sp_data = %__MODULE__{
56 | id: Map.get(opts_map, :id, ""),
57 | entity_id: Map.get(opts_map, :entity_id, ""),
58 | certfile: Map.get(opts_map, :certfile, ""),
59 | keyfile: Map.get(opts_map, :keyfile, ""),
60 | contact_name: Map.get(opts_map, :contact_name, @default_contact_name),
61 | contact_email: Map.get(opts_map, :contact_email, @default_contact_email),
62 | org_name: Map.get(opts_map, :org_name, @default_org_name),
63 | org_displayname: Map.get(opts_map, :org_displayname, @default_org_displayname),
64 | org_url: Map.get(opts_map, :org_url, @default_org_url)
65 | }
66 |
67 | sp_data |> set_id(opts_map) |> load_cert(opts_map) |> load_key(opts_map)
68 | end
69 |
70 | @spec set_id(%SpData{}, map()) :: %SpData{}
71 | defp set_id(%SpData{} = sp_data, %{} = opts_map) do
72 | case Map.get(opts_map, :id, "") do
73 | "" ->
74 | Logger.error("[Samly] Invalid SP Config: #{inspect(opts_map)}")
75 | %SpData{sp_data | valid?: false}
76 |
77 | id ->
78 | %SpData{sp_data | id: id}
79 | end
80 | end
81 |
82 | @spec load_cert(%SpData{}, map()) :: %SpData{}
83 | defp load_cert(%SpData{certfile: ""} = sp_data, _) do
84 | %SpData{sp_data | cert: :undefined}
85 | end
86 |
87 | defp load_cert(%SpData{certfile: certfile} = sp_data, %{} = opts_map) do
88 | try do
89 | cert = :esaml_util.load_certificate(certfile)
90 | %SpData{sp_data | cert: cert}
91 | rescue
92 | _error ->
93 | Logger.error(
94 | "[Samly] Failed load SP certfile [#{inspect(certfile)}]: #{inspect(opts_map)}"
95 | )
96 |
97 | %SpData{sp_data | valid?: false}
98 | end
99 | end
100 |
101 | @spec load_key(%SpData{}, map()) :: %SpData{}
102 | defp load_key(%SpData{keyfile: ""} = sp_data, _) do
103 | %SpData{sp_data | key: :undefined}
104 | end
105 |
106 | defp load_key(%SpData{keyfile: keyfile} = sp_data, %{} = opts_map) do
107 | try do
108 | key = :esaml_util.load_private_key(keyfile)
109 | %SpData{sp_data | key: key}
110 | rescue
111 | _error ->
112 | Logger.error("[Samly] Failed load SP keyfile [#{inspect(keyfile)}]: #{inspect(opts_map)}")
113 | %SpData{sp_data | key: :undefined, valid?: false}
114 | end
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/lib/samly/sp_handler.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.SPHandler do
2 | @moduledoc false
3 |
4 | require Logger
5 | import Plug.Conn
6 | alias Plug.Conn
7 | require Samly.Esaml
8 | alias Samly.{Assertion, Esaml, Helper, IdpData, State, Subject}
9 |
10 | import Samly.RouterUtil, only: [ensure_sp_uris_set: 2, send_saml_request: 5, redirect: 3]
11 |
12 | def send_metadata(conn) do
13 | %IdpData{} = idp = conn.private[:samly_idp]
14 | %IdpData{esaml_idp_rec: _idp_rec, esaml_sp_rec: sp_rec} = idp
15 | sp = ensure_sp_uris_set(sp_rec, conn)
16 | metadata = Helper.sp_metadata(sp)
17 |
18 | conn
19 | |> put_resp_header("content-type", "text/xml")
20 | |> send_resp(200, metadata)
21 |
22 | # rescue
23 | # error ->
24 | # Logger.error("#{inspect error}")
25 | # conn |> send_resp(500, "request_failed")
26 | end
27 |
28 | def consume_signin_response(conn) do
29 | %IdpData{id: idp_id} = idp = conn.private[:samly_idp]
30 | %IdpData{pre_session_create_pipeline: pipeline, esaml_sp_rec: sp_rec} = idp
31 | sp = ensure_sp_uris_set(sp_rec, conn)
32 |
33 | saml_encoding = conn.body_params["SAMLEncoding"]
34 | saml_response = conn.body_params["SAMLResponse"]
35 | relay_state = conn.body_params["RelayState"] |> safe_decode_www_form()
36 |
37 | with {:ok, assertion} <- Helper.decode_idp_auth_resp(sp, saml_encoding, saml_response),
38 | :ok <- validate_authresp(conn, assertion, relay_state),
39 | assertion = %Assertion{assertion | idp_id: idp_id},
40 | conn = conn |> put_private(:samly_assertion, assertion),
41 | {:halted, %Conn{halted: false} = conn} <- {:halted, pipethrough(conn, pipeline)} do
42 | updated_assertion = conn.private[:samly_assertion]
43 | computed = updated_assertion.computed
44 | assertion = %Assertion{assertion | computed: computed, idp_id: idp_id}
45 |
46 | nameid = assertion.subject.name
47 | assertion_key = {idp_id, nameid}
48 | conn = State.put_assertion(conn, assertion_key, assertion)
49 | target_url = auth_target_url(conn, assertion, relay_state)
50 |
51 | conn
52 | |> configure_session(renew: true)
53 | |> put_session("samly_assertion_key", assertion_key)
54 | |> redirect(302, target_url)
55 | else
56 | {:halted, conn} -> conn
57 | {:error, reason} -> conn |> send_resp(403, "access_denied #{inspect(reason)}")
58 | _ -> conn |> send_resp(403, "access_denied")
59 | end
60 |
61 | # rescue
62 | # error ->
63 | # Logger.error("#{inspect error}")
64 | # conn |> send_resp(500, "request_failed")
65 | end
66 |
67 | # IDP-initiated flow auth response
68 | @spec validate_authresp(Conn.t(), Assertion.t(), binary) :: :ok | {:error, atom}
69 | defp validate_authresp(conn, %{subject: %{in_response_to: ""}}, relay_state) do
70 | idp_data = conn.private[:samly_idp]
71 |
72 | if idp_data.allow_idp_initiated_flow do
73 | if idp_data.allowed_target_urls do
74 | if relay_state in idp_data.allowed_target_urls do
75 | :ok
76 | else
77 | {:error, :invalid_target_url}
78 | end
79 | else
80 | :ok
81 | end
82 | else
83 | {:error, :idp_first_flow_not_allowed}
84 | end
85 | end
86 |
87 | # SP-initiated flow auth response
88 | defp validate_authresp(conn, _assertion, relay_state) do
89 | %IdpData{id: idp_id} = conn.private[:samly_idp]
90 | rs_in_session = get_session(conn, "relay_state")
91 | idp_id_in_session = get_session(conn, "idp_id")
92 | url_in_session = get_session(conn, "target_url")
93 |
94 | cond do
95 | rs_in_session == nil || rs_in_session != relay_state ->
96 | {:error, :invalid_relay_state}
97 |
98 | idp_id_in_session == nil || idp_id_in_session != idp_id ->
99 | {:error, :invalid_idp_id}
100 |
101 | url_in_session == nil ->
102 | {:error, :invalid_target_url}
103 |
104 | true ->
105 | :ok
106 | end
107 | end
108 |
109 | defp pipethrough(conn, nil), do: conn
110 |
111 | defp pipethrough(conn, pipeline) do
112 | pipeline.call(conn, [])
113 | end
114 |
115 | defp auth_target_url(_conn, %{subject: %{in_response_to: ""}}, ""), do: "/"
116 | defp auth_target_url(_conn, %{subject: %{in_response_to: ""}}, url), do: url
117 |
118 | defp auth_target_url(conn, _assertion, _relay_state) do
119 | get_session(conn, "target_url") || "/"
120 | end
121 |
122 | def handle_logout_response(conn) do
123 | %IdpData{id: idp_id} = idp = conn.private[:samly_idp]
124 | %IdpData{esaml_idp_rec: _idp_rec, esaml_sp_rec: sp_rec} = idp
125 | sp = ensure_sp_uris_set(sp_rec, conn)
126 |
127 | saml_encoding = conn.body_params["SAMLEncoding"]
128 | saml_response = conn.body_params["SAMLResponse"]
129 | relay_state = conn.body_params["RelayState"] |> safe_decode_www_form()
130 |
131 | with {:ok, _payload} <- Helper.decode_idp_signout_resp(sp, saml_encoding, saml_response),
132 | ^relay_state when relay_state != nil <- get_session(conn, "relay_state"),
133 | ^idp_id <- get_session(conn, "idp_id"),
134 | target_url when target_url != nil <- get_session(conn, "target_url") do
135 | conn
136 | |> configure_session(drop: true)
137 | |> redirect(302, target_url)
138 | else
139 | error -> conn |> send_resp(403, "invalid_request #{inspect(error)}")
140 | end
141 |
142 | # rescue
143 | # error ->
144 | # Logger.error("#{inspect error}")
145 | # conn |> send_resp(500, "request_failed")
146 | end
147 |
148 | # non-ui logout request from IDP
149 | def handle_logout_request(conn) do
150 | %IdpData{id: idp_id} = idp = conn.private[:samly_idp]
151 | %IdpData{esaml_idp_rec: idp_rec, esaml_sp_rec: sp_rec} = idp
152 | sp = ensure_sp_uris_set(sp_rec, conn)
153 |
154 | saml_encoding = conn.body_params["SAMLEncoding"]
155 | saml_request = conn.body_params["SAMLRequest"]
156 | relay_state = conn.body_params["RelayState"] |> safe_decode_www_form()
157 |
158 | with {:ok, payload} <- Helper.decode_idp_signout_req(sp, saml_encoding, saml_request) do
159 | Esaml.esaml_logoutreq(name: nameid, issuer: _issuer) = payload
160 | assertion_key = {idp_id, nameid}
161 |
162 | {conn, return_status} =
163 | case State.get_assertion(conn, assertion_key) do
164 | %Assertion{idp_id: ^idp_id, subject: %Subject{name: ^nameid}} ->
165 | conn = State.delete_assertion(conn, assertion_key)
166 | {conn, :success}
167 |
168 | _ ->
169 | {conn, :denied}
170 | end
171 |
172 | {idp_signout_url, resp_xml_frag} = Helper.gen_idp_signout_resp(sp, idp_rec, return_status)
173 |
174 | conn
175 | |> configure_session(drop: true)
176 | |> send_saml_request(idp_signout_url, idp.use_redirect_for_req, resp_xml_frag, relay_state)
177 | else
178 | error ->
179 | Logger.error("#{inspect(error)}")
180 | {idp_signout_url, resp_xml_frag} = Helper.gen_idp_signout_resp(sp, idp_rec, :denied)
181 |
182 | conn
183 | |> send_saml_request(
184 | idp_signout_url,
185 | idp.use_redirect_for_req,
186 | resp_xml_frag,
187 | relay_state
188 | )
189 | end
190 |
191 | # rescue
192 | # error ->
193 | # Logger.error("#{inspect error}")
194 | # conn |> send_resp(500, "request_failed")
195 | end
196 |
197 | defp safe_decode_www_form(nil), do: ""
198 | defp safe_decode_www_form(data), do: URI.decode_www_form(data)
199 | end
200 |
--------------------------------------------------------------------------------
/lib/samly/sp_router.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.SPRouter do
2 | @moduledoc false
3 |
4 | use Plug.Router
5 | import Plug.Conn
6 | import Samly.RouterUtil, only: [check_idp_id: 2]
7 |
8 | plug :fetch_session
9 | plug :match
10 | plug :check_idp_id
11 | plug :dispatch
12 |
13 | get "/metadata/*idp_id_seg" do
14 | # TODO: Make a release task to generate SP metadata
15 | conn |> Samly.SPHandler.send_metadata()
16 | end
17 |
18 | post "/consume/*idp_id_seg" do
19 | conn |> Samly.SPHandler.consume_signin_response()
20 | end
21 |
22 | post "/logout/*idp_id_seg" do
23 | cond do
24 | conn.params["SAMLResponse"] != nil -> Samly.SPHandler.handle_logout_response(conn)
25 | conn.params["SAMLRequest"] != nil -> Samly.SPHandler.handle_logout_request(conn)
26 | true -> conn |> send_resp(403, "invalid_request")
27 | end
28 | end
29 |
30 | match _ do
31 | conn |> send_resp(404, "not_found")
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/samly/state.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.State do
2 | @moduledoc false
3 |
4 | @state_store :state_store
5 |
6 | def init(store_provider), do: init(store_provider, [])
7 |
8 | def init(store_provider, opts) do
9 | opts = store_provider.init(opts)
10 | Application.put_env(:samly, @state_store, %{provider: store_provider, opts: opts})
11 | end
12 |
13 | def get_assertion(conn, assertion_key) do
14 | %{provider: store_provider, opts: opts} = Application.get_env(:samly, @state_store)
15 | store_provider.get_assertion(conn, assertion_key, opts)
16 | end
17 |
18 | def put_assertion(conn, assertion_key, assertion) do
19 | %{provider: store_provider, opts: opts} = Application.get_env(:samly, @state_store)
20 | store_provider.put_assertion(conn, assertion_key, assertion, opts)
21 | end
22 |
23 | def delete_assertion(conn, assertion_key) do
24 | %{provider: store_provider, opts: opts} = Application.get_env(:samly, @state_store)
25 | store_provider.delete_assertion(conn, assertion_key, opts)
26 | end
27 |
28 | def gen_id() do
29 | 24 |> :crypto.strong_rand_bytes() |> Base.url_encode64()
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/samly/state/ets.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.State.ETS do
2 | @moduledoc """
3 | Stores SAML assertion in ETS.
4 |
5 | This provider creates an ETS table (during initialization) to keep the
6 | authenticated SAML assertions from IdP. The ETS table name in the
7 | configuration is optional.
8 |
9 | ## Options
10 |
11 | + `:table` - ETS table name (optional)
12 | Value must be an atom
13 |
14 | Do not rely on how the state is stored in the ETS table.
15 |
16 | ## Configuration Example
17 |
18 | config :samly, Samly.State,
19 | opts: [table: :my_ets_table]
20 |
21 | This can be used as an example when creating custom stores based on
22 | redis, memcached, database etc.
23 | """
24 |
25 | alias Samly.Assertion
26 |
27 | @behaviour Samly.State.Store
28 |
29 | @assertions_table :samly_assertions_table
30 |
31 | @impl Samly.State.Store
32 | def init(opts) do
33 | assertions_table = Keyword.get(opts, :table, @assertions_table)
34 |
35 | if is_atom(assertions_table) == false do
36 | raise "Samly.State.ETS table name must be an atom: #{inspect(assertions_table)}"
37 | end
38 |
39 | if :ets.info(assertions_table) == :undefined do
40 | :ets.new(assertions_table, [:set, :public, :named_table])
41 | end
42 |
43 | assertions_table
44 | end
45 |
46 | @impl Samly.State.Store
47 | def get_assertion(_conn, assertion_key, assertions_table) do
48 | case :ets.lookup(assertions_table, assertion_key) do
49 | [{^assertion_key, %Assertion{} = assertion}] -> assertion
50 | _ -> nil
51 | end
52 | end
53 |
54 | @impl Samly.State.Store
55 | def put_assertion(conn, assertion_key, assertion, assertions_table) do
56 | :ets.insert(assertions_table, {assertion_key, assertion})
57 | conn
58 | end
59 |
60 | @impl Samly.State.Store
61 | def delete_assertion(conn, assertion_key, assertions_table) do
62 | :ets.delete(assertions_table, assertion_key)
63 | conn
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/samly/state/session.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.State.Session do
2 | @moduledoc """
3 | Stores SAML assertion in Plug session.
4 |
5 | This provider uses Plug session to save the authenticated SAML
6 | assertions from IdP. The session key name in the configuration is optional.
7 |
8 | ## Options
9 |
10 | + `:key` - Session key name used when saving the assertion (optional)
11 | Value is either a binary or an atom
12 |
13 | ## Configuration Example
14 |
15 | config :samly, Samly.State,
16 | store: Samly.State.Session,
17 | opts: [key: :my_assertion]
18 | """
19 |
20 | alias Plug.Conn
21 | alias Samly.Assertion
22 |
23 | @behaviour Samly.State.Store
24 |
25 | @session_key "samly_assertion"
26 |
27 | @impl Samly.State.Store
28 | def init(opts) do
29 | opts |> Map.new() |> Map.put_new(:key, @session_key)
30 | end
31 |
32 | @impl Samly.State.Store
33 | def get_assertion(conn, assertion_key, opts) do
34 | %{key: key} = opts
35 |
36 | case Conn.get_session(conn, key) do
37 | {^assertion_key, %Assertion{} = assertion} -> assertion
38 | _ -> nil
39 | end
40 | end
41 |
42 | @impl Samly.State.Store
43 | def put_assertion(conn, assertion_key, assertion, opts) do
44 | %{key: key} = opts
45 | Conn.put_session(conn, key, {assertion_key, assertion})
46 | end
47 |
48 | @impl Samly.State.Store
49 | def delete_assertion(conn, _assertion_key, opts) do
50 | %{key: key} = opts
51 | Conn.delete_session(conn, key)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/samly/state/store.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.State.Store do
2 | @moduledoc """
3 | Specification for Samly state stores.
4 | """
5 | alias Plug.Conn
6 | alias Samly.Assertion
7 |
8 | @typedoc """
9 | Options passed during the store initialization.
10 | """
11 | @type opts :: Plug.opts()
12 |
13 | @typedoc """
14 | IdP identifier associated with the assertion.
15 | """
16 | @type idp_id :: binary
17 |
18 | @typedoc """
19 | SAML `nameid` returned by IdP.
20 | """
21 | @type name_id :: binary
22 |
23 | @typedoc """
24 | The `name_id` should not be used independent of the `idp_id`. It is within the scope of `idp_id`.
25 | Together these form the assertion key.
26 | """
27 | @type assertion_key :: {idp_id(), name_id()}
28 |
29 | @doc """
30 | Initializes the store.
31 |
32 | The options returned from this function will be given
33 | to `get_assertion/3`, `put_assertion/4` and `delete_assertion/3`.
34 | """
35 | @callback init(opts()) :: opts() | no_return()
36 |
37 | @doc """
38 | Returns a Samly assertion if present in the store.
39 |
40 | Returns `nil` if the assertion for the given key is not present in the store.
41 | """
42 | @callback get_assertion(Conn.t(), assertion_key(), opts()) :: Assertion.t() | nil
43 |
44 | @doc """
45 | Saves the given SAML assertion in the store.
46 |
47 | May raise an error if there is a failure. An authenticated session should not be
48 | established in that case.
49 | """
50 | @callback put_assertion(Conn.t(), assertion_key(), Assertion.t(), opts()) ::
51 | Conn.t() | no_return()
52 |
53 | @doc """
54 | Removes the given SAML assertion from the store.
55 |
56 | May raise an error if there is a failure. An authenticated session must be terminated
57 | after calling this.
58 | """
59 | @callback delete_assertion(Conn.t(), assertion_key(), opts()) :: Conn.t() | no_return()
60 | end
61 |
--------------------------------------------------------------------------------
/lib/samly/subject.ex:
--------------------------------------------------------------------------------
1 | defmodule Samly.Subject do
2 | @moduledoc """
3 | The subject in a SAML 2.0 Assertion.
4 |
5 | This is part of the `Samly.Assertion` struct. The `name` field in this struct should not
6 | be used in any UI directly. It might be a temporary randomly generated
7 | ID from IdP. `Samly` internally uses this to deal with IdP initiated logout requests.
8 |
9 | If an authentication request was sent from `Samly` (SP initiated), the SAML response
10 | is expected to include the original request ID. This ID is made available in
11 | `Samly.Subject.in_response_to`.
12 |
13 | If the authentication request originated from the IDP (IDP initiated), there won't
14 | be a `Samly` request ID associated with it. The `Samly.Subject.in_response_to`
15 | will be an empty string in that case.
16 | """
17 |
18 | require Samly.Esaml
19 | alias Samly.Esaml
20 |
21 | defstruct name: "",
22 | name_qualifier: :undefined,
23 | sp_name_qualifier: :undefined,
24 | name_format: :undefined,
25 | confirmation_method: :bearer,
26 | notonorafter: "",
27 | in_response_to: ""
28 |
29 | @type t :: %__MODULE__{
30 | name: String.t(),
31 | name_qualifier: :undefined | String.t(),
32 | sp_name_qualifier: :undefined | String.t(),
33 | name_format: :undefined | String.t(),
34 | confirmation_method: atom,
35 | notonorafter: String.t(),
36 | in_response_to: String.t()
37 | }
38 |
39 | @doc false
40 | def from_rec(subject_rec) do
41 | Esaml.esaml_subject(
42 | name: name,
43 | name_qualifier: name_qualifier,
44 | sp_name_qualifier: sp_name_qualifier,
45 | name_format: name_format,
46 | confirmation_method: confirmation_method,
47 | notonorafter: notonorafter,
48 | in_response_to: in_response_to
49 | ) = subject_rec
50 |
51 | %__MODULE__{
52 | name: name |> List.to_string(),
53 | name_qualifier: to_string_or_undefined(name_qualifier),
54 | sp_name_qualifier: to_string_or_undefined(sp_name_qualifier),
55 | name_format: to_string_or_undefined(name_format),
56 | confirmation_method: confirmation_method,
57 | notonorafter: notonorafter |> List.to_string(),
58 | in_response_to: in_response_to |> List.to_string()
59 | }
60 | end
61 |
62 | @doc false
63 | def to_rec(subject) do
64 | Esaml.esaml_subject(
65 | name: String.to_charlist(subject.name),
66 | name_qualifier: from_string_or_undefined(subject.name_qualifier),
67 | sp_name_qualifier: from_string_or_undefined(subject.sp_name_qualifier),
68 | name_format: from_string_or_undefined(subject.name_format),
69 | confirmation_method: subject.confirmation_method,
70 | notonorafter: String.to_charlist(subject.notonorafter),
71 | in_response_to: String.to_charlist(subject.in_response_to)
72 | )
73 | end
74 |
75 | defp to_string_or_undefined(:undefined), do: :undefined
76 | defp to_string_or_undefined(s) when is_list(s), do: List.to_string(s)
77 |
78 | defp from_string_or_undefined(:undefined), do: :undefined
79 | defp from_string_or_undefined(s) when is_binary(s), do: String.to_charlist(s)
80 | end
81 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Samly.Mixfile do
2 | use Mix.Project
3 |
4 | @version "1.0.0"
5 | @description "SAML Single-Sign-On Authentication for Plug/Phoenix Applications"
6 | @source_url "https://github.com/handnot2/samly"
7 | @blog_url "https://handnot2.github.io/blog/auth/saml-auth-for-phoenix"
8 |
9 | def project() do
10 | [
11 | app: :samly,
12 | version: @version,
13 | description: @description,
14 | docs: docs(),
15 | package: package(),
16 | elixir: "~> 1.6",
17 | start_permanent: Mix.env() == :prod,
18 | deps: deps()
19 | ]
20 | end
21 |
22 | # Run "mix help compile.app" to learn about applications.
23 | def application() do
24 | [
25 | extra_applications: [:logger, :eex]
26 | ]
27 | end
28 |
29 | # Run "mix help deps" to learn about dependencies.
30 | defp deps() do
31 | [
32 | {:plug, "~> 1.6"},
33 | {:esaml, "~> 4.2"},
34 | {:sweet_xml, "~> 0.6.6"},
35 | {:ex_doc, "~> 0.19.0", only: :dev, runtime: false},
36 | {:inch_ex, "~> 1.0", only: [:dev, :test]}
37 | ]
38 | end
39 |
40 | defp docs() do
41 | [
42 | extras: ["README.md"],
43 | main: "readme",
44 | source_ref: "v#{@version}",
45 | source_url: @source_url
46 | ]
47 | end
48 |
49 | defp package() do
50 | [
51 | maintainers: ["handnot2"],
52 | files: ["config", "lib", "LICENSE", "mix.exs", "README.md"],
53 | licenses: ["MIT"],
54 | links: %{
55 | "GitHub" => @source_url,
56 | "Blog" => @blog_url
57 | }
58 | ]
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "cowboy": {:hex, :cowboy, "2.6.0", "dc1ff5354c89e36a3e3ef8d10433396dcff0dcbb1d4223b58c64c2d51a6d88d9", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
3 | "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"},
4 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"},
5 | "esaml": {:hex, :esaml, "4.2.0", "e3236ec9c1974c50f50766b9923f7f127a95a17b75ea229ef5dad36a7e6dd5fc", [:rebar3], [{:cowboy, "2.6.0", [hex: :cowboy, repo: "hexpm", optional: false]}], "hexpm"},
6 | "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
7 | "inch_ex": {:hex, :inch_ex, "1.0.1", "1f0af1a83cec8e56f6fc91738a09c838e858db3d78ef5f2ec040fe4d5a62dabf", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
8 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
9 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
10 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
11 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
12 | "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
13 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
14 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
15 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
16 | "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},
17 | }
18 |
--------------------------------------------------------------------------------
/test/data/azure_fed_metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | PxBXxVXHYmtelxnhr2ewHTGKEkuviPF429grQa1WNIw=
17 |
18 |
19 | o/8AXiWeX8TpD76ix+hlpX//CnBjL4i5xxniJLDTqcKsVBCa544vBOJsCFKS4Syd7AoXD5Yhj6PPzpfUq6kR+vQRZS1w5/XQ7juSv/GCSLSJaL3Zhhm2Uej/jtJf+Wxq8yrsTXyECPfqx8XTD6RIbEOGcn9Ug3wkuUpnzf3Kgl8PNUNhYa+bKyehsUNmPr5geFqxk/o1Pq/prVFy7gBGXEGPMHigkczYya2vxhZCpLkKUcqm3JlQvPdCFuH060F5/MSKxEJttt/KQIer7fvLD832Yw1LeI+aarJxOXEAgJpbohlE5Wzxs2GvrSvzCqiECTCXZOVAdQ+LGIHJMvehcA==
20 |
21 |
22 | MIIDBTCCAe2gAwIBAgIQQm0sN9lDrblM/7U/vYMVmTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE3MDkwNjAwMDAwMFoXDTE5MDkwNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAONDCuIodN3I6J7+Wc9j5sY5MgLkGZZuUxLApHEmIhgWIVSSyMgM+eu0Nrr5FuIylJrKg/SHOJeLwFuYZOEMfRUVcKRewUTIBKmILqlg5+d56ztYZATuhm60B5bp1RUlUQMZ0GTepz9xpS9hO4Mv5GRBxR5xoBKCOF74xfVuHNkk/wnbV/Yggwt82FrGm8CpiXjbmFzy61Vmp6KbFDadJDij2mmICSwzAJEoVvklLzcL/4Vf03v3l8aBkI/SPfDjm6k55dqeDm+nEIN/baWbs7M2WtNkJXNePy8dR0GkdlhbgESEIJdSVLWeBFt8eV0JQqUXcPCjhwpJE89jrHhwyCECAwEAAaMhMB8wHQYDVR0OBBYEFNISA3dtAzEd0muqNDbWm3kvNlJDMA0GCSqGSIb3DQEBCwUAA4IBAQClLLoAvg3dYqWO63Z6O5L7yataGcilmL3YUqCFoRKsuwej2T833qyc1iLG0iWCGeWAUonKXuGwfCSSSj2E3ksLtgV6xmuMl+NuVPpRpQo+38n+OxUoWKu963dMxnORFENEqKW0pMioipMk/HBaW3aJWyH1oT2rZ3KhFm67SFjKscF8ShAE82tQQIFwEFAXjMItW2oZVGDz3vDOaJN5xC8rfA6xkXTdcCuzy74SalKkLhpBO8S3XIOBVRZw+l0Koog8YNqhsvGsGS+hGXXNlCZTg0I1tR3g2DcSuHRcuTZKh7Z7XPPsDgleNirtvYFEvdvD4K2I7gb2H1xQn87oYAIX
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 | MIIDBTCCAe2gAwIBAgIQQm0sN9lDrblM/7U/vYMVmTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE3MDkwNjAwMDAwMFoXDTE5MDkwNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAONDCuIodN3I6J7+Wc9j5sY5MgLkGZZuUxLApHEmIhgWIVSSyMgM+eu0Nrr5FuIylJrKg/SHOJeLwFuYZOEMfRUVcKRewUTIBKmILqlg5+d56ztYZATuhm60B5bp1RUlUQMZ0GTepz9xpS9hO4Mv5GRBxR5xoBKCOF74xfVuHNkk/wnbV/Yggwt82FrGm8CpiXjbmFzy61Vmp6KbFDadJDij2mmICSwzAJEoVvklLzcL/4Vf03v3l8aBkI/SPfDjm6k55dqeDm+nEIN/baWbs7M2WtNkJXNePy8dR0GkdlhbgESEIJdSVLWeBFt8eV0JQqUXcPCjhwpJE89jrHhwyCECAwEAAaMhMB8wHQYDVR0OBBYEFNISA3dtAzEd0muqNDbWm3kvNlJDMA0GCSqGSIb3DQEBCwUAA4IBAQClLLoAvg3dYqWO63Z6O5L7yataGcilmL3YUqCFoRKsuwej2T833qyc1iLG0iWCGeWAUonKXuGwfCSSSj2E3ksLtgV6xmuMl+NuVPpRpQo+38n+OxUoWKu963dMxnORFENEqKW0pMioipMk/HBaW3aJWyH1oT2rZ3KhFm67SFjKscF8ShAE82tQQIFwEFAXjMItW2oZVGDz3vDOaJN5xC8rfA6xkXTdcCuzy74SalKkLhpBO8S3XIOBVRZw+l0Koog8YNqhsvGsGS+hGXXNlCZTg0I1tR3g2DcSuHRcuTZKh7Z7XPPsDgleNirtvYFEvdvD4K2I7gb2H1xQn87oYAIX
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | MIIDBTCCAe2gAwIBAgIQG3bMDDyO6q1GrI5sdZXCrTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE3MTAxODAwMDAwMFoXDTE5MTAxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPkg1ILQtpwFPPqiBBV3wXd89KoDW5NA/DiBQPZYINe//g6gdrOcnT6JjS5mnntXURsvlC8a8QPNCeX9zuxi0MvEoXjuyROJwnAhZyHmkWQh850pUiFTICCuDJXBrMiMdLYfeaJ9C0SApw2OhDiCED77Hh3oxuP49ooS5My96/b3PM76KC/GTv4VM5ydPOXpvUpLzMoavQdOGUCjUYU2z9twCYXmCLtwOiNWFW9yqof6tSAxEZRezq0Hi9qU6DF0MIp0TT6FKvUC8ui4pcYX7hZ6U9yOXJVB0+6HXdpGQ42sypvoK1h8mcRRLA5Ad/ox6YIJ39j0tJm5y+4H37NvHUCAwEAAaMhMB8wHQYDVR0OBBYEFCO/QGygHvo1YiKeQVulJFVxO9dnMA0GCSqGSIb3DQEBCwUAA4IBAQBZTJK52b+QnBbLicaT5uxC3JnRwps6RovQzPZRBLpxATq4kj5jNMhegb5fx4Rc1dpepXWJHAGzD0Nwsab/vYSx7iqyU02IAUkwt3k7XyYK17R6gTgUAxEFBfRKM3PSFiH0b3tGA+baLT3BdY5U6ZqjxhFA0Rh7tzPZM1TO2WtENk3hKmG5r5GKECnwa5NiE5jxN+d6i8dqM+vMqDvIrfqTA3ooQWXpvs0I9YUWl/LjBNFqyY3rMzxLX3STobLFf8ayHIvVmtiFSM3glCO+8UtGKLwNnPFIfYx3VstJjOO8rjP0Z/oaZwhD0A7MrNp4ztwmXAIzYkGTVyDsNuQJgi1e
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | MIIDKDCCAhCgAwIBAgIQBHJvVNxP1oZO4HYKh+rypDANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTYxMTE2MDgwMDAwWhcNMTgxMTE2MDgwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn5BCs24Hh6L0BNitPrV5s+2/DBhaeytOmnghJKnqeJlhv3ZczShRM2Cp38LW8Y3wn7L3AJtolaSkF/joKN1l6GupzM+HOEdq7xZxFehxIHW7+25mG/WigBnhsBzLv1SR4uIbrQeS5M0kkLwJ9pOnVH3uzMGG6TRXPnK3ivlKl97AiUEKdlRjCQNLXvYf1ZqlC77c/ZCOHSX4kvIKR2uG+LNlSTRq2rn8AgMpFT4DSlEZz4RmFQvQupQzPpzozaz/gadBpJy/jgDmJlQMPXkHp7wClvbIBGiGRaY6eZFxNV96zwSR/GPNkTObdw2S8/SiAgvIhIcqWTPLY6aVTqJfAgMBAAGjWDBWMFQGA1UdAQRNMEuAEDUj0BrjP0RTbmoRPTRMY3WhJTAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXOCEARyb1TcT9aGTuB2Cofq8qQwDQYJKoZIhvcNAQELBQADggEBAGnLhDHVz2gLDiu9L34V3ro/6xZDiSWhGyHcGqky7UlzQH3pT5so8iF5P0WzYqVtogPsyC2LPJYSTt2vmQugD4xlu/wbvMFLcV0hmNoTKCF1QTVtEQiAiy0Aq+eoF7Al5fV1S3Sune0uQHimuUFHCmUuF190MLcHcdWnPAmzIc8fv7quRUUsExXmxSX2ktUYQXzqFyIOSnDCuWFm6tpfK5JXS8fW5bpqTlrysXXz/OW/8NFGq/alfjrya4ojrOYLpunGriEtNPwK7hxj1AlCYEWaRHRXaUIW1ByoSff/6Y6+ZhXPUe0cDlNRt/qIz5aflwO7+W8baTS4O8m/icu7ItE=
52 |
53 |
54 |
55 |
56 |
57 |
60 | Name
61 | The mutable display name of the user.
62 |
63 |
66 | Subject
67 | An immutable, globally unique, non-reusable identifier of the user that is unique to the application for which a token is issued. Given Name
68 | First name of the user.
69 |
70 |
73 | Surname
74 | Last name of the user.
75 |
76 |
79 | Display Name
80 | Display name of the user.
81 |
82 |
85 | Nick Name
86 | Nick name of the user.
87 |
88 |
91 | Authentication Instant
92 | The time (UTC) when the user is authenticated to Windows Azure Active Directory.
93 |
94 |
97 | Authentication Method
98 | The method that Windows Azure Active Directory uses to authenticate users.
99 |
100 |
103 | ObjectIdentifier
104 | Primary identifier for the user in the directory. Immutable, globally unique, non-reusable.
105 |
106 |
109 | TenantId
110 | Identifier for the user's tenant.
111 |
112 |
115 | IdentityProvider
116 | Identity provider for the user.
117 |
118 |
121 | Email
122 | Email address of the user.
123 |
124 |
127 | Groups
128 | Groups of the user.
129 |
130 |
133 | External Access Token
134 | Access token issued by external identity provider.
135 |
136 |
139 | External Access Token Expiration
140 | UTC expiration time of access token issued by external identity provider.
141 |
142 |
145 | External OpenID 2.0 Identifier
146 | OpenID 2.0 identifier issued by external identity provider.
147 |
148 |
151 | GroupsOverageClaim
152 | Issued when number of user's group claims exceeds return limit.
153 |
154 |
157 | Role Claim
158 | Roles that the user or Service Principal is attached to
159 |
160 |
163 | RoleTemplate Id Claim
164 | Role template id of the Built-in Directory Roles that the user is a member of
165 |
166 |
167 |
168 |
169 | https://login.microsoftonline.com/common/wsfed
170 |
171 |
172 |
173 |
174 | https://login.microsoftonline.com/common/wsfed
175 |
176 |
177 |
178 | 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 https://sts.windows.net/%7Btenantid%7D/ https://login.microsoftonline.com/common/wsfed https://login.microsoftonline.com/common/wsfed
179 |
180 |
181 |
182 |
183 |
184 | MIIDBTCCAe2gAwIBAgIQQm0sN9lDrblM/7U/vYMVmTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE3MDkwNjAwMDAwMFoXDTE5MDkwNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAONDCuIodN3I6J7+Wc9j5sY5MgLkGZZuUxLApHEmIhgWIVSSyMgM+eu0Nrr5FuIylJrKg/SHOJeLwFuYZOEMfRUVcKRewUTIBKmILqlg5+d56ztYZATuhm60B5bp1RUlUQMZ0GTepz9xpS9hO4Mv5GRBxR5xoBKCOF74xfVuHNkk/wnbV/Yggwt82FrGm8CpiXjbmFzy61Vmp6KbFDadJDij2mmICSwzAJEoVvklLzcL/4Vf03v3l8aBkI/SPfDjm6k55dqeDm+nEIN/baWbs7M2WtNkJXNePy8dR0GkdlhbgESEIJdSVLWeBFt8eV0JQqUXcPCjhwpJE89jrHhwyCECAwEAAaMhMB8wHQYDVR0OBBYEFNISA3dtAzEd0muqNDbWm3kvNlJDMA0GCSqGSIb3DQEBCwUAA4IBAQClLLoAvg3dYqWO63Z6O5L7yataGcilmL3YUqCFoRKsuwej2T833qyc1iLG0iWCGeWAUonKXuGwfCSSSj2E3ksLtgV6xmuMl+NuVPpRpQo+38n+OxUoWKu963dMxnORFENEqKW0pMioipMk/HBaW3aJWyH1oT2rZ3KhFm67SFjKscF8ShAE82tQQIFwEFAXjMItW2oZVGDz3vDOaJN5xC8rfA6xkXTdcCuzy74SalKkLhpBO8S3XIOBVRZw+l0Koog8YNqhsvGsGS+hGXXNlCZTg0I1tR3g2DcSuHRcuTZKh7Z7XPPsDgleNirtvYFEvdvD4K2I7gb2H1xQn87oYAIX
185 |
186 |
187 |
188 |
189 |
190 |
191 | MIIDBTCCAe2gAwIBAgIQG3bMDDyO6q1GrI5sdZXCrTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE3MTAxODAwMDAwMFoXDTE5MTAxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPkg1ILQtpwFPPqiBBV3wXd89KoDW5NA/DiBQPZYINe//g6gdrOcnT6JjS5mnntXURsvlC8a8QPNCeX9zuxi0MvEoXjuyROJwnAhZyHmkWQh850pUiFTICCuDJXBrMiMdLYfeaJ9C0SApw2OhDiCED77Hh3oxuP49ooS5My96/b3PM76KC/GTv4VM5ydPOXpvUpLzMoavQdOGUCjUYU2z9twCYXmCLtwOiNWFW9yqof6tSAxEZRezq0Hi9qU6DF0MIp0TT6FKvUC8ui4pcYX7hZ6U9yOXJVB0+6HXdpGQ42sypvoK1h8mcRRLA5Ad/ox6YIJ39j0tJm5y+4H37NvHUCAwEAAaMhMB8wHQYDVR0OBBYEFCO/QGygHvo1YiKeQVulJFVxO9dnMA0GCSqGSIb3DQEBCwUAA4IBAQBZTJK52b+QnBbLicaT5uxC3JnRwps6RovQzPZRBLpxATq4kj5jNMhegb5fx4Rc1dpepXWJHAGzD0Nwsab/vYSx7iqyU02IAUkwt3k7XyYK17R6gTgUAxEFBfRKM3PSFiH0b3tGA+baLT3BdY5U6ZqjxhFA0Rh7tzPZM1TO2WtENk3hKmG5r5GKECnwa5NiE5jxN+d6i8dqM+vMqDvIrfqTA3ooQWXpvs0I9YUWl/LjBNFqyY3rMzxLX3STobLFf8ayHIvVmtiFSM3glCO+8UtGKLwNnPFIfYx3VstJjOO8rjP0Z/oaZwhD0A7MrNp4ztwmXAIzYkGTVyDsNuQJgi1e
192 |
193 |
194 |
195 |
196 |
197 |
198 | MIIDKDCCAhCgAwIBAgIQBHJvVNxP1oZO4HYKh+rypDANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTYxMTE2MDgwMDAwWhcNMTgxMTE2MDgwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChn5BCs24Hh6L0BNitPrV5s+2/DBhaeytOmnghJKnqeJlhv3ZczShRM2Cp38LW8Y3wn7L3AJtolaSkF/joKN1l6GupzM+HOEdq7xZxFehxIHW7+25mG/WigBnhsBzLv1SR4uIbrQeS5M0kkLwJ9pOnVH3uzMGG6TRXPnK3ivlKl97AiUEKdlRjCQNLXvYf1ZqlC77c/ZCOHSX4kvIKR2uG+LNlSTRq2rn8AgMpFT4DSlEZz4RmFQvQupQzPpzozaz/gadBpJy/jgDmJlQMPXkHp7wClvbIBGiGRaY6eZFxNV96zwSR/GPNkTObdw2S8/SiAgvIhIcqWTPLY6aVTqJfAgMBAAGjWDBWMFQGA1UdAQRNMEuAEDUj0BrjP0RTbmoRPTRMY3WhJTAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXOCEARyb1TcT9aGTuB2Cofq8qQwDQYJKoZIhvcNAQELBQADggEBAGnLhDHVz2gLDiu9L34V3ro/6xZDiSWhGyHcGqky7UlzQH3pT5so8iF5P0WzYqVtogPsyC2LPJYSTt2vmQugD4xlu/wbvMFLcV0hmNoTKCF1QTVtEQiAiy0Aq+eoF7Al5fV1S3Sune0uQHimuUFHCmUuF190MLcHcdWnPAmzIc8fv7quRUUsExXmxSX2ktUYQXzqFyIOSnDCuWFm6tpfK5JXS8fW5bpqTlrysXXz/OW/8NFGq/alfjrya4ojrOYLpunGriEtNPwK7hxj1AlCYEWaRHRXaUIW1ByoSff/6Y6+ZhXPUe0cDlNRt/qIz5aflwO7+W8baTS4O8m/icu7ItE=
199 |
200 |
201 |
202 |
205 |
208 |
211 |
212 |
213 |
--------------------------------------------------------------------------------
/test/data/idp_metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | MIIF1TCCA72gAwIBAgIJAKdEDzxP3jJLMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWlkbGFuZHMxEjAQBgNVBAcMCVNhZmV2aWxsZTEWMBQGA1UECgwNSUQgRmVkZXJhdGlvbjEhMB8GA1UECwwYRGVwYXJ0bWVudCBvZiBJZGVudGl0aWVzMQ8wDQYDVQQDDAZteS5pZHAwHhcNMTcwOTA5MDExNzMwWhcNMTgwOTA5MDExNzMwWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pZGxhbmRzMRIwEAYDVQQHDAlTYWZldmlsbGUxFjAUBgNVBAoMDUlEIEZlZGVyYXRpb24xITAfBgNVBAsMGERlcGFydG1lbnQgb2YgSWRlbnRpdGllczEPMA0GA1UEAwwGbXkuaWRwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5MXY8olt62YGPQQo4UIZ8Q48lMGGSA4DhEAooSA4yjXeKcmG/aDeINkcTzA/khv1iCy3gFDnJ6T6tGvNghWFDVLWrKRzg3PO0y3+6MaJAJ5CXtCaf+EV9KNFcrt8+DFwdudkWrwEak1gM8OHUfXvOqyD1a5g+vPrSm+QiI9+W4csTDB6zQKfMjnQJnUh+W8H4ZLPX2nJAeJLFhDfE1l66pjKHxVRl8QTiflpunMRNgTBknCUxs+3ilG+4QxXEgQdjzLJCc9SjgNEdqnJHd7BfUOsbIi5qjTAM3UkoumEdrLWNRCutDQoc5m79U9m+xcZso/z1qnrgPD6WkS6FiI4QFT8y/hjBEVbrXC5DZHu7ujeE8h2P2qOLjJszdW7SnvYFOshhMWD5JHkAn+Dzcch75pzvlsxxtmCw44IpCkeqjKeO2ajrQcx/fywSaCyme6gwmiF8hdXLrezj1sO0aFJhkEPM0Yb9JRkG2G3a8ITp4+3J8FCAXeLc7MJ/kxOJYVlNESSCzFhWYeSAR7doN9+eXCZ8dQhkOTN4FRT5u9ylq7tYHfAH1YNr2MKsEGrQtn/pTyb1Q4Oqb1IVpNnDRL2C9RPUt9Lfb8V3OHd1Mx81yoylXsvttX1DiPNY/KR/QCWQkvvfNTfUHTOGj8ic6G/t1/IenZ9RzNRPFxj4GNwHNcCAwEAAaNQME4wHQYDVR0OBBYEFJQbKzk3Twp4XgV0T6W0T5lZVFDZMB8GA1UdIwQYMBaAFJQbKzk3Twp4XgV0T6W0T5lZVFDZMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBALFPci1AfEp5bXuPWHbGSvAi+nVBzwHRE9VDIvsq1gVibmunwqFW7DlXi9xvPwrcQHVZtj2qeSWEc6Grw6iROgunclQIwaf6//uqb7Jkj9RhuhCs58CGcSBQ5umfffC/P2JbOte2hJjTTDNVvawAhPPBQ1bn/t8Y3eTHN/srQUtVBPX2szUqy5uRlpFYlZ2Layfcg3oLHg6Ube/HhEA2mjgCjod7zGtwuwsUOpjktQCp3aPLWXKN+ImcGAChpqAm9frgBWgZv4mxKynjOw15ur3wgSEJO7DNmEdjLqz2OiLHeX0MrDyGpcid5hPj4CgCN6vngeNXBEaoK2lJHp0I5HbdIoRBcghIVm9RzpfrDdK3/Ayy+wEAqjQRCuwBUEvrqsMnmnIjYw8HE+BE3pIS8hh9SyGCOgPJmHbYkwztkrNi6RJyeHROwAoER6J/D1saCKMK63bqLW5WLXDfftg6BNXXfvPjuQyaqlsP+HOjX8wTlKwemvUS93aDxwP8YdcFLt/uw/GXpmNKY+/wsymMYgXkTcS0FWgNUtpH/gZUIh3AMpCi1fBi71nc0uuLUBilRMrCHI5y1Rzi0qBfAkAKyUYbUO87KRvXQ3pjmpCC/IMPFqY4hkudY9juzIcOU2auml+wBSu1c45142wdz4x//oh0N5NrVuLBe74Auy4nVOfH
8 |
9 |
10 |
11 |
12 |
13 |
14 | MIIF1TCCA72gAwIBAgIJAKdEDzxP3jJLMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWlkbGFuZHMxEjAQBgNVBAcMCVNhZmV2aWxsZTEWMBQGA1UECgwNSUQgRmVkZXJhdGlvbjEhMB8GA1UECwwYRGVwYXJ0bWVudCBvZiBJZGVudGl0aWVzMQ8wDQYDVQQDDAZteS5pZHAwHhcNMTcwOTA5MDExNzMwWhcNMTgwOTA5MDExNzMwWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pZGxhbmRzMRIwEAYDVQQHDAlTYWZldmlsbGUxFjAUBgNVBAoMDUlEIEZlZGVyYXRpb24xITAfBgNVBAsMGERlcGFydG1lbnQgb2YgSWRlbnRpdGllczEPMA0GA1UEAwwGbXkuaWRwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5MXY8olt62YGPQQo4UIZ8Q48lMGGSA4DhEAooSA4yjXeKcmG/aDeINkcTzA/khv1iCy3gFDnJ6T6tGvNghWFDVLWrKRzg3PO0y3+6MaJAJ5CXtCaf+EV9KNFcrt8+DFwdudkWrwEak1gM8OHUfXvOqyD1a5g+vPrSm+QiI9+W4csTDB6zQKfMjnQJnUh+W8H4ZLPX2nJAeJLFhDfE1l66pjKHxVRl8QTiflpunMRNgTBknCUxs+3ilG+4QxXEgQdjzLJCc9SjgNEdqnJHd7BfUOsbIi5qjTAM3UkoumEdrLWNRCutDQoc5m79U9m+xcZso/z1qnrgPD6WkS6FiI4QFT8y/hjBEVbrXC5DZHu7ujeE8h2P2qOLjJszdW7SnvYFOshhMWD5JHkAn+Dzcch75pzvlsxxtmCw44IpCkeqjKeO2ajrQcx/fywSaCyme6gwmiF8hdXLrezj1sO0aFJhkEPM0Yb9JRkG2G3a8ITp4+3J8FCAXeLc7MJ/kxOJYVlNESSCzFhWYeSAR7doN9+eXCZ8dQhkOTN4FRT5u9ylq7tYHfAH1YNr2MKsEGrQtn/pTyb1Q4Oqb1IVpNnDRL2C9RPUt9Lfb8V3OHd1Mx81yoylXsvttX1DiPNY/KR/QCWQkvvfNTfUHTOGj8ic6G/t1/IenZ9RzNRPFxj4GNwHNcCAwEAAaNQME4wHQYDVR0OBBYEFJQbKzk3Twp4XgV0T6W0T5lZVFDZMB8GA1UdIwQYMBaAFJQbKzk3Twp4XgV0T6W0T5lZVFDZMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBALFPci1AfEp5bXuPWHbGSvAi+nVBzwHRE9VDIvsq1gVibmunwqFW7DlXi9xvPwrcQHVZtj2qeSWEc6Grw6iROgunclQIwaf6//uqb7Jkj9RhuhCs58CGcSBQ5umfffC/P2JbOte2hJjTTDNVvawAhPPBQ1bn/t8Y3eTHN/srQUtVBPX2szUqy5uRlpFYlZ2Layfcg3oLHg6Ube/HhEA2mjgCjod7zGtwuwsUOpjktQCp3aPLWXKN+ImcGAChpqAm9frgBWgZv4mxKynjOw15ur3wgSEJO7DNmEdjLqz2OiLHeX0MrDyGpcid5hPj4CgCN6vngeNXBEaoK2lJHp0I5HbdIoRBcghIVm9RzpfrDdK3/Ayy+wEAqjQRCuwBUEvrqsMnmnIjYw8HE+BE3pIS8hh9SyGCOgPJmHbYkwztkrNi6RJyeHROwAoER6J/D1saCKMK63bqLW5WLXDfftg6BNXXfvPjuQyaqlsP+HOjX8wTlKwemvUS93aDxwP8YdcFLt/uw/GXpmNKY+/wsymMYgXkTcS0FWgNUtpH/gZUIh3AMpCi1fBi71nc0uuLUBilRMrCHI5y1Rzi0qBfAkAKyUYbUO87KRvXQ3pjmpCC/IMPFqY4hkudY9juzIcOU2auml+wBSu1c45142wdz4x//oh0N5NrVuLBe74Auy4nVOfH
15 |
16 |
17 |
18 |
19 |
20 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient
21 |
22 |
23 |
24 |
25 | Jane
26 | Doe
27 | jane.doe@company.com
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/data/onelogin_idp_metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
10 |
11 |
12 | MIIDEDCCAfigAwIBAgIVALIKvj2cp9VIRuWNKjHwGiV1ITxfMA0GCSqGSIb3DQEB
13 | CwUAMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDAeFw0xNzExMDcxNTE3NDNaFw0zNzEx
14 | MDcxNTE3NDNaMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDCCASIwDQYJKoZIhvcNAQEB
15 | BQADggEPADCCAQoCggEBALmLey0ZWrMYz2O+CTTjr97UcDkaaUzbIfTjw2/0HofU
16 | czVl5b3ElzOjnB0pJ6xl8s27Qyctdq0EZrlmR9hHKUnF2U9v95rG005Cx+QQVdsg
17 | OaZjDZsmC7eLABQcLdfLP3f42dOozxCH9bBQcs+f/ndrumxdL2sIXflmer4mXfEg
18 | 57+HCRL3+s9y07fxdi2LB2ac5gVI8HJbIo8bPOeCyWLYc3UpSZUsxTZouK/wjft0
19 | TMNJ0gu5TCptiyxxZRhJcg6gm2L77d6rjbnax8fWqj/YNMlXkT7BagUxbPbEklAY
20 | YzIKnt6egw8SpOURgAJynZDl4cYxM1QynfIuWaYi5gECAwEAAaNZMFcwHQYDVR0O
21 | BBYEFJhsexKytkNruELg386zOyW1icH1MDYGA1UdEQQvMC2CCXNhbWx5LmlkcIYg
22 | aHR0cHM6Ly9zYW1seS5pZHAvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQAD
23 | ggEBAAhCAuNPhWsrd/b3MSRK+I5GGe0eDSkpQiCT0ULbqucW+BHj0by0DOy6yP98
24 | 0mfATI6eDJ/LUpT+Wenxljujy5nh0EPu6t6RG/MvWTplnr0//m+41L8tQXEZtZkM
25 | NKkrFieiUBe+rcDx7xywzGUvM0qWRdTyD7Yb0JUU8bZKnIFAEZ+mm8Fu1KfXI6kK
26 | sdeh/6gdpah9v2mermegdNfGpktWtXOukvmR4M8ZYLEyAwGQQAuqJcUnOUwuVMFU
27 | chLUXbAfJUduUkGQ3WKw/SNKyv/7Z2ayr7wlkEA7fxIIrLaJzSm928y9wB9s7Irr
28 | 78rpJG67hSRlA+CGTGZyksrk2fE=
29 |
30 |
31 |
32 |
33 |
36 |
37 | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
38 |
39 |
42 |
45 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/test/data/shibboleth_idp_metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 | example.org
14 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | MIIDEDCCAfigAwIBAgIVALIKvj2cp9VIRuWNKjHwGiV1ITxfMA0GCSqGSIb3DQEB
30 | CwUAMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDAeFw0xNzExMDcxNTE3NDNaFw0zNzEx
31 | MDcxNTE3NDNaMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDCCASIwDQYJKoZIhvcNAQEB
32 | BQADggEPADCCAQoCggEBALmLey0ZWrMYz2O+CTTjr97UcDkaaUzbIfTjw2/0HofU
33 | czVl5b3ElzOjnB0pJ6xl8s27Qyctdq0EZrlmR9hHKUnF2U9v95rG005Cx+QQVdsg
34 | OaZjDZsmC7eLABQcLdfLP3f42dOozxCH9bBQcs+f/ndrumxdL2sIXflmer4mXfEg
35 | 57+HCRL3+s9y07fxdi2LB2ac5gVI8HJbIo8bPOeCyWLYc3UpSZUsxTZouK/wjft0
36 | TMNJ0gu5TCptiyxxZRhJcg6gm2L77d6rjbnax8fWqj/YNMlXkT7BagUxbPbEklAY
37 | YzIKnt6egw8SpOURgAJynZDl4cYxM1QynfIuWaYi5gECAwEAAaNZMFcwHQYDVR0O
38 | BBYEFJhsexKytkNruELg386zOyW1icH1MDYGA1UdEQQvMC2CCXNhbWx5LmlkcIYg
39 | aHR0cHM6Ly9zYW1seS5pZHAvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQAD
40 | ggEBAAhCAuNPhWsrd/b3MSRK+I5GGe0eDSkpQiCT0ULbqucW+BHj0by0DOy6yP98
41 | 0mfATI6eDJ/LUpT+Wenxljujy5nh0EPu6t6RG/MvWTplnr0//m+41L8tQXEZtZkM
42 | NKkrFieiUBe+rcDx7xywzGUvM0qWRdTyD7Yb0JUU8bZKnIFAEZ+mm8Fu1KfXI6kK
43 | sdeh/6gdpah9v2mermegdNfGpktWtXOukvmR4M8ZYLEyAwGQQAuqJcUnOUwuVMFU
44 | chLUXbAfJUduUkGQ3WKw/SNKyv/7Z2ayr7wlkEA7fxIIrLaJzSm928y9wB9s7Irr
45 | 78rpJG67hSRlA+CGTGZyksrk2fE=
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | MIIDDzCCAfegAwIBAgIUawrhfDAK6t2xrB0CCKKiLILXdUEwDQYJKoZIhvcNAQEL
56 | BQAwFDESMBAGA1UEAwwJc2FtbHkuaWRwMB4XDTE3MTEwNzE1MTczMVoXDTM3MTEw
57 | NzE1MTczMVowFDESMBAGA1UEAwwJc2FtbHkuaWRwMIIBIjANBgkqhkiG9w0BAQEF
58 | AAOCAQ8AMIIBCgKCAQEAgx/bOpdbzlR33T9ZgVkLWAYVTJvPnS6EbOouV9iSsrul
59 | 9Sg6kxrb3NK9HumFqalDxmwZH81snt+isgIUyIX0uZDEu0eBt++hGrLH4/gvZjQW
60 | Ow5ju1+dVOIt28Qy9+ExzWS4XEblId4m8xxNew2FlKKQwThYojuGH95FkMDo736A
61 | wLJNol7FY3BgZwcGajIDFQoAyBhUrfrScBvE/eEGmPyJPzIO7NmtrlPq5NmATi4W
62 | fG5U7I+dT6t3rasPbhbKf1xsN5dNOgHEYAZmp+wqMJ9t4GNDJgqt5Sftryd/zskk
63 | 9fPjk8MFll4XVJ9NGjg1AjUwS3swQBIK2xejK1zl2QIDAQABo1kwVzAdBgNVHQ4E
64 | FgQUs0m5/0iOU8Z9Rf7JrbfYd2EUrBowNgYDVR0RBC8wLYIJc2FtbHkuaWRwhiBo
65 | dHRwczovL3NhbWx5LmlkcC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOC
66 | AQEAYMBZvg8V9468Jn1mbMJ7YuOb1A8XFB5nuewpBzjnFoDZKRsUim6DUOAt/NYZ
67 | xWxaC7l8t70LdGskxaFgdE2+L7z7TibZRj2Ibc+CRg20O615rCR3C5fUdRv6Z4C/
68 | pSu5yNPQz5NPWOI5ryFmbp9TCf+Yh8hwa49BY/pOIPSjGk5usJk9OVBSqwdJrBpp
69 | iO9wLLCB2z6ZFpK3LpF2DpGAewuJOzHaD8VfPoqqAcXnWR+Q263QOrfv+9GeFv8G
70 | odjFk9wMTYRX5iitBAank4vrE0USbovz30F+wK4VLxm/806Evh4wOPwkroxBomnc
71 | a/dmaqTz0EZ80cr5Le+54VhF/w==
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | MIIDDzCCAfegAwIBAgIUFjdO5mF6khL3fHOV3CcPpMD1juMwDQYJKoZIhvcNAQEL
82 | BQAwFDESMBAGA1UEAwwJc2FtbHkuaWRwMB4XDTE3MTEwNzE1MTczMloXDTM3MTEw
83 | NzE1MTczMlowFDESMBAGA1UEAwwJc2FtbHkuaWRwMIIBIjANBgkqhkiG9w0BAQEF
84 | AAOCAQ8AMIIBCgKCAQEA8X53oxFAwsIdsCltzcko0NgUg63NQtOxXVEoqmPpSKxX
85 | 8wvsXWa0/KLa5XOWuoxshc45yI0Tn9bxGxrzP90StABRIKSnbjs2k1sebXPbOHDM
86 | LPMyMcKq34qiIffg+KfPHiOajksB8gD1EPdu8hSTPuLsiJnCUrWtPB8s/IydEdTa
87 | o79Hk0cd8BQskB/l7PwuakPdsF6QRJj+kcFrBAkECzrET0LZahpIPKmSQpE3LQ08
88 | XxMLEoFJrhvt1Dwj4LTEbmjrJY+l+2HpJxXH9ibwVlJpTzqNAz81vC7Xad/c4oMg
89 | thnk1uhk73IqmeAgQA8FKFKCLwK7iFNJ8X/B99EfYwIDAQABo1kwVzAdBgNVHQ4E
90 | FgQU3jbqNvR4yPzb9R173XFB+E+VGCIwNgYDVR0RBC8wLYIJc2FtbHkuaWRwhiBo
91 | dHRwczovL3NhbWx5LmlkcC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOC
92 | AQEAHlcb/jlVDu8Lj4X6u/IAEiBzwtVjVLpH6X0GHWq1WU8IN4zAtPhsc4ELmtGj
93 | ZJNCZwD7eBi4Ibby6I3NCwvhafpRVB8+PCD/jGB53t/vV8VZII/Zpk/wgYTh11T+
94 | TyOuW/JCckLeHLSwYPiDHmk3z9e8K9CVI/lhHvNXoEEfGtrIiGE8T00RhBAWvflv
95 | 2YATb9dsUGKXojAKi1xZpBToxQPllryjGqN+Big6IcR0Pv/IVOxVvcgU5ZOtYF0g
96 | HuK+YEeTSVMkW24+aC6185LocIKrUPg9d/kyUTi8WK047fctGv+7IdP5mbeGMRiU
97 | pgvjU5nUCkPOUOyZZiVO0VMEIA==
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | example.org
124 |
125 |
126 |
127 |
128 |
129 |
130 | MIIDEDCCAfigAwIBAgIVALIKvj2cp9VIRuWNKjHwGiV1ITxfMA0GCSqGSIb3DQEB
131 | CwUAMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDAeFw0xNzExMDcxNTE3NDNaFw0zNzEx
132 | MDcxNTE3NDNaMBQxEjAQBgNVBAMMCXNhbWx5LmlkcDCCASIwDQYJKoZIhvcNAQEB
133 | BQADggEPADCCAQoCggEBALmLey0ZWrMYz2O+CTTjr97UcDkaaUzbIfTjw2/0HofU
134 | czVl5b3ElzOjnB0pJ6xl8s27Qyctdq0EZrlmR9hHKUnF2U9v95rG005Cx+QQVdsg
135 | OaZjDZsmC7eLABQcLdfLP3f42dOozxCH9bBQcs+f/ndrumxdL2sIXflmer4mXfEg
136 | 57+HCRL3+s9y07fxdi2LB2ac5gVI8HJbIo8bPOeCyWLYc3UpSZUsxTZouK/wjft0
137 | TMNJ0gu5TCptiyxxZRhJcg6gm2L77d6rjbnax8fWqj/YNMlXkT7BagUxbPbEklAY
138 | YzIKnt6egw8SpOURgAJynZDl4cYxM1QynfIuWaYi5gECAwEAAaNZMFcwHQYDVR0O
139 | BBYEFJhsexKytkNruELg386zOyW1icH1MDYGA1UdEQQvMC2CCXNhbWx5LmlkcIYg
140 | aHR0cHM6Ly9zYW1seS5pZHAvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQAD
141 | ggEBAAhCAuNPhWsrd/b3MSRK+I5GGe0eDSkpQiCT0ULbqucW+BHj0by0DOy6yP98
142 | 0mfATI6eDJ/LUpT+Wenxljujy5nh0EPu6t6RG/MvWTplnr0//m+41L8tQXEZtZkM
143 | NKkrFieiUBe+rcDx7xywzGUvM0qWRdTyD7Yb0JUU8bZKnIFAEZ+mm8Fu1KfXI6kK
144 | sdeh/6gdpah9v2mermegdNfGpktWtXOukvmR4M8ZYLEyAwGQQAuqJcUnOUwuVMFU
145 | chLUXbAfJUduUkGQ3WKw/SNKyv/7Z2ayr7wlkEA7fxIIrLaJzSm928y9wB9s7Irr
146 | 78rpJG67hSRlA+CGTGZyksrk2fE=
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | MIIDDzCCAfegAwIBAgIUawrhfDAK6t2xrB0CCKKiLILXdUEwDQYJKoZIhvcNAQEL
157 | BQAwFDESMBAGA1UEAwwJc2FtbHkuaWRwMB4XDTE3MTEwNzE1MTczMVoXDTM3MTEw
158 | NzE1MTczMVowFDESMBAGA1UEAwwJc2FtbHkuaWRwMIIBIjANBgkqhkiG9w0BAQEF
159 | AAOCAQ8AMIIBCgKCAQEAgx/bOpdbzlR33T9ZgVkLWAYVTJvPnS6EbOouV9iSsrul
160 | 9Sg6kxrb3NK9HumFqalDxmwZH81snt+isgIUyIX0uZDEu0eBt++hGrLH4/gvZjQW
161 | Ow5ju1+dVOIt28Qy9+ExzWS4XEblId4m8xxNew2FlKKQwThYojuGH95FkMDo736A
162 | wLJNol7FY3BgZwcGajIDFQoAyBhUrfrScBvE/eEGmPyJPzIO7NmtrlPq5NmATi4W
163 | fG5U7I+dT6t3rasPbhbKf1xsN5dNOgHEYAZmp+wqMJ9t4GNDJgqt5Sftryd/zskk
164 | 9fPjk8MFll4XVJ9NGjg1AjUwS3swQBIK2xejK1zl2QIDAQABo1kwVzAdBgNVHQ4E
165 | FgQUs0m5/0iOU8Z9Rf7JrbfYd2EUrBowNgYDVR0RBC8wLYIJc2FtbHkuaWRwhiBo
166 | dHRwczovL3NhbWx5LmlkcC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOC
167 | AQEAYMBZvg8V9468Jn1mbMJ7YuOb1A8XFB5nuewpBzjnFoDZKRsUim6DUOAt/NYZ
168 | xWxaC7l8t70LdGskxaFgdE2+L7z7TibZRj2Ibc+CRg20O615rCR3C5fUdRv6Z4C/
169 | pSu5yNPQz5NPWOI5ryFmbp9TCf+Yh8hwa49BY/pOIPSjGk5usJk9OVBSqwdJrBpp
170 | iO9wLLCB2z6ZFpK3LpF2DpGAewuJOzHaD8VfPoqqAcXnWR+Q263QOrfv+9GeFv8G
171 | odjFk9wMTYRX5iitBAank4vrE0USbovz30F+wK4VLxm/806Evh4wOPwkroxBomnc
172 | a/dmaqTz0EZ80cr5Le+54VhF/w==
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | MIIDDzCCAfegAwIBAgIUFjdO5mF6khL3fHOV3CcPpMD1juMwDQYJKoZIhvcNAQEL
183 | BQAwFDESMBAGA1UEAwwJc2FtbHkuaWRwMB4XDTE3MTEwNzE1MTczMloXDTM3MTEw
184 | NzE1MTczMlowFDESMBAGA1UEAwwJc2FtbHkuaWRwMIIBIjANBgkqhkiG9w0BAQEF
185 | AAOCAQ8AMIIBCgKCAQEA8X53oxFAwsIdsCltzcko0NgUg63NQtOxXVEoqmPpSKxX
186 | 8wvsXWa0/KLa5XOWuoxshc45yI0Tn9bxGxrzP90StABRIKSnbjs2k1sebXPbOHDM
187 | LPMyMcKq34qiIffg+KfPHiOajksB8gD1EPdu8hSTPuLsiJnCUrWtPB8s/IydEdTa
188 | o79Hk0cd8BQskB/l7PwuakPdsF6QRJj+kcFrBAkECzrET0LZahpIPKmSQpE3LQ08
189 | XxMLEoFJrhvt1Dwj4LTEbmjrJY+l+2HpJxXH9ibwVlJpTzqNAz81vC7Xad/c4oMg
190 | thnk1uhk73IqmeAgQA8FKFKCLwK7iFNJ8X/B99EfYwIDAQABo1kwVzAdBgNVHQ4E
191 | FgQU3jbqNvR4yPzb9R173XFB+E+VGCIwNgYDVR0RBC8wLYIJc2FtbHkuaWRwhiBo
192 | dHRwczovL3NhbWx5LmlkcC9pZHAvc2hpYmJvbGV0aDANBgkqhkiG9w0BAQsFAAOC
193 | AQEAHlcb/jlVDu8Lj4X6u/IAEiBzwtVjVLpH6X0GHWq1WU8IN4zAtPhsc4ELmtGj
194 | ZJNCZwD7eBi4Ibby6I3NCwvhafpRVB8+PCD/jGB53t/vV8VZII/Zpk/wgYTh11T+
195 | TyOuW/JCckLeHLSwYPiDHmk3z9e8K9CVI/lhHvNXoEEfGtrIiGE8T00RhBAWvflv
196 | 2YATb9dsUGKXojAKi1xZpBToxQPllryjGqN+Big6IcR0Pv/IVOxVvcgU5ZOtYF0g
197 | HuK+YEeTSVMkW24+aC6185LocIKrUPg9d/kyUTi8WK047fctGv+7IdP5mbeGMRiU
198 | pgvjU5nUCkPOUOyZZiVO0VMEIA==
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
--------------------------------------------------------------------------------
/test/data/simplesaml_idp_metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
11 |
12 |
13 | MIIF1TCCA72gAwIBAgIJAKib45YfneRFMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWlkbGFuZHMxEjAQBgNVBAcMCVNhZmV2aWxsZTEWMBQGA1UECgwNSUQgRmVkZXJhdGlvbjEhMB8GA1UECwwYRGVwYXJ0bWVudCBvZiBJZGVudGl0aWVzMQ8wDQYDVQQDDAZteS5pZHAwHhcNMTcwOTI5MjAyNDAxWhcNMTgwOTI5MjAyNDAxWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pZGxhbmRzMRIwEAYDVQQHDAlTYWZldmlsbGUxFjAUBgNVBAoMDUlEIEZlZGVyYXRpb24xITAfBgNVBAsMGERlcGFydG1lbnQgb2YgSWRlbnRpdGllczEPMA0GA1UEAwwGbXkuaWRwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtZ6sClsZ55ulmtcZYIf18FPd5Hwc/TF4J32fu6WY0J0R9zHbFo+2HiSHSwM8jFHVCbKbvOS0bspHSh40RI/hfdWCnGujbZTZ0z9NPCluziruerKr3lb7BVTw3YGAA1h7TsdrsFlosTYFcn4dJcXwrLXkCQyi5wXC4IcPzNrL1Wuved97oxAsLdKopyBjvYRTaxFgNh5dfxyxX8RU+LIpcNnE8ZvKUy3QeN1idN1mywYCIoNnXXVu+hDpden4WIHcDOj/upYE2RqdywE1yV4KlxUx3Wgpc3382vEFtRXDHABi/qwnxiN3H7GCB0LK3eduasaBLJek0D10ONO4kWxQDSqVjzISWXVylSHijFX1IuIJoEHr3CRiY/e5xRIgVRnMfnUQu+yv6Fjp/aN5gMJypi2QHAXNWAS7bUjAvnHTfCMz2oPZokCtmQSqfNzq3RFHQye/dcLI8f4IwZ5Te96NV7egle3owXYdxrdCgkGglwdLOkf469Z6MjniH0doGtIF/mDlvr1Fww2Gp9E9gblaaLj5pVxpTKfVnw1OPcpDwfXiK39+Pz2m0mMKhhYBg8WZN8dddigfGmlZhBgbMPrqr7tBHm+c99MktskZq23fvVtsif7ARsG26Sm3lkCb03T4zOvPmENRcM2VAD7C2I+2IseVElCyEOadku3WZULYIP8CAwEAAaNQME4wHQYDVR0OBBYEFNHR4yCVyK5oB1LGcD+qXxeYEgOmMB8GA1UdIwQYMBaAFNHR4yCVyK5oB1LGcD+qXxeYEgOmMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAFrx1ggzUb+BKpvxWU9Nypb+jJyuRPYpBbvCnNohUcm8hdEE2wwybkesX01CKIH8KMjNBRytq/1aoR2nwog4aPXHciZBM5/zX2tIzDdfk4x3rRR6IdEfsXkC919TuVM+Uj/A+VgGHbqtGxEt1+nqTy9jRCOuALuYbFt3cycgqH2WjJazUPbVlkx2KdjkOmSYi1IrxrW3HXjuDPgpQKMFfaXiS7DwKzN4U5DIfTnX476u3N5oTNHfWK/JsqtrfQ061uJGEMt4P8BaliAqHoHt289XHD03wAm/b2ajgJQoW/Hhkpz8gN785doKHjEeoEkZzFllUj7e69SmD7HWDWAFeO1OOpikgwuX93f+YYccTnUAznqN8r05t5WjqPypizhr193Qc5TlEm+FkzlfjzivltTLCv0aJDlRlrQVOrZL4Buh0E4weZFk97jKeghLO6qnwxID/jSptEeN5L69Q8QzOFWia7SFGszlvrl8Y/3CQxYCzRmtSZmqdY9a6taZZ/w433+Ktc6vOstjqRE67yq1pp+0Ee4haMB/E87hDuh3Drcq2tT+b9wLNNfhSTYIw4AGpW27xhPh65RouOi9ufGjyFs1ZqqEEmBMIJi95KhUCFokMRf8L29ijqQqpDve8EvEYkBa7KgzTq3R925D5G55orLXoG4Bfdq9FY35hmR8TbLC
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | MIIF1TCCA72gAwIBAgIJAKib45YfneRFMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWlkbGFuZHMxEjAQBgNVBAcMCVNhZmV2aWxsZTEWMBQGA1UECgwNSUQgRmVkZXJhdGlvbjEhMB8GA1UECwwYRGVwYXJ0bWVudCBvZiBJZGVudGl0aWVzMQ8wDQYDVQQDDAZteS5pZHAwHhcNMTcwOTI5MjAyNDAxWhcNMTgwOTI5MjAyNDAxWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pZGxhbmRzMRIwEAYDVQQHDAlTYWZldmlsbGUxFjAUBgNVBAoMDUlEIEZlZGVyYXRpb24xITAfBgNVBAsMGERlcGFydG1lbnQgb2YgSWRlbnRpdGllczEPMA0GA1UEAwwGbXkuaWRwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtZ6sClsZ55ulmtcZYIf18FPd5Hwc/TF4J32fu6WY0J0R9zHbFo+2HiSHSwM8jFHVCbKbvOS0bspHSh40RI/hfdWCnGujbZTZ0z9NPCluziruerKr3lb7BVTw3YGAA1h7TsdrsFlosTYFcn4dJcXwrLXkCQyi5wXC4IcPzNrL1Wuved97oxAsLdKopyBjvYRTaxFgNh5dfxyxX8RU+LIpcNnE8ZvKUy3QeN1idN1mywYCIoNnXXVu+hDpden4WIHcDOj/upYE2RqdywE1yV4KlxUx3Wgpc3382vEFtRXDHABi/qwnxiN3H7GCB0LK3eduasaBLJek0D10ONO4kWxQDSqVjzISWXVylSHijFX1IuIJoEHr3CRiY/e5xRIgVRnMfnUQu+yv6Fjp/aN5gMJypi2QHAXNWAS7bUjAvnHTfCMz2oPZokCtmQSqfNzq3RFHQye/dcLI8f4IwZ5Te96NV7egle3owXYdxrdCgkGglwdLOkf469Z6MjniH0doGtIF/mDlvr1Fww2Gp9E9gblaaLj5pVxpTKfVnw1OPcpDwfXiK39+Pz2m0mMKhhYBg8WZN8dddigfGmlZhBgbMPrqr7tBHm+c99MktskZq23fvVtsif7ARsG26Sm3lkCb03T4zOvPmENRcM2VAD7C2I+2IseVElCyEOadku3WZULYIP8CAwEAAaNQME4wHQYDVR0OBBYEFNHR4yCVyK5oB1LGcD+qXxeYEgOmMB8GA1UdIwQYMBaAFNHR4yCVyK5oB1LGcD+qXxeYEgOmMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAFrx1ggzUb+BKpvxWU9Nypb+jJyuRPYpBbvCnNohUcm8hdEE2wwybkesX01CKIH8KMjNBRytq/1aoR2nwog4aPXHciZBM5/zX2tIzDdfk4x3rRR6IdEfsXkC919TuVM+Uj/A+VgGHbqtGxEt1+nqTy9jRCOuALuYbFt3cycgqH2WjJazUPbVlkx2KdjkOmSYi1IrxrW3HXjuDPgpQKMFfaXiS7DwKzN4U5DIfTnX476u3N5oTNHfWK/JsqtrfQ061uJGEMt4P8BaliAqHoHt289XHD03wAm/b2ajgJQoW/Hhkpz8gN785doKHjEeoEkZzFllUj7e69SmD7HWDWAFeO1OOpikgwuX93f+YYccTnUAznqN8r05t5WjqPypizhr193Qc5TlEm+FkzlfjzivltTLCv0aJDlRlrQVOrZL4Buh0E4weZFk97jKeghLO6qnwxID/jSptEeN5L69Q8QzOFWia7SFGszlvrl8Y/3CQxYCzRmtSZmqdY9a6taZZ/w433+Ktc6vOstjqRE67yq1pp+0Ee4haMB/E87hDuh3Drcq2tT+b9wLNNfhSTYIw4AGpW27xhPh65RouOi9ufGjyFs1ZqqEEmBMIJi95KhUCFokMRf8L29ijqQqpDve8EvEYkBa7KgzTq3R925D5G55orLXoG4Bfdq9FY35hmR8TbLC
22 |
23 |
24 |
25 |
26 |
27 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient
28 |
29 |
30 |
31 |
32 | Jane
33 | Doe
34 | jane.doe@company.com
35 |
36 |
37 |
--------------------------------------------------------------------------------
/test/data/test.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFszCCA5ugAwIBAgIJAPHxGZeovYThMA0GCSqGSIb3DQEBCwUAMHAxCzAJBgNV
3 | BAYTAlVTMREwDwYDVQQIDAhNaWRsYW5kczESMBAGA1UEBwwJU2FmZXZpbGxlMRQw
4 | EgYDVQQKDAtTYW1seSBGYW1seTEOMAwGA1UECwwFSG93dG8xFDASBgNVBAMMC3Nh
5 | bWx5Lmhvd3RvMB4XDTE3MDkwNzA1MDQ0M1oXDTE4MDkwNzA1MDQ0M1owcDELMAkG
6 | A1UEBhMCVVMxETAPBgNVBAgMCE1pZGxhbmRzMRIwEAYDVQQHDAlTYWZldmlsbGUx
7 | FDASBgNVBAoMC1NhbWx5IEZhbWx5MQ4wDAYDVQQLDAVIb3d0bzEUMBIGA1UEAwwL
8 | c2FtbHkuaG93dG8wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDrPJpY
9 | myiDDXFIkH4kmv8mT8T6qkrcu3d7hlu/zJeQcfPQyH2ONqj0AGnlDamTvQW4w72Q
10 | aJ1Ddo/0iAfB2CVMnJdePexZgpaPToGMBVUVEzm9Rpk6heCmhb20X8NVUI+LMzeL
11 | udDl3VbaQS1Q7xxKC3BUDlCpXxQfq8cFUYOpFGCBwwVr+ceGdTXs4cjNyCX8iBZW
12 | dR4Yhm25oAGgDU+JQWt+WoQ/cINifvEhwmA4wTTlyMYhlFQxVYIaT8WnWtrdNEUz
13 | YfLTjzpFZHEq063EYe6Dk87oIeASNQx/TAh2OAykRiR7MAgvbCLB65maGz0UCbNz
14 | LfLcjloFr6oUZ+PaLdgCj2KWQnoGDIqJ+CGZ1JlRvieIV/nQHCupAjGX60omVwxq
15 | a6Wd1PYynmMF3ybxJKrEHvtYwM8uVU+WQkCJiIO5yKAdAmiKM8kOM0NWTc/8fAWK
16 | u+SdTGSi7XPyVMep/ORftmufC/QltTUJ+Vigg97UM2E3Q3IqBgF165fLtzYAkLLB
17 | 6LuBB6WBlOz03mmruvWqsUjzKl2Oslh693EADXBjbYY+17TlqM3JmmPEu/b+DKSG
18 | DZF2I8aibFnGJC1yaUgW09ACDHBFR/5hD5w71udu4rjf/7IiKzjxKxMfSWO0dYfy
19 | va2nJabdTlv5xCQ4k4fXGmgK6KyflqWjlbuKqQIDAQABo1AwTjAdBgNVHQ4EFgQU
20 | UD3OWT8XbhyOm1aHUhYZKz+qgjMwHwYDVR0jBBgwFoAUUD3OWT8XbhyOm1aHUhYZ
21 | Kz+qgjMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAkdhG7Zv4NSjq
22 | N/aU3BjoWHEHYXI/h+pP5kSeI9RmIIBLMvOqZvJ7Eh22sGX4kEPN1oPJtJxr0qbV
23 | O6b/AcvPdHc5l5+ahqnQaBd4Vp5u7os07PNPW3ssYnD+6Xton6xQWH4uXqWxloaZ
24 | KVDI76/BsdP4rO8smyUFcxm4AnEZZZYwvAbb3us8Tz3iyhvt/W2wOOweP55LF7Ob
25 | 01/owCzmCPlPJHWfNEba4RhbMKm7kRJXuNiuzUibOcaHdK88LkgxygyV8uc6F7Za
26 | TLYZmIsZIBtUNL5k8T/5GCobofJQoe3daiJ2XFIfGqqwCobZa7CK0DcRdDfbj3bY
27 | Fa7pvtg/RbjVHp6/LfmkZ9hIOfj9KitnpzFWOD5XFU50IHzdnSqasDrX22r/8s/K
28 | Tr90MZrsR99ed5k1Y6eV/5o4abz2NaPL3T+q48lf+B1b5oICjR/AIo1NGfan/OIZ
29 | hIGlLvdNOjOWcyD4Es7d7N0BsmGoRMTH/oswh+e5n6KBciKgmGFJ2jxAB7RieIbT
30 | zmIvjs9FpyoEsTDM1QGkjwk/3bozvGC+/Sd8QQHvvyz3du4UQEvqXMATExzo8o44
31 | Mg5wVknh/y06P4+xPLn5wxPumpQZcuoTH4I5yMLC8nh7ns4zVLzxMze5FGYrdpaT
32 | 4WBwLGcoQsXB27G5lTXNqELSN/iVF/A=
33 | -----END CERTIFICATE-----
34 |
--------------------------------------------------------------------------------
/test/data/test.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | 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
51 | qpuhVP1fFzr1d26vUtG9IBvI2PKrFB7YMg==
52 | -----END PRIVATE KEY-----
53 |
--------------------------------------------------------------------------------
/test/data/testshib_metadata.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
25 |
26 | testshib.org
27 |
28 | TestShib Test IdP
29 | TestShib IdP. Use this as a source of attributes
30 | for your test SP.
31 | https://www.testshib.org/testshibtwo.jpg
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEB
43 | CwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0
44 | WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIB
45 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryh
46 | m3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEm
47 | lGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBn
48 | xoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB
49 | 3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTH
50 | ot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQID
51 | AQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQw
52 | EoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzR
53 | OZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QP
54 | dRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS
55 | 80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOT
56 | MVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhO
57 | RkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqX
58 | MLRKhDgdmA==
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
76 |
77 | urn:mace:shibboleth:1.0:nameIdentifier
78 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient
79 |
80 |
82 |
84 |
86 |
88 |
89 |
90 |
91 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEB
100 | CwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0
101 | WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIB
102 | IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryh
103 | m3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEm
104 | lGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBn
105 | xoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB
106 | 3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTH
107 | ot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQID
108 | AQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQw
109 | EoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzR
110 | OZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QP
111 | dRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS
112 | 80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOT
113 | MVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhO
114 | RkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqX
115 | MLRKhDgdmA==
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
129 |
131 |
132 | urn:mace:shibboleth:1.0:nameIdentifier
133 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient
134 |
135 |
136 |
137 |
138 | TestShib Two Identity Provider
139 | TestShib Two
140 | http://www.testshib.org/testshib-two/
141 |
142 |
143 | Nate
144 | Klingenstein
145 | ndk@internet2.edu
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | TestShib Test SP
185 | TestShib SP. Log into this to test your machine.
186 | Once logged in check that all attributes that you expected have been
187 | released.
188 | https://www.testshib.org/testshibtwo.jpg
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | MIIEPjCCAyagAwIBAgIBADANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJVUzEV
197 | MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMSIwIAYD
198 | VQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQDEw9zcC50ZXN0
199 | c2hpYi5vcmcwHhcNMDYwODMwMjEyNDM5WhcNMTYwODI3MjEyNDM5WjB3MQswCQYD
200 | VQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1
201 | cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQD
202 | Ew9zcC50ZXN0c2hpYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
203 | AQDJyR6ZP6MXkQ9z6RRziT0AuCabDd3x1m7nLO9ZRPbr0v1LsU+nnC363jO8nGEq
204 | sqkgiZ/bSsO5lvjEt4ehff57ERio2Qk9cYw8XCgmYccVXKH9M+QVO1MQwErNobWb
205 | AjiVkuhWcwLWQwTDBowfKXI87SA7KR7sFUymNx5z1aoRvk3GM++tiPY6u4shy8c7
206 | vpWbVfisfTfvef/y+galxjPUQYHmegu7vCbjYP3On0V7/Ivzr+r2aPhp8egxt00Q
207 | XpilNai12LBYV3Nv/lMsUzBeB7+CdXRVjZOHGuQ8mGqEbsj8MBXvcxIKbcpeK5Zi
208 | JCVXPfarzuriM1G5y5QkKW+LAgMBAAGjgdQwgdEwHQYDVR0OBBYEFKB6wPDxwYrY
209 | StNjU5P4b4AjBVQVMIGhBgNVHSMEgZkwgZaAFKB6wPDxwYrYStNjU5P4b4AjBVQV
210 | oXukeTB3MQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYD
211 | VQQHEwpQaXR0c2J1cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3Zp
212 | ZGVyMRgwFgYDVQQDEw9zcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
213 | BgkqhkiG9w0BAQUFAAOCAQEAc06Kgt7ZP6g2TIZgMbFxg6vKwvDL0+2dzF11Onpl
214 | 5sbtkPaNIcj24lQ4vajCrrGKdzHXo9m54BzrdRJ7xDYtw0dbu37l1IZVmiZr12eE
215 | Iay/5YMU+aWP1z70h867ZQ7/7Y4HW345rdiS6EW663oH732wSYNt9kr7/0Uer3KD
216 | 9CuPuOidBacospDaFyfsaJruE99Kd6Eu/w5KLAGG+m0iqENCziDGzVA47TngKz2v
217 | PVA+aokoOyoz3b53qeti77ijatSEoKjxheBWpO+eoJeGq/e49Um3M2ogIX/JAlMa
218 | Inh+vYSYngQB2sx9LGkR9KHaMKNIGCDehk93Xla4pWJx1w==
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
237 |
239 |
241 |
243 |
244 |
245 |
246 | urn:oasis:names:tc:SAML:2.0:nameid-format:transient
247 | urn:mace:shibboleth:1.0:nameIdentifier
248 |
249 |
255 |
256 |
259 |
262 |
265 |
268 |
271 |
274 |
275 |
276 |
277 |
280 |
283 |
284 |
285 |
286 |
287 |
288 | TestShib Two Service Provider
289 | TestShib Two
290 | http://www.testshib.org/testshib-two/
291 |
292 |
293 | Nate
294 | Klingenstein
295 | ndk@internet2.edu
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
--------------------------------------------------------------------------------
/test/samly_idp_data_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SamlyIdpDataTest do
2 | use ExUnit.Case
3 | require Samly.Esaml
4 | alias Samly.{Esaml, IdpData, SpData}
5 |
6 | @sp_config1 %{
7 | id: "sp1",
8 | entity_id: "urn:test:sp1",
9 | certfile: "test/data/test.crt",
10 | keyfile: "test/data/test.pem"
11 | }
12 |
13 | @sp_config2 %{
14 | id: "sp2",
15 | certfile: "test/data/test.crt",
16 | keyfile: "test/data/test.pem"
17 | }
18 |
19 | @sp_config3 %{
20 | id: "sp3",
21 | keyfile: "test/data/test.pem"
22 | }
23 |
24 | @sp_config4 %{
25 | id: "sp4",
26 | certfile: "test/data/test.crt"
27 | }
28 |
29 | @sp_config5 %{
30 | id: "sp5"
31 | }
32 |
33 | @idp_config1 %{
34 | id: "idp1",
35 | sp_id: "sp1",
36 | base_url: "http://samly.howto:4003/sso",
37 | metadata_file: "test/data/idp_metadata.xml"
38 | }
39 |
40 | @idp_config2 %{
41 | id: "idp2",
42 | sp_id: "sp2",
43 | base_url: "http://samly.howto:4003/sso",
44 | metadata_file: "test/data/idp_metadata.xml"
45 | }
46 |
47 | setup context do
48 | sp_data1 = SpData.load_provider(@sp_config1)
49 | sp_data2 = SpData.load_provider(@sp_config2)
50 | sp_data3 = SpData.load_provider(@sp_config3)
51 | sp_data4 = SpData.load_provider(@sp_config4)
52 | sp_data5 = SpData.load_provider(@sp_config5)
53 |
54 | [
55 | sps: %{
56 | sp_data1.id => sp_data1,
57 | sp_data2.id => sp_data2,
58 | sp_data3.id => sp_data3,
59 | sp_data4.id => sp_data4,
60 | sp_data5.id => sp_data5
61 | }
62 | ]
63 | |> Enum.into(context)
64 | end
65 |
66 | test "valid-idp-config-1", %{sps: sps} do
67 | %IdpData{} = idp_data = IdpData.load_provider(@idp_config1, sps)
68 | assert idp_data.valid?
69 | end
70 |
71 | # verify defaults
72 | test "valid-idp-config-2", %{sps: sps} do
73 | %IdpData{} = idp_data = IdpData.load_provider(@idp_config1, sps)
74 | refute idp_data.use_redirect_for_req
75 | assert idp_data.sign_requests
76 | assert idp_data.sign_metadata
77 | assert idp_data.signed_assertion_in_resp
78 | assert idp_data.signed_envelopes_in_resp
79 | end
80 |
81 | test "valid-idp-config-3", %{sps: sps} do
82 | idp_config =
83 | Map.merge(@idp_config1, %{
84 | use_redirect_for_req: false,
85 | sign_requests: true,
86 | sign_metadata: true,
87 | signed_assertion_in_resp: true,
88 | signed_envelopes_in_resp: true
89 | })
90 |
91 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
92 | refute idp_data.use_redirect_for_req
93 | assert idp_data.sign_requests
94 | assert idp_data.sign_metadata
95 | assert idp_data.signed_assertion_in_resp
96 | assert idp_data.signed_envelopes_in_resp
97 | end
98 |
99 | test "valid-idp-config-4", %{sps: sps} do
100 | idp_config =
101 | Map.merge(@idp_config1, %{
102 | use_redirect_for_req: true,
103 | sign_requests: false,
104 | sign_metadata: false,
105 | signed_assertion_in_resp: false,
106 | signed_envelopes_in_resp: false
107 | })
108 |
109 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
110 | assert idp_data.use_redirect_for_req
111 | refute idp_data.sign_requests
112 | refute idp_data.sign_metadata
113 | refute idp_data.signed_assertion_in_resp
114 | refute idp_data.signed_envelopes_in_resp
115 | end
116 |
117 | test "valid-idp-config-5", %{sps: sps} do
118 | idp_config = %{@idp_config1 | base_url: nil}
119 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
120 | assert idp_data.valid?
121 | assert idp_data.base_url == nil
122 | end
123 |
124 | test "valid-idp-config-6", %{sps: sps} do
125 | idp_config = Map.put(@idp_config1, :pre_session_create_pipeline, MyPipeline)
126 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
127 | assert idp_data.valid?
128 | assert idp_data.pre_session_create_pipeline == MyPipeline
129 | end
130 |
131 | test "valid-idp-config-7", %{sps: sps} do
132 | idp_config = %{@idp_config1 | metadata_file: "test/data/azure_fed_metadata.xml"}
133 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
134 | assert idp_data.valid?
135 | end
136 |
137 | test "valid-idp-config-8", %{sps: sps} do
138 | idp_config = %{@idp_config1 | metadata_file: "test/data/onelogin_idp_metadata.xml"}
139 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
140 | assert idp_data.valid?
141 | end
142 |
143 | test "valid-idp-config-9", %{sps: sps} do
144 | idp_config = %{@idp_config1 | metadata_file: "test/data/shibboleth_idp_metadata.xml"}
145 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
146 | assert idp_data.valid?
147 | end
148 |
149 | test "valid-idp-config-10", %{sps: sps} do
150 | idp_config = %{@idp_config1 | metadata_file: "test/data/simplesaml_idp_metadata.xml"}
151 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
152 | assert idp_data.valid?
153 | end
154 |
155 | test "valid-idp-config-11", %{sps: sps} do
156 | idp_config = %{@idp_config1 | metadata_file: "test/data/testshib_metadata.xml"}
157 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
158 | assert idp_data.valid?
159 | end
160 |
161 | test "url-test-1", %{sps: sps} do
162 | idp_config = %{@idp_config1 | metadata_file: "test/data/shibboleth_idp_metadata.xml"}
163 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
164 | assert idp_data.valid?
165 |
166 | Esaml.esaml_idp_metadata(
167 | login_location: sso_url,
168 | logout_location: slo_url
169 | ) = idp_data.esaml_idp_rec
170 |
171 | assert sso_url |> List.to_string() |> String.ends_with?("/SAML2/POST/SSO")
172 | assert slo_url |> List.to_string() |> String.ends_with?("/SAML2/POST/SLO")
173 | end
174 |
175 | test "url-test-2", %{sps: sps} do
176 | idp_config = %{@idp_config1 | metadata_file: "test/data/shibboleth_idp_metadata.xml"}
177 | idp_config = Map.put(idp_config, :use_redirect_for_req, true)
178 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
179 | assert idp_data.valid?
180 |
181 | Esaml.esaml_idp_metadata(
182 | login_location: sso_url,
183 | logout_location: slo_url
184 | ) = idp_data.esaml_idp_rec
185 |
186 | assert sso_url |> List.to_string() |> String.ends_with?("/SAML2/Redirect/SSO")
187 | assert slo_url |> List.to_string() |> String.ends_with?("/SAML2/Redirect/SLO")
188 | end
189 |
190 | test "sp entity_id test-1", %{sps: sps} do
191 | %IdpData{} = idp_data = IdpData.load_provider(@idp_config2, sps)
192 | assert idp_data.valid?
193 | Esaml.esaml_sp(entity_id: entity_id) = idp_data.esaml_sp_rec
194 | assert entity_id == :undefined
195 | end
196 |
197 | @tag :skip
198 | test "invalid-idp-config-1", %{sps: sps} do
199 | idp_config = %{@idp_config1 | id: ""}
200 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
201 | refute idp_data.valid?
202 | end
203 |
204 | test "invalid-idp-config-2", %{sps: sps} do
205 | idp_config = %{@idp_config1 | sp_id: "unknown-sp"}
206 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
207 | refute idp_data.valid?
208 | end
209 |
210 | test "valid-idp-config-signing-turned-off", %{sps: sps} do
211 | idp_config =
212 | Map.merge(@idp_config1, %{
213 | sp_id: "sp5",
214 | use_redirect_for_req: true,
215 | sign_requests: false,
216 | sign_metadata: false
217 | })
218 |
219 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
220 | assert idp_data.valid?
221 | end
222 |
223 | test "invalid-idp-config-signing-on-cert-missing", %{sps: sps} do
224 | idp_config =
225 | Map.merge(@idp_config1, %{
226 | sp_id: "sp3",
227 | use_redirect_for_req: true,
228 | sign_requests: true,
229 | sign_metadata: false
230 | })
231 |
232 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
233 | refute idp_data.valid?
234 | end
235 |
236 | test "invalid-idp-config-signing-on-key-missing", %{sps: sps} do
237 | idp_config =
238 | Map.merge(@idp_config1, %{
239 | sp_id: "sp4",
240 | use_redirect_for_req: true,
241 | sign_requests: true,
242 | sign_metadata: false
243 | })
244 |
245 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
246 | refute idp_data.valid?
247 | end
248 |
249 | test "nameid-format-in-metadata-but-not-config-should-use-metadata", %{sps: sps} do
250 | %IdpData{} = idp_data = IdpData.load_provider(@idp_config1, sps)
251 | assert idp_data.nameid_format == 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
252 | end
253 |
254 | test "nameid-format-in-config-but-not-metadata-should-use-config", %{sps: sps} do
255 | idp_config =
256 | Map.merge(@idp_config1, %{
257 | metadata_file: "test/data/shibboleth_idp_metadata.xml",
258 | nameid_format: :email
259 | })
260 |
261 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
262 | assert idp_data.nameid_format == 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
263 | end
264 |
265 | test "nameid-format-in-metadata-and-config-should-use-config", %{sps: sps} do
266 | idp_config =
267 | Map.merge(@idp_config1, %{
268 | nameid_format: :persistent
269 | })
270 |
271 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
272 | assert idp_data.nameid_format == 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
273 | end
274 |
275 | test "nameid-format-in-neither-metadata-nor-config-should-be-unknown", %{sps: sps} do
276 | idp_config =
277 | Map.merge(@idp_config1, %{
278 | metadata_file: "test/data/shibboleth_idp_metadata.xml"
279 | })
280 |
281 | %IdpData{} = idp_data = IdpData.load_provider(idp_config, sps)
282 | assert idp_data.nameid_format == :unknown
283 | end
284 | end
285 |
--------------------------------------------------------------------------------
/test/samly_sp_data_test.exs:
--------------------------------------------------------------------------------
1 | defmodule SamlySpDataTest do
2 | use ExUnit.Case
3 | alias Samly.SpData
4 |
5 | @sp_config1 %{
6 | id: "sp1",
7 | entity_id: "urn:test:sp1",
8 | certfile: "test/data/test.crt",
9 | keyfile: "test/data/test.pem"
10 | }
11 |
12 | test "valid-sp-config-1" do
13 | %SpData{} = sp_data = SpData.load_provider(@sp_config1)
14 | assert sp_data.valid?
15 | end
16 |
17 | test "cert-unspecified-sp-config" do
18 | sp_config = %{@sp_config1 | certfile: ""}
19 | %SpData{cert: :undefined} = sp_data = SpData.load_provider(sp_config)
20 | assert sp_data.valid?
21 | end
22 |
23 | test "key-unspecified-sp-config" do
24 | sp_config = %{@sp_config1 | keyfile: ""}
25 | %SpData{key: :undefined} = sp_data = SpData.load_provider(sp_config)
26 | assert sp_data.valid?
27 | end
28 |
29 | test "invalid-sp-config-1" do
30 | sp_config = %{@sp_config1 | id: ""}
31 | %SpData{} = sp_data = SpData.load_provider(sp_config)
32 | refute sp_data.valid?
33 | end
34 |
35 | test "invalid-sp-config-2" do
36 | sp_config = %{@sp_config1 | certfile: "non-existent.crt"}
37 | %SpData{} = sp_data = SpData.load_provider(sp_config)
38 | refute sp_data.valid?
39 | end
40 |
41 | test "invalid-sp-config-3" do
42 | sp_config = %{@sp_config1 | keyfile: "non-existent.pem"}
43 | %SpData{} = sp_data = SpData.load_provider(sp_config)
44 | refute sp_data.valid?
45 | end
46 |
47 | test "invalid-sp-config-4" do
48 | sp_config = %{@sp_config1 | certfile: "test/data/test.pem"}
49 | %SpData{} = sp_data = SpData.load_provider(sp_config)
50 | refute sp_data.valid?
51 | end
52 |
53 | @tag :skip
54 | test "invalid-sp-config-5" do
55 | sp_config = %{@sp_config1 | keyfile: "test/data/test.crt"}
56 | %SpData{} = sp_data = SpData.load_provider(sp_config)
57 | refute sp_data.valid?
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/test/samly_state_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Samly.StateTest do
2 | use ExUnit.Case, async: true
3 | use Plug.Test
4 |
5 | setup do
6 | opts =
7 | Plug.Session.init(
8 | store: :cookie,
9 | key: "_samly_state_test_session",
10 | encryption_salt: "salty enc",
11 | signing_salt: "salty signing",
12 | key_length: 64
13 | )
14 |
15 | Samly.State.init(Samly.State.Session)
16 |
17 | conn =
18 | conn(:get, "/")
19 | |> Plug.Session.call(opts)
20 | |> fetch_session()
21 |
22 | [conn: conn]
23 | end
24 |
25 | test "put/get assertion", %{conn: conn} do
26 | assertion = %Samly.Assertion{}
27 | assertion_key = {"idp1", "name1"}
28 | conn = Samly.State.put_assertion(conn, assertion_key, assertion)
29 | assert assertion == Samly.State.get_assertion(conn, assertion_key)
30 | end
31 |
32 | test "get failure for unknown assertion key", %{conn: conn} do
33 | assertion = %Samly.Assertion{}
34 | assertion_key = {"idp1", "name1"}
35 | conn = Samly.State.put_assertion(conn, assertion_key, assertion)
36 | assert nil == Samly.State.get_assertion(conn, {"idp1", "name2"})
37 | end
38 |
39 | test "delete assertion", %{conn: conn} do
40 | assertion = %Samly.Assertion{}
41 | assertion_key = {"idp1", "name1"}
42 | conn = Samly.State.put_assertion(conn, assertion_key, assertion)
43 | assert assertion == Samly.State.get_assertion(conn, assertion_key)
44 | conn = Samly.State.delete_assertion(conn, assertion_key)
45 | assert nil == Samly.State.get_assertion(conn, assertion_key)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------