You are working in a company and need to set up a Single Sign-On (SSO) system on Azure with FastAPI? You don’t find what you need in the Azure Official documentation? You’ve come to the right place!

In this comprehensive guide, we’ll walk through the entire process of setting up Azure SSO with FastAPI, from configuring your Microsoft Entra ID application to implementing the authentication flow in your FastAPI application.

Note: Microsoft has renamed Azure Active Directory (Azure AD) to Microsoft Entra ID. This guide will use the new terminology, but you may still encounter the old names in some documentation and online articles.

Understanding the Authentication Flow

Before diving into the implementation, it’s important to understand how the whole authentication process works. When a user attempts to access your FastAPI application, they’ll be redirected to Microsoft’s login page. After successful authentication, Microsoft Entra ID returns an authorization code to your application, which you then exchange for access tokens. These tokens contain user information and can be validated to ensure the request is legitimate. If your user is already logged in, they won’t even see these redirections. Here is the complete flow below :

Diagram of login process with Azure SSO and FastAPI
Diagram of SSO Login Process

The beauty of this approach is that your application never handles user passwords directly. All authentication is managed by Microsoft’s secure infrastructure, and you simply validate the tokens they provide.

Step 1: Setting up Azure

1.1 Create the app registration

First, we need to set up an app registration before implementing SSO on our application. In a production setting, it is advised to do this using IaC (Infrastructure-as-code) pipelines, but for this tutorial, we will skip this part and do this manually.

The app registration will be the identity of your application in Azure. They can be used for a variety of purposes, such as role-based access for Azure resources. Here we will only use it for SSO. Select app registration in the Azure Portal and click “Register an application”.

The app registration logo in Azure

To create you application, simply choose a name and select the type of supported accounts. This is rather self-explanatory as you can see below.

Screenshot showing how to create a new app registration on Azure

The Redirect URI is the more interesting part. Here, you will define the URL that Azure will be allowed to use after the login process. This is a security measure used by Azure to ensure that your tokens cannot be stolen with a man-in-the-middle attack to some other address.

The Redirect URI type for this tutorial will be Web, which is the generic solution for a web API.

After this, you can then create your app registration.

1.2 Optional: Add more Redirect URLs

Note: After creation, it will be possible to list multiple Redirect URIs. This is useful for listing a local URL for development and a production URL, for example. Simply go to the authentication tab and click “Add URI”, add a new URL, and click “Save”.

Screenshot showing how to add a new redirect URL on Azure

1.3 Create a secret

Ok we are nearly done, we just have to create a client secret. This will act as a password to use the app registration in our application.

  1. Click on the “Certificates & Secrets” tab
  2. Under the Client secrets section, click “+ New client secret.”
  3. Enter a description (e.g., API access key) and choose an expiration period
  4. Click “Add.”
  5. Copy the value immediately — it will only be shown once.
Screenshot showing how to create a new client secret on Azure

Again, in practice, you would do this using some kind of IaC pipeline and store the secret in a key vault after creating it automatically. Also, client secrets need to be renewed, so don’t forget to schedule a moment in time to renew them or set up an automatic renewal process via IaC.

1.4 Collect the client ID & Tenant ID

Go to the overview page and get the Client ID and Tenant ID (also called Directory ID). You now have the 3 strings that we will need to connect to Azure.

Step 2: Pip install the requirements

Okay, now, let’s set up our project. First, create a requirements.txt file with the following content:

fastapi
httpx
python-dotenv
python-jose
msal # This is for azure authentification
uvicorn

Then, install them using pip:

pip install -r requirements.txt

Step 3: Implementing the FastAPI Application

Time to code! We will create a single main.py file for this tutorial. In practice, you may want to separate the auth code and routes in a different file with the FastAPI router feature, but this is out of the scope of this article.

3.1 Imports and Important Initializations

First, we will need to import a bunch of things :

import httpx
import secrets
import os
from jose import jwt, JWTError
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.responses import RedirectResponse, JSONResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from dotenv import load_dotenv
import msal
from starlette.middleware.session import SessionMiddleware

Then, we will have to define a few elements, as we can see in the code below. What matters most is the msal_client, which will represent the app registration in code. Also note that we add SessionMiddleware to the middlewares of FastAPI. This will come in handy later.

AZURE_CLIENT_ID = # Retrieved from previous step
AZURE_CLIENT_SECRET = # Retrieved from previous step 
AZURE_TENANT_ID = # Retrieved from previous step
AZURE_REDIRECT_URI = # The redirect URL you defined in the app registration
SECRET_KEY = # Random value you choose for securing the session 
AUTHORITY = f"https://login.microsoftonline.com/{AZURE_TENANT_ID}"
SCOPE = ["User.Read"] # This scope will be used to say ( we want to access user info)

app = FastAPI()

# Add session middleware
# NOTE: In production, ensure 'https_only=True' and 'samesite="none"' for cross-site SSO redirects.
app.add_middleware(
    SessionMiddleware,
    secret_key=SECRET_KEY,
    https_only=True, 
    samesite="none"     
)

# Initialize MSAL Confidential Client
msal_client = msal.ConfidentialClientApplication(
    client_id=AZURE_CLIENT_ID,
    authority=AUTHORITY,
    client_credential=AZURE_CLIENT_SECRET,
)

3.2 Login & Callback endpoints

Now let’s write the basic authorization endpoints that will allow users to log in using Microsoft Entra ID (Azure AD) and handle the OAuth callback.

The route /login will redirect the user to the Microsoft login page. After logging in to their Microsoft account, the user will be redirected to /login/callback. The exact callback link was provided in /login with the variable AZURE_REDIRECT_URI. If the user is already logged in to Microsoft, then the user won’t even see the redirection front-end side.

In the /login/callback, we exchange our code for a user token. Again, this is a security measure to avoid sending the token over the internet. Notice that we then store our token in the user session. This is essential; otherwise, the token will only exist until the next redirection. Without this, we can enter into an infinite redirection loop.

At the end, the callback route redirects the user to the protected route. This is a simplified implementation. A more flexible approach is to store the address the user tried to reach in the session. Then, retrieve it in the callback to ensure we can come back where we came from.

# --- API Endpoints ---

@app.get("/login")
async def login(request: Request):
    """
    Initiate the Microsoft Entra ID login flow.
    Stores the state in the session.
    """
    state = secrets.token_urlsafe(32)
    request.session["state"] = state

    authorization_url = msal_client.get_authorization_request_url(
        scopes=SCOPE,
        state=state,
        redirect_uri=AZURE_REDIRECT_URI
    )
    return RedirectResponse(url=authorization_url)

@app.get("/login/callback")
async def callback(code: str, state: str, request: Request):
    """
    Handle the OAuth callback from Microsoft Entra ID.
    Stores the user's token and info in the session.
    """
    # Verify the state to prevent CSRF attacks
    if state != request.session.get("state"):
        raise HTTPException(status_code=400, detail="Invalid state parameter")
    
    request.session.pop("state", None)

    token_response = msal_client.acquire_token_by_authorization_code(
        code=code,
        scopes=SCOPE,
        redirect_uri=AZURE_REDIRECT_URI
    )

    if "error" in token_response:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=token_response.get("error_description", "Failed to acquire token")
        )
    
    # The id_token contains user claims
    id_token_claims = jwt.decode(
        token_response['id_token'],
        options={"verify_signature": False} # Signature already verified by MSAL/Entra ID
    )
    
    # Store user information in the session
    request.session["user"] = {
        "id": id_token_claims.get("oid"),
        "name": id_token_claims.get("name"),
        "email": id_token_claims.get("preferred_username"),
        "roles": id_token_claims.get("roles", []),
    }

    return RedirectResponse(url="/protected")

3.3 Create dependencies

Let’s create a few utility functions. We will use these as dependencies with FastAPI’s Depends feature. It allows FastAPI to automatically resolve and inject the results of these utilities where needed, making your code more modular and cleaner.

These functions will simply try to get the user object or roles from the current session. If it doesn’t exist, it will raise an error flag as a 302 HTTPException. This will be sent to the web browser, which will understand that it needs to do a redirection. In this case, to the /login endpoint.

# --- Authentication Logic ---
# (We don't need manual token verification with sessions, but this can be useful for other purposes)

