visit
It’s a good exercise in case your third-party service does not support SSO authentication (or you just don’t want to pay much for it) - but you would be happy with a simple solution. All you need is Python, Docker, and, surely, an understanding of what you are going to do. Don’t use this solution in production as it is :)
In my case, the service required a JWT token as a part of the URL to the content: //dest-url.com/content/?jwt_token=<token>
. So I simply had to build a redirect URL with a JWT token after all operations on my side. Also, JWT tokens can be passed in different ways, but anyway, it’s not rocket science to figure out how to include them in your resulting request.
Read
permission will be enough);
We are going to connect JWT and AD authentication via a dockerized Python application deployed on our side. It will be based on Flask and the Microsoft Authentication Library for Python (MSAL). Below, you will find a list of vital Python libraries you will need:
flask
: overall application architecture and endpoint design;msal
: authentication workflow;jwt
: generating JWT tokens based on third-party service JWT secrets;fernet
: additional encryption of a user-side session cookie.
//python-authentication-service.com/
. If they try to access the destination service directly, it will also redirect them for authentication via this fallback URL.
The easiest way to achieve this is to use the Flask session
library and save all the stuff on the user side. In this case, all replicas of the authentication service will retain access to this shared cookie. Flow encryption here is used only to provide an additional security layer: since it’s a user-side cookie, we can rely on lightweight encryption algorithms.
Alternatively, if you deploy the service in a singleton pattern, you would not need session cookies. Moreover, you can play hard and create shared session storage on the server side, e.g., with the use of Redis or alternative solutions.
/src
├── application.py
└── config.py
Dockerfile
requirements.txt
Flask>=2.3.2,<3
msal>=1.22.0,<2
requests>=2.31.0,<3
fernet>=1.0.1
MSAL_CLIENT_ID = #Your AD application (client) ID
MSAL_CLIENT_SECRET = #Your AD application secret
MSAL_AUTHORITY = #//login.microsoftonline.com/your-tenant-id
AD_SCOPES = #List of scopes defined when creating the AD application (for testing purposes, can be empty: '[]')
AUTH_SERVICE_URL = #URL of this authentication service (e.g. //python-authentication-service.com/ as above)
JWT_SECRET = #JWT secret generated by the destination service
JWT_EXPIRATION_SECONDS = #Desired TTL for the destination service JWT token
FLASK_SECRET = #Built-in Flask cookie secret key to encrypt the session cookie with
FERNET_SECRET = #Additional Fernet secret key to encrypt the flow object
TARGET_URL = #URL of the destination service to redirect into with the resulting JWT token (e.g. //dest-url.com/content/ as above)
from flask import Flask, session, request, redirect
import msal
import jwt
from fernet import Fernet
import json
import time
import config
app = Flask(__name__)
msal_app = msal.ConfidentialClientApplication(
client_id=config.MSAL_CLIENT_ID,
authority=config.MSAL_AUTHORITY,
client_credential=config.MSAL_CLIENT_SECRET,
)
fe = Fernet(bytes(config.FERNET_SECRET, encoding='utf8'))
Since we are going to use the Flask session
library to store session cookies, we also have to define the cookie secret we added to the config before:
app.config.update(SECRET_KEY=config.FLASK_SECRET, ENV='development')
/
- root endpoint to start the authentication flow and create the corresponding object with the authorization key./getadtoken
- auxiliary authorization endpoint to validate authentication flow and create an AD authentication token. When done, this endpoint proceeds to the core stuff: generates the destination service JWT token, appends it to the destination URL, and redirects the user.
@app.route('/')
def root():
flow = msal_app.initiate_auth_code_flow(config.AD_SCOPES, redirect_uri=config.AUTH_SERVICE_URL + '/getadtoken')
session['flow'] = fe.encrypt(json.dumps(flow).encode())
return redirect(flow['auth_uri'], code=302)
Here you can see that we create the session
object and add the encrypted flow
inside. The session will be stored on the user side and shared between the application replicas, as I’ve mentioned before.
The flow
object also contains the authentication URI on the Azure side to redirect the user for authentication.
Proceeding to the /getadtoken
endpoint:
@app.route('/getadtoken')
def getadtoken():
if 'flow' not in session:
return 'Unauthorised', 403
flow = json.loads(fe.decrypt(session['flow']).decode())
saml_result = msal_app.acquire_token_by_auth_code_flow(flow, request.args)
if not saml_result or 'error' in saml_result:
return 'Unauthorised', 403
jwt_token = jwt.encode({'data': 'empty', 'exp': int(time.time()) + config.JWT_EXPIRATION_SECONDS}, config.JWT_SECRET, algorithm='RS256') #NB: your encoding algorithm may differ.
return redirect(config.TARGET_URL + '?jwt_token=' + jwt_token, code=302)
Here, first of all, we have to check if the flow
object is inside the Flask session to prevent direct access to the /getadtoken
endpoint. In other words, if the user does not have any session cookie stored, they are unauthenticated and cannot proceed.
Then, we decrypt the flow
object and pass it to generate the AD authentication token. The authorization code contained in the flow is validated on the Azure side, so if there are any problems in the resulting object, the application returns 403
.
FROM python:3.11.3-alpine
RUN apk update
WORKDIR /app
COPY requirements.txt ./
RUN pip install requirements.txt
COPY ./src .
CMD ["python", "./application.py"]
As you can see, I’ve described the simplest possible approach to implement the Azure AD authentication on your side. It highlights the core principles and can be paired with many third-party services supporting JWT authentication. There are many ways to improve; let me show some to you: