Performing SAML SSO using JWT in Django

When the Django application needs to be separated into front-end and back-end, and you want to authenticate your calls to your other platforms/services, the stateless JWT in pair with Django Rest Framework is a good choice. 
But what if you want to integrate single sign-on/single log-out with the other applications which are using SAML? Moreover, your application may be Service Provider and Identity Provider at the same time.
 

Oleksandr Gorbunov
Oleksandr Gorbunov
Sept. 17, 202013 min read
Performing SAML SSO using JWT in Django

SAML (Security Assertion Markup Language) is an open standard that allows you to perform single sign-on (SSO), namely secure log in to third-party applications using session from another application. That means you don’t need to enter your credentials to authenticate some sites (Service Providers) if you once logged in to the particular site (Identity Provider).

There are many libraries on the internet that allow us to easily integrate the Django authentication mechanism with the SAML, but all of them are based on the standard Django’s session-based authentication, but you can’t use that. Therefore, you need some adapter between JWT → SAML and vice versa SAML → JWT. Have a look at the illustration below:

This diagram shows the basic idea of the project. The circle in the middle is our Django app with some front end layer and JWT token authentication enabled. Imagine, you have hundreds of instances of our app, so users, who have an account in the external IDP (left circle), have the ability to log in to the particular instance with just one click. Also, since your app acts as an IDP, imagine that there is an external service, for example, some statistics aggregator, and when the user clicks the “statistics” tab in our site, they will be redirected and silently authenticated to a completely different site, without having to enter credentials and have the account at that statistics website.

 

Libraries used in the project:

  • djangorestframework – Rest framework

  • django-rest-framework-simplejwt – JWT Auth

  • djangosaml2 – SAML Service Provider

  • djangosaml2idp – SAML Identity Provider

Assuming that the SAML and metadata are configured properly, and you can perform SSO using Django and obtain a session. Let’s start from the part when the Django app acts as a Service Provider.

Django app as a Service Provider

First, you have to create view, to which the user will be redirected after SAML login:

 

from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import RedirectView

from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.views import TokenCookieViewMixin


class Saml2JwtView(LoginRequiredMixin, TokenCookieViewMixin, RedirectView):
   # url to redirect to (front end url)
   url = "http://mysite.com"

   def get(self, request, *args, **kwargs):
       response = super().get(request, *args, **kwargs)
       # Obtain JWT tokens for logged in user
       refresh = RefreshToken.for_user(request.user)
       # Set JWT cookies
       response = self.set_auth_cookies(
           response, {"access": str(refresh.access_token), "refresh": str(refresh)}
           )
       # Logout from django (remove session)
       logout(request)
       return response

 

Add this view to urls.py:

 

urlpatterns = [
   path("saml2/jwt/", Saml2JwtView.as_view()),
   path("saml2/", include("djangosaml2.urls")),
]

 


And to get redirected to this view, add this line to settings.py

 

LOGIN_REDIRECT_URL = "/saml2/jwt/"

 

What just happened? Basically, you obtained a JWT token for the authenticated (session-based) user, set it to the cookie, and deleted the session as you didn’t need it. It was easy, wasn't it?

To increase security, it’s better to store refresh and auth token in the httpOnly cookie instead of local storage (Be aware that this option closes the XSS vulnerability but opens the CSRF). At the time of writing, library Django-rest-framework-simplejwt doesn’t deliver storing tokens in the cookies, this functionality can be found in one of the pull requests. If you don’t want to use cookie storage, as an option, you can just add these tokens as URL params to the redirected URL, instead of setting them into cookies.

Django app as an Identity Provider

In this case, things are even easier. Let’s say you have logged in into your site, you have set JWT cookie, and you want to perform IDP-initiated login to the different site. And here you are stuck again because to perform an IDP-initiated login, you have to have a Django session (SSOInitView is using LoginRequiredMixin), but as we are using stateless tokens, from the perspective of Django views, you are not authenticated:

@method_decorator(never_cache, name='dispatch')
class SSOInitView(LoginRequiredMixin, IdPHandlerViewMixin, View):
    """ View used for IDP initialized login, doesn't handle any SAML authn request
    """

This time you have to do the opposite: Authenticate Django view using JWT token. To implement this in the clean and reusable way, let’s create the view decorator:

from functools import wraps
 
from rest_framework_simplejwt.authentication import JWTAuthentication
 
 
def authenticate_by_jwt(view):
    """
    Authenticate django views by JWT token from cookies.
    """
 
    @wraps(view)
    def inner(request, *args, **kwargs):
        auth = JWTAuthentication()
        try:
            user, _ = auth.authenticate(request)
        except Exception:
            pass
        else:
            if user:
                request.user = user
        return view(request, *args, **kwargs)
    return inner

And decorate the SSO view:

from djangosaml2idp.views import SSOInitView
from apps.custom_auth.decorators import authenticate_by_jwt
urlpatterns = [
   path(
       "idp/sso/init/",
       authenticate_by_jwt(SSOInitView.as_view()),
       name="saml_idp_init",
   ),
   path("idp/", include("djangosaml2idp.urls")),
]

Also you can reuse this decorator to decorate the SLO view in the same way.

Conclusion

As you can see, The solution turned out to be easier than it seemed at first. And if the library doesn't provide needed functionality out of the box, it doesn't mean that you have to dig and rewrite everything. You can just do the adapter:)

Oleksandr Gorbunov
written by
Oleksandr Gorbunov

Skilled python enthusiast with 8 years of backend experience. Over the years, he has developed, both frontend and backend applications, no challenge is too big.

Let’s get the cooperation started!

See how we can help your business become more efficient

Get started!

Find us on