# --- Dependencies ---
def require_auth(request: Request):
    """
    Dependency to protect an endpoint.
    Raises an exception if the user is not authenticated.
    """
    user = request.session.get("user")
    if not user:
        response = RedirectResponse(url="/login", status_code=302)
        raise HTTPException(
            status_code=response.status_code, 
            headers=response.headers
            )
    return user

def require_roles(required_roles: List[str]):
    """
    Dependency factory to check for required roles in the session.
    """
    def role_checker(
        user: Dict[str, Any] = Depends(require_auth)
    ):
        user_roles = user.get("roles", [])
        if not any(role in user_roles for role in required_roles):
            response = RedirectResponse(url="/login", status_code=302)
            raise HTTPException(
                status_code=response.status_code, 
                headers=response.headers
                )
        return user
    return role_checker

3.4 Let’s create some actual routes

Great, now that we have the authorization logic handled, we can use it to serve useful endpoints in our applications. Here are a few examples that require a user token, a specific role, or no logging at all. Notice how we use the FastAPI Depends() feature to seamlessly integrate authentication in our endpoints.

@app.get("/protected")
async def protected_endpoint(
    user = Depends(require_auth)
):
    """
    A protected endpoint that requires authentication via session.
    """

    return {
        "message": f"Hello, {user.get('name')}! This is protected data.",
        "user_details": user
    }

@app.get("/roleProtected", dependencies=[Depends(require_roles(["Admin"]))])
async def role_protected_endpoint(
    user = Depends(require_auth)
):
    """
    An endpoint protected by role 'Admin'.
    """

    return {
        "message": f"Welcome, Admin {user.get('name')}!",
        "detail": "You have access to this role-protected data."
    }

@app.get("/unprotected")
async def unprotected_endpoint():
    """
    An unprotected endpoint that anyone can access.
    """
    return {"message": "This is an unprotected endpoint."}

if __name__ == "__main__":
    import uvicorn
    # This is for development only. In production, use a proper ASGI server.
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

An Important Note on Session Cookies and SSO

We use Starlette’s SessionMiddleware for session management. When a user logs in, their identity information will be stored in a secure, signed cookie.

During the SSO process, the user is redirected from your application to Microsoft’s login page and then back. From the browser’s perspective, this is a cross-site request. For the session cookie to be correctly handled after this redirect, it must be configured with SameSite=None and https_only=True. This tells the browser that the cookie can be sent in cross-origin contexts, but only over HTTPS.

Tip 1: When SameSite=None is used, browsers will silently refuse to set the cookie if the connection is not secure (i.e., not HTTPS). This can be a huge pain to debug because your authentication flow will simply fail without an obvious error. When deploying to Azure or any other production environment, ensure you are using HTTPS.

Tip 2: This can make it difficult to work locally; you will need to use an HTTPS URL for local development as well. In some cases, some browsers will allow you to disable this security for development purposes.

Tip 3: Be careful when constructing your response objects in FastAPI. If you create a new Response object instead of letting FastAPI manage it, you might accidentally wipe out the Set-Cookie header that the SessionMiddleware adds, effectively logging the user out. It’s best to modify request.session and let the middleware handle the cookie management.

Step 4: Testing Your Implementation

Now that everything is set up, let’s test the authentication flow. Start your FastAPI application:

python main.py

Or using uvicorn directly:

uvicorn main:app --reload

Your application should now be running. You can test the authentication flow:

  1. Navigate to the address in your browser
  2. You’ll be redirected to Microsoft’s login page
  3. Sign in with your Microsoft Entra ID credentials
  4. After successful authentication, you’ll be redirected back to your callback endpoint
  5. The callback will return your user information and tokens
  6. You should now have access to your application

Common pitfalls

  • Cookies reset: Be careful that your browser doesn’t reset your cookies. Make sure you use the right parameters when creating cookies as discussed above.
  • Testing in http, not https: If you test on a local http://localhost url. Your cookies won’t be saved. This is because this isn’t an HTTPS URL.
  • Token Expiry: Tokens have an expiration date. Don’t use the same token for too long when testing.

Judicael Poumay (Ph.D.)
Index