{"id":13507440,"url":"https://github.com/handnot2/samly","last_synced_at":"2026-01-18T13:44:00.127Z","repository":{"id":39916557,"uuid":"101718433","full_name":"handnot2/samly","owner":"handnot2","description":"Elixir Plug library to enable SAML 2.0 SP SSO in Phoenix/Plug applications.","archived":false,"fork":false,"pushed_at":"2024-02-26T19:12:30.000Z","size":142,"stargazers_count":132,"open_issues_count":16,"forks_count":96,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-03-30T08:32:17.210Z","etag":null,"topics":["plug-pipeline","saml-assertion","saml2-sp-sso"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/handnot2.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-08-29T04:19:52.000Z","updated_at":"2025-03-12T19:41:09.000Z","dependencies_parsed_at":"2024-06-21T16:47:13.332Z","dependency_job_id":"707a63d3-e8e8-4acd-9244-7b1b68461079","html_url":"https://github.com/handnot2/samly","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/handnot2/samly","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handnot2%2Fsamly","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handnot2%2Fsamly/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handnot2%2Fsamly/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handnot2%2Fsamly/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/handnot2","download_url":"https://codeload.github.com/handnot2/samly/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/handnot2%2Fsamly/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28536773,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T13:04:05.990Z","status":"ssl_error","status_checked_at":"2026-01-18T13:01:44.092Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["plug-pipeline","saml-assertion","saml2-sp-sso"],"created_at":"2024-08-01T02:00:33.629Z","updated_at":"2026-01-18T13:43:56.996Z","avatar_url":"https://github.com/handnot2.png","language":"Elixir","funding_links":[],"categories":["Authentication"],"sub_categories":[],"readme":"# Samly\n\nA SAML 2.0 Service Provider Single-Sign-On Authentication library. This Plug library can be used to SAML enable a Plug/Phoenix application.\n\nThis has been used in the wild with the following Identity Providers:\n\n+ Okta\n+ Ping Identity\n+ OneLogin\n+ ADFS\n+ Nexus GO\n+ Shibboleth\n+ SimpleSAMLphp\n\nPlease send a note by DM if you have successfully used `Samly` with other Identity Providers.\n\n[![Inline docs](http://inch-ci.org/github/handnot2/samly.svg)](http://inch-ci.org/github/handnot2/samly)\n\nThis library uses Erlang [`esaml`](https://github.com/handnot2/esaml) to provide\nplug enabled routes.\n\n## Setup\n\n```elixir\n# mix.exs\n\n# v1.0.0 uses esaml v4.2 which in turn relies on cowboy 2.x\n# If you need to work with cowboy 1.x, you need the following override:\n# {:esaml, \"~\u003e 3.7\", override: true}\n\ndefp deps() do\n  [\n    # ...\n    {:samly, \"~\u003e 1.0.0\"},\n  ]\nend\n```\n\n## Supervision Tree\n\nAdd `Samly.Provider` to your application supervision tree.\n\n```elixir\n# application.ex\n\nchildren = [\n  # ...\n  {Samly.Provider, []},\n]\n```\n\n## Router Change\n\nMake the following change in your application router.\n\n```elixir\n# router.ex\n\n# Add the following scope ahead of other routes\n# Keep this as a top-level scope and **do not** add\n# any plugs or pipelines explicitly to this scope.\nscope \"/sso\" do\n  forward \"/\", Samly.Router\nend\n```\n\n## Certificate and Key for Samly\n\n`Samly` needs a private key and a corresponding certificate. These are used to\nsign the SAML requests when communicating with the Identity Provider. This certificate\nshould be made available to `Samly` via config settings. It should also be made\navailable to the Identity Provider so it can verify the SAML signed requests.\n\nYou can create a self-signed certificate for this purpose. You can use `phx.gen.cert`\nmix task that is available as part of Phoenix 1.4 or use `openssl` directly to generate\nthe key and corresponding certificate.\n(Check out [`samly_howto`](https://github.com/handnot2/samly_howto) `README.md` for this.)\n\n## Identity Provider Metadata\n\n`Samly` expects information about the Identity Provider including information about\nits SAML endpoints in an XML file. Most Identity Providers have some way of\nexporting the IdP metadata in XML form. Some may provide a web UI to export/save\nthe XML locally. Others may provide a URL that can be used to fetch the metadata.\n\nFor example, `SimpleSAMLPhp` IdP provides a URL for the metadata. You can fetch\nit using `wget`.\n\n```\nwget --no-check-certificate -O idp1_metadata.xml https://idp1.samly:9091/simplesaml/saml2/idp/metadata.php\n```\n\nIf you are using the `SimpleSAMLPhp` administrative Web UI, login with you\nadmin credentials (`https://idp1.samly:9091/simplesaml`). Go to the `Federation`\ntab. At the top there will be a section titled \"SAML 2.0 IdP Metadata\". Click\non the `Show metadata` link. Copy the metadata XML from this page and save it\nin a local file (`idp1_metadata.xml` for example).\n\nMake sure to save this XML file and provide the path to the saved file in\n`Samly` configuration.\n\n## Identity Provider ID in Samly\n\n`Samly` has the ability to support multiple Identity Providers. All IdPs that\n`Samly` needs to talk to must have an identifier (idp_id). This IdP id will be\nused in the service provider URLs. This is how `Samly` figures out which SAML\nrequest corresponds to what IdP so that it can perform relevant validation checks\nand process the requests/responses.\n\nThere are two options when it comes to how the idp_id is represented in the\nService Provider SAML URLs.\n\n#### URL Path Segment\n\nIn this model, the idp_id is present as a URL path segment. Here is an\nexample URL: `https://do-good.org/sso/auth/signin/affiliates`. The idp_id\nin this URL is \"affiliates\". If you have more than one IdP, only this last\npart changes. The URLs for this model are:\n\n| Description | URL |\n|:----|:----|\n| Sign-in button/link in Web UI | `/sso/auth/signin/affiliates` |\n| Sign-out button/link in Web UI | `/sso/auth/signout/affiliates` |\n| SP Metadata URL | `https://do-good.org/sso/sp/metadata/affiliates` |\n| SAML Assertion Consumer Service | `https://do-good.org/sso/sp/consume/affiliates` |\n| SAML SingleLogout Service | `https://do-good.org/sso/sp/logout/affiliates` |\n\nThe path segment model is the default one in `Samly`. If there is only one Identity Provider, use this mode.\n\n\u003e These URL routes are automatically created based on the configuration information and\n\u003e the above mentioned router scope definition.\n\u003e\n\u003e Use the Sign-in and Sign-out URLs shown above in your application's Web UI buttons/links.\n\u003e When the end-user clicks on these buttons/links, the HTTP `GET` request is handled by `Samly`\n\u003e which internally does a `POST` that in turn sends the appropriate SAML request to the IdP.\n\n#### Subdomain in Host Name\n\nIn 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`\nin this model look different.\n\n| Description | URL |\n|:----|:----|\n| Sign-in button/link in Web UI | `/sso/auth/signin` |\n| Sign-out button/link in Web UI | `/sso/auth/signout` |\n| SP Metadata URL | `https://ngo.do-good.org/sso/sp/metadata` |\n| SAML Assertion Consumer Service | `https://ngo.do-good.org/sso/sp/consume` |\n| SAML SingleLogout Service | `https://ngo.do-good.org/sso/sp/logout` |\n\n\u003e Take a look at [`samly_howto`](https://github.com/handnot2/samly_howto) - a reference/demo\n\u003e application on how to use this library.\n\u003e\n\u003e Make sure to use HTTPS URLs in production deployments.\n\n#### Target URL for Sign-In and Sign-Out Actions\n\nThe sign-in and sign-out URLs (HTTP GET) mentioned above optionally take a `target_url`\nquery parameter. `Samly` will redirect the browser to these URLs upon successfuly\ncompleting the sign-in/sign-out operations initiated from your application.\n\n\u003e This `target_url` query parameter value must be `x-www-form-urlencoded`.\n\n## Samly Configuration\n\n```elixir\n# config/dev.exs\n\nconfig :samly, Samly.Provider,\n  idp_id_from: :path_segment,\n  service_providers: [\n    %{\n      id: \"do-good-affiliates-sp\",\n      entity_id: \"urn:do-good.org:affiliates-app\",\n      certfile: \"path/to/samly/certfile.pem\",\n      keyfile: \"path/to/samly/keyfile.pem\",\n      #contact_name: \"Affiliates Admin\",\n      #contact_email: \"affiliates-admin@do-good.org\",\n      #org_name: \"Do Good\",\n      #org_displayname: \"Goodly, No evil!\",\n      #org_url: \"https://do-good.org\"\n    }\n  ],\n  identity_providers: [\n    %{\n      id: \"affiliates\",\n      sp_id: \"do-good-affiliates-sp\",\n      base_url: \"https://do-good.org/sso\",\n      metadata_file: \"idp1_metadata.xml\",\n      #pre_session_create_pipeline: MySamlyPipeline,\n      #use_redirect_for_req: false,\n      #sign_requests: true,\n      #sign_metadata: true,\n      #signed_assertion_in_resp: true,\n      #signed_envelopes_in_resp: true,\n      #allow_idp_initiated_flow: false,\n      #allowed_target_urls: [\"https://do-good.org\"],\n      #nameid_format: :transient\n    }\n  ]\n```\n\n| Parameters | Description |\n|:------------|:-----------|\n| `idp_id_from` | _(optional)_`:path_segment` or `:subdomain`. Default is `:path_segment`. |\n| **Service Provider Parameters** | |\n| `id` | _(mandatory)_ |\n| `identity_id` | _(optional)_ If omitted, the metadata URL will be used |\n| `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`|\n| `keyfile` | _(optional)_ Similar to `certfile` |\n| `contact_name` | _(optional)_ Technical contact name for the Service Provider |\n| `contact_email` | _(optional)_ Technical contact email address |\n| `org_name` | _(optional)_ SAML Service Provider (your app) Organization name |\n| `org_displayname` | _(optional)_ SAML SP Organization displayname |\n| `org_url` | _(optional)_ Service Provider Organization web site URL |\n| **Identity Provider Parameters** | |\n| `id` | _(mandatory)_ This will be the idp_id in the URLs |\n| `sp_id` | _(mandatory)_ The service provider definition to be used with this Identity Provider definition |\n| `base_url` | _(optional)_ If missing `Samly` will use the current URL to derive this. It is better to define this in production deployment. |\n| `metadata_file` | _(mandatory)_ Path to the IdP metadata XML file obtained from the Identity Provider. |\n| `pre_session_create_pipeline` | _(optional)_ Check the customization section. |\n| `use_redirect_for_req` | _(optional)_ Default is `false`. When this is `false`, `Samly` will POST to the IdP SAML endpoints. |\n| `sign_requests`, `sign_metadata` | _(optional)_ Default is `true`. |\n| `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. |\n| `allow_idp_initiated_flow` | _(optional)_ Default is `false`. IDP initiated SSO is allowed only when this is set to `true`. |\n| `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. |\n| `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. |\n\n#### Authenticated SAML Assertion State Store\n\n`Samly` internally maintains the authenticated SAML assertions (from `LoginResponse` SAML requests).\nThere are two built-in state store options available - one based on ETS and the other on Plug Sessions.\nThe ETS store can be setup using the following configuration:\n\n```elixir\nconfig :samly, Samly.State,\n  store: Samly.State.ETS,\n  opts: [table: :my_ets_table]\n```\n\nThis state configuration is optional. If omitted, `Samly` uses `Samly.State.ETS` provider by default.\n\n| Options | Description |\n|:------------|:-----------|\n| `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`. |\n\n\u003e Use `Samly.State.Session` provider in a clustered deployment. This provider uses\n\u003e the Plug Sessions to keep the authenticated SAML assertions.\n\nThis session based provider can be enabled using the following:\n\n```elixir\nconfig :samly, Samly.State,\n  store: Samly.State.Session,\n  opts: [key: :my_assertion_key]\n```\n\n| Options | Description |\n|:------------|:-----------|\n| `opts` | _(optional)_ The `:key` is the name of the session key where assertion is stored. Default is `:samly_assertion`. |\n\n## SAML Assertion\n\nOnce authentication is completed successfully, IdP sends a \"consume\" SAML\nrequest to `Samly`. `Samly` in-turn performs its own checks (including checking\nthe integrity of the \"consume\" request). At this point, the SAML assertion\nwith the authenticated user subject and attributes is available.\n\nThe subject in the SAML assertion is tracked by `Samly` so that subsequent\nlogout/signout request, either service provider initiated or IdP initiated\nwould result in proper removal of the corresponding SAML assertion.\n\nUse the `Samly.get_active_assertion` function to get the SAML assertion\nfor the currently authenticated user. This function will return `nil` if\nthe user is not authenticated.\n\n\u003e Avoid using the subject in the SAML assertion in UI. Depending on how the\n\u003e IdP is setup, this might be a randomly generated id.\n\u003e\n\u003e You should only rely on the user attributes in the assertion.\n\u003e As an application working with an IdP, you should know which attributes\n\u003e will be made available to your application and out of\n\u003e those attributes which one should be treated as the logged in userid/name.\n\u003e For example it could be \"uid\" or \"email\" depending on how the authentication\n\u003e source is setup in the IdP.\n\n## Customization\n\n#### Pipeline\n\n`Samly` allows you to specify a Plug Pipeline if you need more control over\nthe authenticated user's attributes and/or do a Just-in-time user creation.\nThe Plug Pipeline is invoked after the user has successfully authenticated\nwith the IdP but before a session is created.\n\nThis is just a vanilla Plug Pipeline. The SAML assertion from\nthe IdP is made available in the Plug connection as a \"private\".\n(The pipeline plugs have access to the `idp_id` in this assertion.)\nIf you want to derive new attributes, create an Elixir map data (`%{}`)\nand update the `computed` field of the SAML assertion and put it back\nin the Plug connection private with `Conn.put_private` call.\n\nHere is a sample pipeline that shows this:\n\n```elixir\ndefmodule MySamlyPipeline do\n  use Plug.Builder\n  alias Samly.{Assertion}\n\n  plug :compute_attributes\n  plug :jit_provision_user\n\n  def compute_attributes(conn, _opts) do\n    assertion = conn.private[:samly_assertion]\n\n    # This assertion has the idp_id\n    # %Assertion{idp_id: idp_id} = assertion\n\n    first_name = Map.get(assertion.attributes, \"first_name\")\n    last_name  = Map.get(assertion.attributes, \"last_name\")\n\n    computed = %{\"full_name\" =\u003e \"#{first_name} #{last_name}\"}\n\n    assertion = %Assertion{assertion | computed: computed}\n\n    conn\n    |\u003e  put_private(:samly_assertion, assertion)\n\n    # If you have an error condition:\n    # conn\n    # |\u003e  send_resp(404, \"attribute mapping failed\")\n    # |\u003e  halt()\n  end\n\n  def jit_provision_user(conn, _opts) do\n    # your user creation here ...\n    conn\n  end\nend\n```\n\nMake this pipeline available in your config:\n\n```elixir\nconfig :samly, Samly.Provider,\n  identity_providers: [\n    %{\n      # ...\n      pre_session_create_pipeline: MySamlyPipeline,\n      # ...    \n    }\n  ]\n```\n\n#### State Store\n\nTake 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.).\n\n## Security Related\n\n+   `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`).\n+   Besides the `RelayState`, the request and response `idp_id`s must match. Reponse is rejected if they don't.\n+   `Samly` makes the original request ID that an auth response corresponds to\nin `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.\n+   OOTB SAML requests and responses are signed.\n+   Signature digest method supported: `SHA256`.\n    \u003e Some Identity Providers may be using `SHA1` by default.\n    \u003e Make sure to configure the IdP to use `SHA256`. `Samly`\n    \u003e will reject (`access_denied`) IdP responses using `SHA1`.\n+   `esaml` provides additional checks such as trusted certificate verification, recipient verification among others.\n+   By default, `Samly` signs the SAML requests it sends to the Identity Provider. It also\n    expects the SAML reqsponses to be signed (both assertion and envelopes). If your IdP is\n    not configured to sign, you will have to explicitly turn them off in the configuration.\n    It is highly recommended to turn signing on in production deployments.\n+   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.\n    \u003e [Supported Encryption algorithms](https://github.com/handnot2/esaml#assertion-encryption)\n+   Make sure to use HTTPS URLs in production deployments.\n\n## FAQ\n\n#### How to setup a SAML 2.0 IdP for development purposes?\n\nDocker based setup of [`SimpleSAMLPhp`](https://simplesamlphp.org) is made available\nat [`samly_simplesaml`](https://github.com/handnot2/samly_simplesaml) Git Repo.\nCheck out the `README.md` file of this repo.\n\nThere is also a Docker based setup of [`Shibboleth`](https://www.shibboleth.net/).\nCheckout the corresponding `README.md` file in [`samly_shibboleth`](https://github.com/handnot2/samly_shibboleth) Git Repo.\n\n#### Any sample Phoenix application that shows how to use Samly?\n\nClone the [`samly_howto`](https://github.com/handnot2/samly_howto) Git Repo.\nDetailed instructions on how to setup and run this application are available\nin the `README.md` file in this repo.\n\n\u003e It is recommended that you use the `SamlyHowto` application to\n\u003e sort out any configuration issues by making that demo application work\n\u003e successfully with your Identity Provider (IdP) before attempting your\n\u003e application.\n\u003e\n\u003e This demo application supports experimentation with multiple IdPs.\n\n#### How to register the service provider with IdP\n\nIf you are using `samly_simplesaml` or `samly_shibboleth`, the instructions\nyou followed there would take care of registering your Phoenix SAML Service provider\nappliccation. For any other IdP, follow the instructions from the respective\nIdP vendor.\n\n#### Common Errors\n\n`access_denied {:error, :bad_recipient}` - Check the `base_url` in your `Samly`\nconfig setting under `indentity_providers`.\n\n`access_denied {:error, :bad_audience}` - Make sure that the `entity_id` in\nthe `Samly` config setting is correct.\n\n`access_denied {:envelope, {:error, :cert_no_accepted}}` - Make sure the\nIdentity Provider metadata XML file you are using in the `Samly` config setting\nis correct and corresponds to the IdP you are attempting to talk to. You get\nthis error if the certificate used by the IdP to sign the SAML responses\nhas changed and you don't have the updated IdP metadata XML file on the `Samly` end.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhandnot2%2Fsamly","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhandnot2%2Fsamly","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhandnot2%2Fsamly/lists"}