Introduction
Why another book? Because in the last few months I found it very difficult to learn how to use SAML properly. Even though there are excelleant Python module and projects available. So, this is my try to write down my learning. I hope someone else will also be able to learn from this document.
This is a rolling relase book. Means the more I will learn, I will update update the book accordingly.
This book (and my learning) is possible thanks to the hand holding and guidance from Ivan Kanakarakis & Giuseppe De Marco.
How to get help about examples in this book?
If you want to talk to me, then come to #learnandteach
IRC channel on Libera.chat server. If you want know more about pySAML2
module or anything
else in the SAML land, maybe join the idpy slack and ask there.
You can also find me on Mastodon or on Twitter.
Security Assertion Markup Language (SAML)
It is a set XML based protocol messages + bindings + profiles. You can find more details in general at the wikipedia page.
To learn about SAML, we will have to learn about Identity Provider (IdP), service provider (SP) and the user . Generally the IdP is provided by some enterprise or organization (say your university or Google), and SP is the application written by you which will use SAML to authenticate users using those IdPs. The user will do various steps using the Broser (in most cases).
Example flow
Here is an example flow.
At first the user tries to access some resource using the brower, (1) the SP
checks if it is restricted access or not. Say it is only for logged in users,
then SP will show a screen to the user (or directly redirect the user) and then
redirect the user to do Single Sign-On (SSO) on the IdP (2). After the user logs
in, the IdP respons (4) with XHTML form which automatically does HTTP POST
request from the browser (5). The SP then varifies the SAML XML (assertion) and
treats the user as logged in. In the final step (6) the SP finally redirects the
user to the initial resource.
To know about how do they look like, please read (use Google to translate) docs.italia.it.
More examples can be found at samltool.com.
TLS setup using mkcert
To do development (and also for final production deployment) of SAML applications, we will need proper TLS ceritifcates. To do the same on the local development system, we will use the excellent mkcert from Filippo Valsorda.
Install nss-tools package for your system
For Fedora, I installed it via dnf
.
$ sudo dnf install nss-tools -y
or in Debian/Ubuntu
$ apt install libnss-tools
Getting mkcert
Then we get the latest release from the github release page.
$ wget https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.4-linux-amd64
$ mv mkcert-v1.4.4-linux-amd64 ~/bin/mkcert
$ chmod +x ~/bin/mkcert
Setting up the local CA
$ mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
The local CA is now installed in the Firefox trust store (requires browser restart)! 🦊
This will create two important files inside of your user home directory.
$ ls -l .local/share/mkcert/
.r--------@ 2.5k kdas 20 Dec 12:14 rootCA-key.pem
.rw-r--r--@ 1.8k kdas 20 Dec 12:14 rootCA.pem
The rootCA-key.pem
is an important file and it can allow people to decrypt traffic from your system. Do not share or randomly copy it around.
Setting up certificate for local development
$ mkcert localhost 127.0.0.1 ::1
Created a new certificate valid for the following names 📜
- "localhost"
- "127.0.0.1"
- "::1"
The certificate is at "./localhost+2.pem" and the key at "./localhost+2-key.pem" ✅
It will expire on 20 March 2024 🗓
We are going to use these two files (the certificate & the key file) in the coming chapters.
pySAML2
pySAML2 is a pure Python implementation of SAML2.0. Roland Hedberg is the original author, and now he maintains the project along with the other team members of Identity Python project.
For the rest of the examples in this book, I will be using Python3.10 in a virtual environment (on Fedora). But, you should be able to follow along in any modern Linux distribution (or on Mac or in WSL).
Creating our own service provider (SP)
We will start with creating a new project directory for our SP. We will also need a virtual environment for the same.
$ mkdir sp
$ cd sp
$ python3 -m venv .venv
$ source .venv/bin/activate
$ python3 -m pip install wheel
We are going to use Flask as the web framework for SP. Let us install a few other packages too.
$ python3 -m pip install flask ipdb pysaml2
Then copy over the certificate & the key file from home directory (we created them in the TLS chapter).
$ cp ~/localhost+2.pem ./backend.pem
$ cp ~/localhost+2-key.pem ./backend.key
Create test IdP
We will use the example provided by pysaml2. For that we will clone the repository, create a virtualenv for the same, install all required packages, and also copy over the key & certificate we created before.
$ git clone https://github.com/IdentityPython/pysaml2/
$ cd pysaml2/examples/idp2
$ python3 -m venv .venv
$ source .venv/bin/activate
$ python3 -m pip install wheel
$ cd ../..
$ python3 -m pip install .
$ python3 -m pip install mako cherrypy
$ cp ~/localhost+2.pem ./server.pem
$ cp ~/localhost+2-key.pem ./server.key
$ cp idp_conf.py.example idp_conf.py
In the last command, we copied the example IdP
configuration. We will make
three small modifications in this file. First, we mentioned that we want to
provide signed response and also changed the server key & certificate files path
to use the key & certificate we generated. Also the SP
metadata file path is
now commented.
--- idp_conf.py.example 2022-07-15 08:31:54.442424007 +0200
+++ idp_conf.py 2022-07-21 14:06:34.328494860 +0200
@@ -69,6 +69,8 @@
},
"idp": {
"name": "Rolands IdP",
+ "sign_response": True,
+ "sign_assertion": True,
"endpoints": {
"single_sign_on_service": [
("%s/sso/redirect" % BASE, BINDING_HTTP_REDIRECT),
@@ -111,10 +113,10 @@
},
},
"debug": 1,
- "key_file": full_path("pki/mykey.pem"),
- "cert_file": full_path("pki/mycert.pem"),
+ "key_file": full_path("./server.key"),
+ "cert_file": full_path("./server.pem"),
"metadata": {
- "local": [full_path("../sp-wsgi/sp.xml")],
+ "local": [full_path("./sp.xml")],
},
"organization": {
"display_name": "Rolands Identiteter",
Next, we will have to regenerate the metadata for the IdP
.
$ make_metadata.py idp_conf.py > idp.xml
[2022-07-20 13:37:20,260] [DEBUG] [saml2.assertion.__init__] policy restrictions: None
[2022-07-20 13:37:20,260] [DEBUG] [saml2.assertion.__init__] policy restrictions: None
[2022-07-20 13:37:20,260] [DEBUG] [saml2.assertion.__init__] policy restrictions: {'default': {'lifetime': {'minutes': 15}, 'attribute_restrictions': None, 'name_form': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'entity_categories': None}}
Every time we make any change to the configuration, we will have to regenerate the metadata. Same goes for the SP
.
Service provider configuration
The configuration of the pysaml2 is the core part of the whole learning process. And I should admit that my brain totally broke when I looked at these at first. Even though the documentation explains each configuration value, it was still a lot. Also this is the first time I had to configure a library/module to use. Before now I always to configure applications, but never a library at this detailed level.
After the first few failed attempts, Ivan showed me an easier way. Instead of writing the configuration from scratch, I started with an example configuration from the SATOSA project.
I am also using the code from Satosa, that way I can very easily parse the configuration from the YAML file and use it.
saml2_backend.yaml
---
module: satosa.backends.saml2.SAMLBackend
name: Saml2
config:
idp_blacklist_file: /path/to/denylist.json
acr_mapping:
"": default-LoA
"https://accounts.google.com": LoA1
# disco_srv must be defined if there is more than one IdP in the metadata specified above
disco_srv: http://disco.example.com
entityid_endpoint: true
mirror_force_authn: no
memorize_idp: no
use_memorized_idp_when_force_authn: no
send_requester_id: no
enable_metadata_reload: no
sp_config:
name: "Demo SP written in Python"
description: "Our amazing SP"
key_file: backend.key
cert_file: backend.crt
organization: {display_name: Example Identities, name: Example Identities Org., url: 'http://www.example.com'}
contact_person:
- {contact_type: technical, email_address: technical@example.com, given_name: Technical}
- {contact_type: support, email_address: support@example.com, given_name: Support}
metadata:
local: [idp.xml]
entityid: http://localhost:5000/proxy_saml2_backend.xml
accepted_time_diff: 60
service:
sp:
ui_info:
display_name:
- lang: en
text: "SP Display Name"
description:
- lang: en
text: "SP Description"
information_url:
- lang: en
text: "http://sp.information.url/"
privacy_statement_url:
- lang: en
text: "http://sp.privacy.url/"
keywords:
- lang: se
text: ["Satosa", "SP-SE"]
- lang: en
text: ["Satosa", "SP-EN"]
logo:
text: "http://sp.logo.url/"
width: "100"
height: "100"
authn_requests_signed: true
want_response_signed: true
want_assertion_signed: true
allow_unknown_attributes: true
allow_unsolicited: true
endpoints:
assertion_consumer_service:
- [http://localhost:5000/acs/post, 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']
- [http://localhost:5000/acs/redirect, 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
discovery_response:
- [<base_url>/<name>/disco, 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol']
name_id_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
# A name_id_format of 'None' will cause the authentication request to not
# include a Format attribute in the NameIDPolicy.
# name_id_format: 'None'
name_id_format_allow_create: true
We are interested only the values in the sp_config
dictionary here. We mentioned the key file and the certificate file. We also defined
the URL for callback (via POST
or redirect
) in the endpoints
as assertion_consumer_service
.
The
meatadata
contains various service related metadata, in our case we are mentioning an
idp.xml
file. Let us copy over this file from our test IdP.
$ cp ../../pysaml2/example/idp2/idp.xml .
The flask application (SP) will run locally with default flask app port 5000. So, we will have to define the routes for all the URLs mentioned here.
We will need the satosa
installed in the virtual environment so that we can reuse that code.
$ python3 -m pip install satosa
app.py
Next, is our main Flask application. Please open the current code in a browser tab. We will go through this code and try to understand it better.
We are first reading the YAMl file for configuration values and then creating a configuration object.
with open("./saml2_backend.yaml") as fobj:
sp_config = SPConfig().load(yaml.load(fobj, SafeLoader)["config"]["sp_config"])
sp = Saml2Client(sp_config)
Next, we have a rndstr
function to create random string.
def rndstr(size=16, alphabet=""):
"""
Returns a string of random ascii characters or digits
:type size: int
:type alphabet: str
:param size: The length of the string
:param alphabet: A string with characters.
:return: string
"""
rng = random.SystemRandom()
if not alphabet:
alphabet = string.ascii_letters[0:52] + string.digits
return type(alphabet)().join(rng.choice(alphabet) for _ in range(size))
We also have a function to find the IdP
's entity ID, called get_idp_entity_id
.
In our code it is using the global object sp
.
def get_idp_entity_id():
"""
Finds the entity_id for the IDP
:return: the entity_id of the idp
"""
idps = sp.metadata.identity_providers()
only_idp = idps[0]
entity_id = only_idp
return entity_id
We have our index page route as hello_world
function. This currently shows a login link.
@app.route("/")
def hello_world():
return '<p><a href="/login/">Login</a></p>'
We also need a /metadata/
endpoint, this will help us to generate the metadata (XML format) for the SP.
We need to pass this metadata information to our IdP
before hand. We are using the create_metadata_string
function
from the pysaml2
module.
@app.route("/metadata/")
def metadata():
metadata_string = create_metadata_string(
None, sp.config, 4, None, None, None, None, None
).decode("utf-8")
return Response(metadata_string, mimetype="text/xml")
We will come back to this metadata later in the chapter/book. Meanwhile, we look into the most complex function in our code (yet),
the /login/
route.
@app.route("/login/")
def login():
try:
acs_endp, response_binding = sp.config.getattr("endpoints", "sp")[
"assertion_consumer_service"
][0]
relay_state = rndstr()
entity_id = get_idp_entity_id()
req_id, binding, http_args = sp.prepare_for_negotiated_authenticate(
entityid=entity_id,
response_binding=response_binding,
relay_state=relay_state,
)
if binding == BINDING_HTTP_REDIRECT:
headers = dict(http_args["headers"])
return redirect(str(headers["Location"]), code=303)
return Response(http_args["data"], headers=http_args["headers"])
except Exception as e:
print(e)
Here, first we get the response_binding
value from our configuration, the
default value for this is BINDING_HTTP_POST
, that is
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
. You will also notice that
the function again returns a binding
value. The function itself checks for
available options from IdP
, and returns the correct binding back. For our test
IdP
it actually returns BINDING_HTTP_REDIRECT
, so our code calls redirect
function with status code 303
. The Location
header value contains all required SAML data in encoded format.
This is what it looks like:
{'data': [],
'headers': [('Location',
'https://localhost:8088/sso/redirect?SAMLRequest=nVdZj9rYEn7vX4HII%2Bp4YzOazuh4xRgb78Z%2BGXnDC8YGH%2B%2B%2FftzdSTqJckdzBwmJqlPL91XVqSP%2B%2BLO%2F5bM2qmBaFi9z7DM6%2F%2FPL0x8FRHegqZNCix5NBOvZZFXA3aR%2BmTdVsSs9mE6id4vgrg52OpCOO%2FwzurtXZV0GZT7%2F7oD9s4MHYVTVU%2B4PD%2FxlntT1fYcgXdd97ojPZRUjOIqiCEoik1EI0%2FjTfCYwL%2FM0fKblaISbwh1q806mZNLNZ9Y3NlOCyQ7CJhIKWHtFPalQHH9GN884amD4jiB22Nadz5iJYlp49ZvXa3I4Zc%2FLwMuTEta7LbrdIhCWSBWFaRUF9XymfCVKpUWYFvE%2Fk%2FTfjeBubxjKs3LSjfkMfCNOlwVsblGlR1WbBpGpHb%2Fz%2F0CweqXvBRC5T9Jb9jYNo0qeUr3MmehWznRl1lVpXUfFLC1mylAnU02%2FTI3Edm8VqGZcWd28%2Bp%2BRvmqmol7eTHdRUaf1MP%2FyezhTr%2FvhL%2Bjdcvwv3wuuURF%2BntrzB%2FKR8zU%2FvtPTeKptU0UzIXyZf5ew%2BcdxFArFpXyXaa8oi3RKlY5vHZGiiUs4A3lcTgST2%2F8YDwzB0NfxeI764DnAlsWnOfJL%2Fn8Z6adBq6D3DBMP%2BxZMiy5RFRVBNDM14WX%2B6bcj%2BG5qVF4BX0sJf5H%2FPwRR0UZ5eY%2FCZ%2FiNyDcw%2Fz7i76uD%2FA4mk8bTdfgvtfqxTu9RLC9voi%2Fb6%2BZGW7wRrvsNlWS2Lmx0zeTPegtf3jH8aP2u%2BV7nr%2FKvU%2FK9qe9Ogo%2FWnB7eKJDd95ZYDf7ysBFpRJOWZge5QVizqGKKEVJ1anvmdV32F55LEKv04Q0Lc%2FNUEWxuUhcsoQQhkLZxKpWSjnjtIs0RJTkiSHURo6LnL01ubOP7xipCdM%2FYe58G8nUBkScXD1cNeoTQjkWtR%2Ff3fiGQJbdVXZXVUl0A9GD5jrPdbFqH6x0Hi629NpKjXYYPI3TTp7ErWaItYEDl0ul6zu93z1hyvR0Z3GZjr1fnA5lrTtF2toaKx%2Fahk7cer9wq89bZpS3IJxS1zDQOJT0iWSRb19eztTawTeZhjkQ9KLLmiD0kuYDblqFnQ%2F8mFzhaqUeEgxxdt%2BqTTpy0u6LeZBQ8CvuoYIJNS%2BDl5aMDP1T8rQtiNHy05LxCScarvQ%2BJfl1xl%2Bku19EXSRBYj6FpUNMx6AQKxIIGjkR475wwcU2CRolQLI6dFzodozoHsXSFpA1koLJHSgVd7MB%2Bn4GAimWLAqXB2Vjl4NYQ0tTVPbu5z5Odb1vNJDOqwXYSveQBZrJ01xUeriUBSkEflxOfn6ISajPZZ47dX30cSzx7GYtsjQV4kvg0xTpn6S4ZQSerDmOpKsPQq9rD5Two1Pg%2FxaKWZ8YQBolhx%2Bm7lBgVlbHyVYdOv191xFddZ49sIdHmO%2FZEiny7ztzzARV4LQ9vFvQJqnbt1SS%2F4tHu7i3PnLOWSwbojvEb3iNDh5XLc6PK962Dc9DjyTGkV18xa61vc3c%2FBaVOWKPHc1BgJ07DtYtjNpUAytP6g9cFn2BUlgKqCcBSoJgOvJ6LoJx6pzIh3NS26njr4NCrVedb10tWEvQhzxEd1MLdHGQ3XTlRiYar9pD2W9xoVfNkZ%2B2Fb%2BPONEbGshKBw5qLqIIFRykx4nhDwrdieTXgGtMP%2BxMpp%2FUtpYvuxkCKXtnxVi7iJr9dRLzcVpY5tMMQrGxSAYj%2BQE16uyKOG1oxlIcqKZsTbg%2FN4mIf1z69ujAqeZ%2FmA1c8es2g9qZAyQVLXFV3GdNJdHUc092v0lPdg3tVn%2FNlnFDj2DpGhx6GZdMnY7dJy5C0eWHvnvoizJYx9AX8sQ8ieZvs7zVw7Q1dcEa6n9bLPshvyxQxdY8S2j0ukcTx6iC80d2dMZZuRnfKG2zkff5kTOOxCW8iMzx8EEsUAHwWZhQqgbf5DZmOpZBOZac7w5UMMF7nf6%2BbLMMAkYrjiopZjlKD6QRc3s8klmeAHVPq2IeLx7UqO00EbF6IFIY6vfToOpQB8M1WY9mDAbI4zn%2BYZ31P7bdgwrFnwe8%2B7O%2FuJxPHPAXoFXtoOLklM5nhfR%2B11jnRdebp8CBL%2FcEyaCrBlaiIK22IvXW4jBbZKWu0dbDRaz6iUa5BrqOCkTiaINbYaCyBCeFePmD2gin5UwYvkmX7D23ZuduUp7bn9Exh8sN83Fp6URN0uCzZgPEP25OxPRd8IeVw9IlNVbjp8IAZTgz6mq0BgRH0SsU94lAddHyAWrhVWJXg7kyHkA8VQffa9noC55bUyP7Sc0t%2BPSZiY4XjyCq5iyZ7oTJDgQqVI48LFwWnFR7WxuoRItPGxbZHJx%2F0nhhLSk1Ryn5IGdoNcqfTC0o8OE1wnPa2yHgafTkZ6wt39Kh6BHXvX%2FNrcuJSzI2YR8M1eIIj8ma9ykw7txfrfr2UeLeRPfmBysdpTyzrq5sCVLFWUMAGRljlp8KD%2BVqu1I1zuxAufbyITYt6Qz2Wh8oBAYdICgUYnRX05JrFDWgj9QCglrl60yUcGVv4WcGxVtZ988Ce96vkXsXn%2Fuotxa23MrFKHmvRGSkDC9VsRDL%2FNG0NMjx5NG%2BobrCMvz0Wv27%2FD%2B37C4H89Hr8%2FL68yT%2F%2F%2F%2Fjy9Dc%3D&RelayState=w55JXDjMjii6zPmk&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=jdwo4NH3Hna4v3GRkKF55H%2Ftlv3OOxfDMkyxQJ%2BauR3bQ8aHiYerpBNITVzSyQFqXcv8Cj2uBeeG%2BEBbVuXsW1fx0LHtJuJw03NxuDnxGZnGaAg8XrEBbwOOCtVat7irAIzG9EanDyslAs1YxM4VQtaoXqlBVDr0ZqVzfWHtNAByfEDJAgR%2B7XMlUkZ8iwLDneT9vAnIj%2BZFsnAl%2BphJMOzoK2zR%2BFw81WndrwSbEdOMSTeONOpckMrQwOP2GGx9T5jehf3e2CikZFN03MAhuY1lP0tlUSdP3nQqPweh2aBD1MlC5mYlijZrmZ1L79fPcRY9UgWXEouw84B%2BmPe0Gg%3D%3D')],
'method': 'GET',
'status': 303,
'url': 'https://localhost:8088/sso/redirect'}
The last two routes are for step 5 of our flow diagram, in this step we validate the response we receive.
@app.route("/acs/post", methods=["POST"])
def acs_post():
outstanding_queries = {}
binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
authn_response = sp.parse_authn_request_response(
request.form["SAMLResponse"], binding, outstanding=outstanding_queries
)
#ipdb.set_trace()
return str(authn_response.ava)
@app.route("/acs/redirect", methods=["GET"])
def acs_redirect():
outstanding_queries = {}
binding = BINDING_HTTP_REDIRECT
authn_response = sp.parse_authn_request_response(
request.form["SAMLResponse"], binding, outstanding=outstanding_queries
)
return str(authn_response.ava)
The sp.parse_authn_request_response
validates the response, in future we will
put this function inside of a try-except
block to handle any failure or to
deal with bad response. These functions are supposed to redirect the user
(after marking them as logged in user) to the actual resource. But, for now we
will just print all the validated information we received from the IdP
.
Starting the SP Flask application
Do the following in the same directory of SP (remember to activate the virtual environment first).
$ flask run
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
Right now the server is listening on port 5000
.
Now, let us move to the test IdP
terminal, keep this application running.
Starting the test IdP
We will download the SP
metadata first.
$ curl http://localhost:5000/metadata/ -o sp.xml
Update the idp_conf.py
file with the metadata file to point to the new XML file.
"metadata": {
"local": [full_path("./sp.xml")],
},
Regenerate the idp.xml
and copy it over to SP
, and then restart SP
Flask application.
$ make_metadata.py idp_conf.py > idp.xml
[2022-07-21 13:37:20,260] [DEBUG] [saml2.assertion.__init__] policy restrictions: None
[2022-07-21 13:37:20,260] [DEBUG] [saml2.assertion.__init__] policy restrictions: None
[2022-07-21 13:37:20,260] [DEBUG] [saml2.assertion.__init__] policy restrictions: {'default': {'lifetime': {'minutes': 15}, 'attribute_restrictions': None, 'name_form': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'entity_categories': None}}
$ cp idp.xml ../samp-examples/sp/
Now, let us start the IdP
.
$ python idp.py idp_conf
[2022-07-21 10:42:25,773] [DEBUG] [saml2.assertion.__init__] policy restrictions: None
[2022-07-21 10:42:25,773] [DEBUG] [saml2.assertion.__init__] policy restrictions: None
[2022-07-21 10:42:25,773] [DEBUG] [saml2.assertion.__init__] policy restrictions: {'default': {'lifetime': {'minutes': 15}, 'attribute_restrictions': None, 'name_form': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'entity_categories': None}}
[2022-07-21 10:42:25,843] [INFO] [saml2.idp.<module>] Server starting
IDP listening on localhost:8088
On the browser
We will use Firefox as the browser for this step, you can use any browser of your choice. But, we will install saml-tracer plugin so that we can see the SAML request and response objects nicely in our browser.
After starting the tracer plugin, if we open the SP
URL http://localhost:5000/ we will see the following.
Next, we can click and login. The username/password combinations are mentioned in the idp.py
file. We are using testuser/qwerty
combination.
We can all the claims released by the IdP
at the last screen.
If we click on the SAML request in the tracer we can see the actual request.
Same about SAML response.
This where the tracer becomes so useful :) I personally found it super useful to see the actual SAML request/response and go through the various values inside of those.
Making the SP useful
The SAML request
Let us dig more into a SAML request. Here is one from our example in the last chapter.
<ns0:AuthnRequest xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:ns2="http://www.w3.org/2000/09/xmldsig#"
ID="id-JUwmMTjY6T9RTAN7S"
Version="2.0"
IssueInstant="2022-07-21T09:56:57Z"
Destination="https://localhost:8088/sso/redirect"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
AssertionConsumerServiceURL="http://localhost:5000/acs/post"
ProviderName="Demo SP written in Python"
>
<ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://localhost:5000/proxy_saml2_backend.xml</ns1:Issuer>
<ns2:Signature Id="Signature1">
<ns2:SignedInfo>
<ns2:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ns2:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<ns2:Reference URI="#id-JUwmMTjY6T9RTAN7S">
<ns2:Transforms>
<ns2:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ns2:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ns2:Transforms>
<ns2:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<ns2:DigestValue>knF1koHdOZhADoSsakgYkYNkmDM=</ns3:DigestValue>
</ns2:Reference>
</ns2:SignedInfo>
<ns2:SignatureValue>M1vZtdwkW8KBhNgBsw/8kMDWNcXIeMBtuUid6+r9zqE8647izXpHyn3cYQ4ORJR6
nDG496u7ZeRuqobbhpgZfwHU9c4obov0ilOaBJomxrfggwCYF+zExaIL9DzSph90
H1lkJG1UUiRuwK9OtSZeov2LjCD0A8H3YZFm/i3AwZYYavO0Nf6+cJ9bvrNLfcvV
aXCbeScPTsJJ/ju3l3BCQl2qozE1Bh8w+so96EmUx/OE9yIDMf2qKZP6N7bSHTaI
IeUbZ+xbHfs1ZkRLcrtwr7W48C2/kzSyXEcVGq5JVtBcZKh4WkmgMuKUkvJ0k5B6
KcRoRlIkK1CNlD4pk1pHuQ==</ns2:SignatureValue>
<ns2:KeyInfo>
<ns2:X509Data>
<ns2:X509Certificate>MIIEaDCCAtCgAwIBAgIRAL3dpwYdhZU3C03dKnLwadYwDQYJKoZIhvcNAQELBQAwgYsxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEwMC4GA1UECwwna2Rhc0Bsb2NhbGhvc3QubG9jYWxkb21haW4gKEt1c2hhbCBEYXMpMTcwNQYDVQQDDC5ta2NlcnQga2Rhc0Bsb2NhbGhvc3QubG9jYWxkb21haW4gKEt1c2hhbCBEYXMpMB4XDTIyMDEzMDE4MDQ0N1oXDTI0MDQzMDE3MDQ0N1owWzEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTAwLgYDVQQLDCdrZGFzQGxvY2FsaG9zdC5sb2NhbGRvbWFpbiAoS3VzaGFsIERhcykwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDds7tWQYa6cJxQrwbVkfjo3CJll/SAtIpUyNZi5Yeo0d5vJix82TvQUOWjvfGvgwUTzDVVhIF1ufKQA+FBPg/YayhGvKokTs61SJHO9NitmiCnwmDsBC5Wg8NngulmfK2o8rVUyvyyc5W9PA/Sq0UC853L7CPTPqQMP7O2Wyu+fWL6bC5fDQ9pbWV2PaC6D0W7n09+E3kQZ4gChekYYUZH5iOtxAprtXl4ghBzzvYTw0Jy4uxhzw7iod9WGIHZOxndj4gsbI2qHceN8hHptAZW7CnFTiHGSSHclm4i/USaBIvH2M93LkY/GTwpYzgMmTwOlu1zGbGOTMpM7dmKDyqbAgMBAAGjdjB0MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBQzxd+qkrowRKAElnKB10YxMqww0DAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBAC5EJuFNv9jNDGbb0V6l3wwUOJq9oSqED0iMs5KPK5Ryga6d4e+jOjuR6c7StGeC0Fu/kzP1920h/VzuRE31IdHNJ1W+DoGOjsfMVWbqR4wZ8iGB8XiXB1NqUqmvC+t3Cd4oEcDbJ8OT8XnGnMlszb37rnZiyqsj23yS6EtA313C5Q2a3JrJS2ysRd8PEQ3FpDw/9qQ/0HR8kOAXv9R9xfxF4G6zhKuVdzzEPlZ0hHIrUdIBdPLG2IfP2CPGstT5qd/6T118LYlySx3zoBQi0BWqMj0wyNwSC+BKJYucL2rZKDaRCfOT6fFLaBtzAtxbklkhOFi1ZeDquFu2h2/N765jUWlW+6x64MGZuNaNq0NLWzE4tkZiA0PV5sI1yDI5lOnasl6NrQ7Ymf3ZCLfKuv0aytzoJrYAcF/MPBADSEIShkjguAveQJAsRjZSuwhF9gV2XP21vNSbUJEXH5hprgXxka4K8a5U1rNztKYzBT1dQjz/jbOykw9dOaCGTQZc4g==</ns2:X509Certificate>
</ns2:X509Data>
</ns2:KeyInfo>
</ns2:Signature>
</ns0:AuthnRequest>
The request is signed. And AssertionConsumerServiceURL
contains the SP
route
(URL) where the response will be POST
back from the IdP
via the browser.
The SAML response
<ns0:Response xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:ns2="http://www.w3.org/2000/09/xmldsig#"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
ID="id-2AtrGBfajnH62i1VZ"
InResponseTo="id-ew6aM7l0xFh07xbCQ"
Version="2.0"
IssueInstant="2022-07-21T12:07:57Z"
Destination="http://localhost:5000/acs/post"
>
<ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://localhost:8088/idp.xml</ns1:Issuer>
<ns2:Signature Id="Signature1">
<ns2:SignedInfo>
<ns2:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ns2:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<ns2:Reference URI="#id-2AtrGBfajnH62i1VZ">
<ns2:Transforms>
<ns2:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ns2:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ns2:Transforms>
<ns2:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<ns2:DigestValue>G6ZNgH2RvTLXJzv3jZ/SvQ25E04=</ns2:DigestValue>
</ns2:Reference>
</ns2:SignedInfo>
<ns2:SignatureValue>c6Y+YJ8IGIT3lzDLxm3kCoxst4/p2k1MMl/oU8qUc0qnmt3D+hWvd+3vVyp4NaRJ
prYlqh3eqN5a0Ln4/LcQRQV/Rg5CQ8bkhQ5e+jgFjyHRVtOtvb20Vyb3q5Kyx/Eg
Vk2jRGMpAollVk4+5+mp46MY4F7+s1suhY0/wDwbo6Djl/L38K/LXhF1qQ3gxs2n
m63oMktW3bSomIiDGzLEEMqgbhYWjisTN9YsIK+WwQO3mJ/3bUF7baljt40hCwQA
QhIJP/rgOoyVzJj8eSmMGbZJ+r9gVNSGBybFhs2yJj5+PJA1JvtJJs340ujEXkZf
29GI0vG7PcOWY03wj6PfMw==</ns2:SignatureValue>
<ns2:KeyInfo>
<ns2:X509Data>
<ns2:X509Certificate>MIIEaDCCAtCgAwIBAgIRAL3dpwYdhZU3C03dKnLwadYwDQYJKoZIhvcNAQELBQAwgYsxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEwMC4GA1UECwwna2Rhc0Bsb2NhbGhvc3QubG9jYWxkb21haW4gKEt1c2hhbCBEYXMpMTcwNQYDVQQDDC5ta2NlcnQga2Rhc0Bsb2NhbGhvc3QubG9jYWxkb21haW4gKEt1c2hhbCBEYXMpMB4XDTIyMDEzMDE4MDQ0N1oXDTI0MDQzMDE3MDQ0N1owWzEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTAwLgYDVQQLDCdrZGFzQGxvY2FsaG9zdC5sb2NhbGRvbWFpbiAoS3VzaGFsIERhcykwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDds7tWQYa6cJxQrwbVkfjo3CJll/SAtIpUyNZi5Yeo0d5vJix82TvQUOWjvfGvgwUTzDVVhIF1ufKQA+FBPg/YayhGvKokTs61SJHO9NitmiCnwmDsBC5Wg8NngulmfK2o8rVUyvyyc5W9PA/Sq0UC853L7CPTPqQMP7O2Wyu+fWL6bC5fDQ9pbWV2PaC6D0W7n09+E3kQZ4gChekYYUZH5iOtxAprtXl4ghBzzvYTw0Jy4uxhzw7iod9WGIHZOxndj4gsbI2qHceN8hHptAZW7CnFTiHGSSHclm4i/USaBIvH2M93LkY/GTwpYzgMmTwOlu1zGbGOTMpM7dmKDyqbAgMBAAGjdjB0MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBQzxd+qkrowRKAElnKB10YxMqww0DAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBAC5EJuFNv9jNDGbb0V6l3wwUOJq9oSqED0iMs5KPK5Ryga6d4e+jOjuR6c7StGeC0Fu/kzP1920h/VzuRE31IdHNJ1W+DoGOjsfMVWbqR4wZ8iGB8XiXB1NqUqmvC+t3Cd4oEcDbJ8OT8XnGnMlszb37rnZiyqsj23yS6EtA313C5Q2a3JrJS2ysRd8PEQ3FpDw/9qQ/0HR8kOAXv9R9xfxF4G6zhKuVdzzEPlZ0hHIrUdIBdPLG2IfP2CPGstT5qd/6T118LYlySx3zoBQi0BWqMj0wyNwSC+BKJYucL2rZKDaRCfOT6fFLaBtzAtxbklkhOFi1ZeDquFu2h2/N765jUWlW+6x64MGZuNaNq0NLWzE4tkZiA0PV5sI1yDI5lOnasl6NrQ7Ymf3ZCLfKuv0aytzoJrYAcF/MPBADSEIShkjguAveQJAsRjZSuwhF9gV2XP21vNSbUJEXH5hprgXxka4K8a5U1rNztKYzBT1dQjz/jbOykw9dOaCGTQZc4g==</ns2:X509Certificate>
</ns2:X509Data>
</ns2:KeyInfo>
</ns2:Signature>
<ns0:Status>
<ns0:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</ns0:Status>
<ns1:Assertion Version="2.0"
ID="id-57PKtFAHtjXCPVdW0"
IssueInstant="2022-07-21T12:07:57Z"
>
<ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://localhost:8088/idp.xml</ns1:Issuer>
<ns2:Signature Id="Signature2">
<ns2:SignedInfo>
<ns2:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ns2:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<ns2:Reference URI="#id-57PKtFAHtjXCPVdW0">
<ns2:Transforms>
<ns2:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ns2:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ns2:Transforms>
<ns2:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<ns2:DigestValue>2xtlJf6NK9dDj+rys0jTIqvmfdA=</ns2:DigestValue>
</ns2:Reference>
</ns2:SignedInfo>
<ns2:SignatureValue>U4dGSI7dvOeGcekCSv16KxpwhYJvvk2lroJgquA17fMKHH4uImc87Kse41aH/NUY
FufdAcAFhW1EZNIZ8/mj3i8jhD4maJREp+N78/NO9HFCgdRnPpgPR1/XU/Eqkl/C
YhFU4tnfpMO35r4/PTWh0fhs9SLUI94j46G8GtwzqBLCXwk6++tCetsMWEQbcyIs
6aN383aUw+r0VcJQ1yfv1Qtl8sN370bW+FK+h8oa7IQZIUuOn+uuOaHpxwHryBAs
q0RdpDbXdK4oIhXmkJfhlyEoiRdWXngKPMXqme4HOlr4+pH5odlh82YB32JmlKw7
BsQ+JBB5Cy7O/f1Q30mvtQ==</ns2:SignatureValue>
<ns2:KeyInfo>
<ns2:X509Data>
<ns2:X509Certificate>MIIEaDCCAtCgAwIBAgIRAL3dpwYdhZU3C03dKnLwadYwDQYJKoZIhvcNAQELBQAwgYsxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEwMC4GA1UECwwna2Rhc0Bsb2NhbGhvc3QubG9jYWxkb21haW4gKEt1c2hhbCBEYXMpMTcwNQYDVQQDDC5ta2NlcnQga2Rhc0Bsb2NhbGhvc3QubG9jYWxkb21haW4gKEt1c2hhbCBEYXMpMB4XDTIyMDEzMDE4MDQ0N1oXDTI0MDQzMDE3MDQ0N1owWzEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTAwLgYDVQQLDCdrZGFzQGxvY2FsaG9zdC5sb2NhbGRvbWFpbiAoS3VzaGFsIERhcykwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDds7tWQYa6cJxQrwbVkfjo3CJll/SAtIpUyNZi5Yeo0d5vJix82TvQUOWjvfGvgwUTzDVVhIF1ufKQA+FBPg/YayhGvKokTs61SJHO9NitmiCnwmDsBC5Wg8NngulmfK2o8rVUyvyyc5W9PA/Sq0UC853L7CPTPqQMP7O2Wyu+fWL6bC5fDQ9pbWV2PaC6D0W7n09+E3kQZ4gChekYYUZH5iOtxAprtXl4ghBzzvYTw0Jy4uxhzw7iod9WGIHZOxndj4gsbI2qHceN8hHptAZW7CnFTiHGSSHclm4i/USaBIvH2M93LkY/GTwpYzgMmTwOlu1zGbGOTMpM7dmKDyqbAgMBAAGjdjB0MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBQzxd+qkrowRKAElnKB10YxMqww0DAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBAC5EJuFNv9jNDGbb0V6l3wwUOJq9oSqED0iMs5KPK5Ryga6d4e+jOjuR6c7StGeC0Fu/kzP1920h/VzuRE31IdHNJ1W+DoGOjsfMVWbqR4wZ8iGB8XiXB1NqUqmvC+t3Cd4oEcDbJ8OT8XnGnMlszb37rnZiyqsj23yS6EtA313C5Q2a3JrJS2ysRd8PEQ3FpDw/9qQ/0HR8kOAXv9R9xfxF4G6zhKuVdzzEPlZ0hHIrUdIBdPLG2IfP2CPGstT5qd/6T118LYlySx3zoBQi0BWqMj0wyNwSC+BKJYucL2rZKDaRCfOT6fFLaBtzAtxbklkhOFi1ZeDquFu2h2/N765jUWlW+6x64MGZuNaNq0NLWzE4tkZiA0PV5sI1yDI5lOnasl6NrQ7Ymf3ZCLfKuv0aytzoJrYAcF/MPBADSEIShkjguAveQJAsRjZSuwhF9gV2XP21vNSbUJEXH5hprgXxka4K8a5U1rNztKYzBT1dQjz/jbOykw9dOaCGTQZc4g==</ns2:X509Certificate>
</ns2:X509Data>
</ns2:KeyInfo>
</ns2:Signature>
<ns1:Subject>
<ns1:NameID NameQualifier="https://localhost:8088/idp.xml"
SPNameQualifier="http://localhost:5000/proxy_saml2_backend.xml"
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
>615abcec23b7a4165907c61172ce29ea5eeecfa100f4428e61388329a08c9e87</ns1:NameID>
<ns1:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<ns1:SubjectConfirmationData NotOnOrAfter="2022-07-21T12:22:57Z"
Recipient="http://localhost:5000/acs/post"
InResponseTo="id-ew6aM7l0xFh07xbCQ"
/>
</ns1:SubjectConfirmation>
</ns1:Subject>
<ns1:Conditions NotBefore="2022-07-21T12:07:57Z"
NotOnOrAfter="2022-07-21T12:22:57Z"
>
<ns1:AudienceRestriction>
<ns1:Audience>http://localhost:5000/proxy_saml2_backend.xml</ns1:Audience>
</ns1:AudienceRestriction>
</ns1:Conditions>
<ns1:AuthnStatement AuthnInstant="2022-07-21T12:07:57Z"
SessionIndex="id-8XXMIpMS8VVJubvRo"
>
<ns1:AuthnContext>
<ns1:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</ns1:AuthnContextClassRef>
<ns1:AuthenticatingAuthority>https://localhost:8088</ns1:AuthenticatingAuthority>
</ns1:AuthnContext>
</ns1:AuthnStatement>
<ns1:AttributeStatement>
<ns1:Attribute Name="urn:oid:2.5.4.4"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="sn"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>Testsson</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:2.5.4.42"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="givenName"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>Test</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="eduPersonAffiliation"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>student</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.9"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="eduPersonScopedAffiliation"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>student@example.com</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="eduPersonPrincipalName"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>test@example.com</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:0.9.2342.19200300.100.1.1"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="uid"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>testuser</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="eduPersonTargetedID"
>
<ns1:AttributeValue>
<ns1:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">one!for!all</ns1:NameID>
</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:2.5.4.6"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="c"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>SE</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:2.5.4.10"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="o"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>Example Co.</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:2.5.4.11"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="ou"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>IT</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:2.5.4.43"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="initials"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>P</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:0.9.2342.19200300.100.1.43"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="co"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>co</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:0.9.2342.19200300.100.1.3"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="mail"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>mail</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:1.3.6.1.4.1.2428.90.1.6"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="noreduorgacronym"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>noreduorgacronym</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:1.3.6.1.4.1.25178.1.2.9"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="schacHomeOrganization"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>example.com</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:1.2.840.113549.1.9.1.1"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="email"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>test@example.com</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:2.16.840.1.113730.3.1.241"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="displayName"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>Test Testsson</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="labeledURL"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>http://www.example.com/test My homepage</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:1.3.6.1.4.1.2428.90.1.5"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="norEduPersonNIN"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>SE199012315555</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:2.5.4.16"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="postaladdress"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>postaladdress</ns1:AttributeValue>
</ns1:Attribute>
<ns1:Attribute Name="urn:oid:2.5.4.3"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
FriendlyName="cn"
>
<ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:type="xs:string"
>cn</ns1:AttributeValue>
</ns1:Attribute>
</ns1:AttributeStatement>
</ns1:Assertion>
</ns0:Response>
A big XML :) We can see both the response and the attributes are signed. By default our test IdP
released all the attributes it had. You can see the different
maps available in the source.
You can search more to find details about these attributes, here are a few links:
- https://wiki.refeds.org/display/STAN/eduPerson+2020-01
- https://github.com/voperson/voperson/tree/2.0.0
Now a Service Provider using Flask with users and authentication
Our last example was purely a demo example, something which is nice to start learning. But, we need more things to be production ready.
So, in our virtual environment we need some more packages.
python3 -m pip install flask-sqlalchemy flask-login flask-migrate flask ipdb pysaml2
If you are new to Flask, then I would suggest to first look at the Flask Mega Tutorial. That tutorial should explain you all the basics we need for our codebase.
To make things easier, feel free to clone the saml-examples repository, and then look at the sp2 directory.
All of our application spefic code is now at app
directory & all database migrations are in migrations
directory. You will have to
copy over the backend.key
, backend.crt
& idp.xml
in the same directory to make it working.
app/models.py
In this file we are declaring our database model. Now I am just using the email address for every user. Based on your need you can add as much details as you want in the model.
from flask_login import UserMixin
from app import db
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(250), index=True, unique=True)
def __repr__(self):
return '<User {}>'.format(self.email)
app/templates
In this directory we have various HTML templates. The one we should look at is profile.html
.
{% extends "base.html" %}
{% block content %}
<h1>
Hello {{ current_user.email }}
</h1>
{% endblock %}
This page assumes there is a logged in user, and we can access the email address by {{ current_user.email }}
.
app/init.py
This is the primary file for our application.
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite"
app.config[
"SECRET_KEY"
] = "192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf"
db = SQLAlchemy(app)
migrate = Migrate(app, db)
login_manager = LoginManager()
login_manager.login_view = "login"
login_manager.init_app(app)
from .models import User
We are creating the Flask app
variable with required configrutions, like
database path, and a static SECRET_KEY
for our development purpose. Remember
to change these values with proper ones for production.
We are also creating the db
instance, and also the migrate
to make sure that
we have database migrations. The login_manager
is the piece of code which will
deal with user authentication related steps. We are marking our login
function
as the view to be used when the user needs to login. At the last line we are
importing the User
we defined in our models.py
.
@login_manager.user_loader
def load_user(user_id):
# since the user_id is just the primary key of our user table, use it in the query for the user
return User.query.get(int(user_id))
Next, we have a load_user
function as defined by the flask_login.
@app.route("/")
def index():
return render_template("index.html")
We also have a nicer index page, where we are rendering the index.html
template.
@app.route("/profile/", methods=["GET"])
@login_required
def profile():
return render_template("profile.html")
@app.route("/logout/", methods=["GET"])
def logout():
logout_user()
return redirect(url_for("index"))
We also added two new routes for our application. /profile
is the page which
only authenticated users can view, that is why we used the @login_required
decorator there (from flask_login
module). And next is the /logout
to make
sure that the user can logout.
@app.route("/acs/post", methods=["POST"])
def acs_post():
outstanding_queries = {}
binding = BINDING_HTTP_POST
try:
authn_response = sp.parse_authn_request_response(
request.form["SAMLResponse"], binding, outstanding=outstanding_queries
)
except:
return render_template("error.html"), 500
# ipdb.set_trace()
email = authn_response.ava["email"][0]
# Now check if an user exists, or add one
user = User.query.filter_by(email=email).first()
if not user:
user = User(email=email)
db.session.add(user)
db.session.commit()
login_user(user, remember=True)
return redirect(url_for("profile"))
The /acs/post
route got the biggest changes. After validating the SAML
respose, we are extracting the user email addresss, and first checking if an
user account is already there or not. If no such user, then we create one and
save the database using db.session.commit()
. Then next step is to call
login_user
. And finally we redirect the logged in user to the /profile
view.
We can see it live above, we can also see because we are logged in to the IdP
,
when the user logsout and then again type in /profile
, the application goes to
IdP
and then logs in (as user is already logged in there) and comes back to
the /profile
view.
What next?
I will be pushing the chapters on Django soon.
SATOSA
SATOSA is a proxy written in Python which helps for translating between different authentication protocols such as SAML2, OpenID Connect and OAuth2.
We will slowly learn about SATOSA in details in the coming chapters. For now we will learn how to configure one SATOSA instance to work with our example IdP and service provider.
SP ---> SATOSA FRONTEND <--->SATOSA BACKEND <-----> IdP
Setting up SATOSA
$ git clone https://github.com/IdentityPython/satosa.git
$ cd satosa
$ python3 -m venv .venv
$ source .venv/bin/activate
$ python3 -m pip install wheel
$ python3 -m pip install -e .
$ cd example/
$ cp internal_attributes.yaml.example internal_attributes.yaml
$ cp proxy_conf.yaml.example proxy_conf.yaml
$ cp ~/localhost+2.pem ./proxy.crt
$ cp ~/localhost+2.pem ./backend.crt
$ cp ~/localhost+2-key.pem ./backend.key
$ cp ~/localhost+2-key.pem ./proxy.key
Here we are using the same key/certificate for both the proxy frontend and backend.
Update the proxy_conf.yaml
file first.
--- proxy_conf.yaml.example 2023-11-27 12:48:53.272742476 +0100
+++ proxy_conf.yaml 2023-11-21 16:15:21.878249924 +0100
@@ -1,7 +1,7 @@
-BASE: https://example.com
+BASE: https://localhost:8010
COOKIE_STATE_NAME: "SATOSA_STATE"
-CONTEXT_STATE_DELETE: yes
+CONTEXT_STATE_DELETE: false
STATE_ENCRYPTION_KEY: "asdASD123"
cookies_samesite_compat:
@@ -15,8 +15,8 @@
FRONTEND_MODULES:
- "plugins/frontends/saml2_frontend.yaml"
-MICRO_SERVICES:
- - "plugins/microservices/static_attributes.yaml"
+MICRO_SERVICES: []
+# - "plugins/microservices/static_attributes.yaml"
LOGGING:
version: 1
We are mentioning that we will be using port 8010
along with TLS for the example server.
We are also using both SAML frontend and backend. This is mentioned via the
following in the proxy_conf.yaml
file. There can be more than one frontend and
backend.
BACKEND_MODULES:
- "plugins/backends/saml2_backend.yaml"
FRONTEND_MODULES:
- "plugins/frontends/saml2_frontend.yaml"
We need to setup next frontend and backend.
$ cd plugins/backends/
$ cp saml2_backend.yaml.example saml2_backend.yaml
diff -Naur saml2_backend.yaml.example saml2_backend.yaml
--- saml2_backend.yaml.example 2023-11-21 16:06:31.650318811 +0100
+++ saml2_backend.yaml 2023-11-21 16:26:37.869130158 +0100
@@ -1,14 +1,14 @@
module: satosa.backends.saml2.SAMLBackend
name: Saml2
config:
- idp_blacklist_file: /path/to/blacklist.json
+ #idp_blacklist_file: /path/to/blacklist.json
- acr_mapping:
- "": default-LoA
- "https://accounts.google.com": LoA1
+ # acr_mapping:
+ # "": default-LoA
+ # "https://accounts.google.com": LoA1
# disco_srv must be defined if there is more than one IdP in the metadata specified above
- disco_srv: http://disco.example.com
+ #disco_srv: http://disco.example.com
entityid_endpoint: true
mirror_force_authn: no
@@ -80,8 +80,9 @@
text: "http://sp.logo.url/"
width: "100"
height: "100"
- authn_requests_signed: true
- want_response_signed: true
+ authn_requests_signed: false
+ want_response_signed: false
+ want_assertions_or_response_signed: true
allow_unsolicited: true
endpoints:
assertion_consumer_service:
And the same for the frontend:
$ cd ../frontends/
$ cp saml2_frontend.yaml.example saml2_frontend.yaml
And update it with the following configuration.
--- saml2_frontend.yaml.example 2023-11-27 12:41:56.187392054 +0100
+++ saml2_frontend.yaml 2023-11-21 16:20:25.274196172 +0100
@@ -49,8 +49,8 @@
"remd:contactType": "http://refeds.org/metadata/contactType/security",
},
}
- key_file: frontend.key
- cert_file: frontend.crt
+ key_file: proxy.key
+ cert_file: proxy.crt
metadata:
local: [sp.xml]
Next we will have to generate the metadata for the SATOSA proxy and then start the proxy server itself.
$ cd ../..
$ satosa-saml-metadata proxy_conf.yaml proxy.key proxy.crt
$ gunicorn -b 0.0.0.0:8010 satosa.wsgi:app --keyfile proxy.key --certfile proxy.crt
Updating the IdP with proxy's metadata
Now we have to update the IdP with the proxy's metadata as service provider. We can either link or copy the backend.xml
file for this.
$ ln -s ../../../satosa/example/backend.xml
And then updated the idp_conf.py
file.
--- idp_conf.py.example 2023-11-21 14:41:57.200660142 +0100
+++ idp_conf.py 2023-11-21 16:30:32.719075149 +0100
@@ -2,15 +2,13 @@
# -*- coding: utf-8 -*-
import os.path
+from saml2 import BINDING_HTTP_REDIRECT, BINDING_URI
from saml2 import BINDING_HTTP_ARTIFACT
from saml2 import BINDING_HTTP_POST
-from saml2 import BINDING_HTTP_REDIRECT
from saml2 import BINDING_SOAP
-from saml2 import BINDING_URI
from saml2.saml import NAME_FORMAT_URI
-from saml2.saml import NAMEID_FORMAT_PERSISTENT
from saml2.saml import NAMEID_FORMAT_TRANSIENT
-
+from saml2.saml import NAMEID_FORMAT_PERSISTENT
try:
from saml2.sigver import get_xmlsec_binary
@@ -71,6 +69,8 @@
},
"idp": {
"name": "Rolands IdP",
+ "sign_response": True,
+ "sign_assertion": True,
"endpoints": {
"single_sign_on_service": [
("%s/sso/redirect" % BASE, BINDING_HTTP_REDIRECT),
@@ -113,10 +113,10 @@
},
},
"debug": 1,
- "key_file": full_path("pki/mykey.pem"),
- "cert_file": full_path("pki/mycert.pem"),
+ "key_file": full_path("./server.key"),
+ "cert_file": full_path("./server.pem"),
"metadata": {
- "local": [full_path("../sp-wsgi/sp.xml")],
+ "local": [full_path("./backend.xml")],
},
"organization": {
"display_name": "Rolands Identiteter",
We can then start the IdP server on port 8088
.
$ python idp.py idp_conf
Setting up the service provider
To make things easier, feel free to clone the saml-examples repository, and then look at the sp3 directory.
We copied over the frontend.xml
as the metadata from the proxy service, this will be used as IdP's metadata in the Flask based service provider.
--- ../sp2/saml2_backend.yaml 2022-07-21 14:02:10.464265209 +0200
+++ saml2_backend.yaml 2023-11-21 16:32:31.219046414 +0100
@@ -29,7 +29,7 @@
- {contact_type: support, email_address: support@example.com, given_name: Support}
metadata:
- local: [idp.xml]
+ local: [frontend.xml]
entityid: http://localhost:5000/proxy_saml2_backend.xml
accepted_time_diff: 60
We can start the service next.
$ flask run
Then we can login to the server. If you notice the address bar, you can see that we fist went to the SATOSA proxy and then to the IdP server.