DEV Community

aliona matveeva
aliona matveeva

Posted on

Using ADFS as the Identity Provider for your Flask application

Federated identity allows users to access different resources using the same set of credentials. It makes the workflow more efficient not only by improving user experience but by increasing security. At the same time, it also makes it easier to develop your own applications, saving you from implementing custom auth every time.

There are multiple different federated identity management systems, and in this article I’ll explain how to use one of them — Microsoft AD FS — as the identity provider in your Flask application. This article is intended for beginners.

The setup

For a better understanding of the structure and idea of the flow, I put everything on the scheme:

Interactions scheme

Here, the User wants to access some data on Resource X. As Resource X doesn’t have any credential storage or authentication mechanism, the Flask application needs to verify the user’s identity for Resource X — and this happens via AD FS. Usually, after the user identity was confirmed on the identity provider side, the IdP is sending a callback request to the Service Provider server. This callback request contains some trusted data (usually a token or user information) that will later be used to access the desired resource.

AD FS Prerequisites

Note that before actually writing a Flask app, you or your administrator need to configure the AD FS to be the identity provider. There are multiple manuals around the Web and I’m not going to repeat them. Here’s one of the many.

These are the main things that we’ll need to have configured to establish a relationship between our Service and Identity Providers —

for AD FS:

  • relying party SSO service URL

  • party trust identifier

  • custom claim rules

for Flask app:

  • identity provider URL

  • sign in URL

  • AD FS certificate

The talking protocol

Many identity providers use SAML 2.0 protocol to exchange security data, AD FS is one of them as well. SAML-based identity federation enables using single sign-on (SSO). SSO simplifies password management and user authentication, and that’s exactly what we need to make user experience smooth and comfortable. You can read more on SAML here.

We’ll be using the python3-saml library by OneLogin to manage SAML communication. This library helps to easily add SAML support to your Python web application, and I love its simplicity yet such a usability at the same time.

The python3-saml library has brilliant documentation, and for details you can always refer there, but as I was building my Service Provider app, I spent some time figuring out how do some of the things work, so I thought I’d put it all in one place.

The coding

The first thing after installing the library is configuring the settings.json file — the file that’s actually responsible for establishing a connection to the IdP. For my simple app, the bare minimum of the settings looks like this:

{
  "strict": true,
  "debug": true,

  // service provider -- our app -- data
  "sp": {
                // the sp_domain is your SP's address, in our case, that's the address
                // of the Flask application. the metadata URL is used by AD FS to identiyfy the SP
                // based on the info provided in the settings file
        "entityId": "https://<sp_domain>/metadata/",
       // specifies info about the callback endpoint that will handle IdP's SAML response. 
                // for our example, here the sp_domain will also contain the 'adfs' path, as in the adfs_route below
                // ACS stands for Attribute Consumer Service
        "assertionConsumerService": {
            "url": "https://<sp_domain>/?acs",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        },
                // specifies info about the log out callback endpoint.
       // SLS stands for Single Logout Service
        "singleLogoutService": {
            "url": "https://<sp_domain>/?sls",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
        "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
        "x509cert": "",
        "privateKey": ""
    },
   // identity provider -- ad fs -- data
   "idp": {
                // the idp_domain is set for the AD FS server.  
       // the /adfs/services/trust URI contains IdP's metadata
        "entityId": "http://<idp_domain>/adfs/services/trust",
       // specifies the location where the log in request will be sent
        "singleSignOnService": {
            "url": "https://<idp_domain>/adfs/ls/",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
       // specifies the location where the log out request will be sent
        "singleLogoutService": {
            "url": "https://<idp_domain>/adfs/ls/",
            "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        },
                // the AD FS certificate 
        "x509cert": "ExampleCertString"
    }
}
Enter fullscreen mode Exit fullscreen mode

This is basically it. Now we only need to configure Flask’s endpoints mentioned above, so the app is ready to communicate with the Identity Provider.

The settings.json file is loaded to the python-saml toolkit by initializing an OneLogin_Saml2_Auth object. This object will later be used to perform SAML2.0-based actions. It also requires a ‘request’ object that contains the current request’s data. Again, see the perfect documentation where they provide detailed information on each of these objects and how to configure everything for your needs.

I will just show you the final view that handles every AD FS interaction:

import os, requests
from Flask import request

@app.route("/adfs", methods=["GET", "POST"])
def adfs_route():
    req = prepare_request(request)
    # project_dirpath should contain full path to the root directory of the Flask project
    auth = OneLogin_Saml2_Auth(req, custom_base_path=os.path.join(%project_dirpath%, 'saml'))

    error = None

    # single sign on. the 'entrypoint'
    if "sso" in request.args:
        return redirect(auth.login())

    # process AD FS callback response
    elif "acs" in request.args:
        not_auth_warn = not auth.is_authenticated()
        auth.process_response()
        if not auth.get_errors():
            session["samlUserdata"] = auth.get_attributes()
            self_url = OneLogin_Saml2_Utils.get_self_url(req)
            if "RelayState" in request.form and self_url != request.form["RelayState"]:
                return redirect(auth.redirect_to(request.form["RelayState"]))
        elif auth.get_settings().is_debug_active():
            error = auth.get_last_error_reason()

    # process log out
    elif "slo" in request.args:
        session.clear()
        return redirect(url_for("adfs_route"))

    if "samlUserdata" in session:
        attributes = session['samlUserdata']
        user_id = attributes.get("USER_ID")[0]
        ### send request to the Resource X here ###
        # this is a dummy code just to show the idea
        resourcex_data = requests.get(f"https://resourcex.com/{user_id}")
        return render_template('data.html', data=resourcex_data)

    return render_template('index.html', error=error)
Enter fullscreen mode Exit fullscreen mode

As simple as it is. In this example, I don’t process log out via AD FS, as for the minimum application and to demonstrate communication with AD FS Identity Provider terminating the session on our side only is pretty much fine. However, if you want to use the Single Log Out as well, you will need to configure the “sls” part of the endpoint, which will handle the AD FS log out callback after calling auth.logout().

Bonus — issues I ran into

While developing the Flask Service Provider application, I ran into a couple of issues that I’d like to share here to save your time:

  • At first the received AD FS callback had this ‘invalid_response’ error inside auth.get_errors(). The error message looked extra weird, like "The response was received at https://sp_domain.com/adfs instead of https://sp_domain.com/adfs". This is not a mistake, the URLs in the error message could be just the same. Turns out, sometimes, when your Flask app is hiding behind some proxy, some port confusion might appear. To solve that, consider configuring ‘server_port’ in the request dict built for the Saml2_Auth object.
  • I was a bit confused with this NameID thing, what are the different formats and how are they used and even why. As my AD FS was configured by an admin, they didn’t have the NameID specified, which caused errors. Apparently, the NameID is an important SAML element usually used to identify the subject SP and IdP are communicating about (in our case, the user). However, it’s not required. If you’re building a very basic app, it might not be needed, and the python3-saml lib allows you to turn NameID checking off. This is easily done by setting “wantNameId” attribute to false in advanced_settings.json file. If you want to know more about NameID formats, it’s worth checking section 8.3 of this SAML documentation.

Conclusion

As the number of services we use grows every day, we make our users remember more and more different credentials for different websites and applications. To simplify the process, most people reuse the same login/password pair for multiple accounts. But that’s not recommended as it creates a huge risk of an account being stolen or abused. Adding an Identity Provider to your organization solves this issue, improving user experience together with security. With AD FS and python3-saml it’s also easily implemented, and the bare minimum to set everything up is described in this article. Hope that helps!

Top comments (2)

Collapse
 
curiouspaul1 profile image
Curious Paul

thanks for this, really great article. Glad to see a fellow flask enthusiast here.

Collapse
 
yellalena profile image
aliona matveeva

thank you Paul!