A compact, all-in-one SAML test service provider (SP) built with Flask. It provides a simple identity-provider portal and separate Okta and Microsoft Entra test paths. Use it to observe SAML redirects, authentication flows, and assertion handling.
This repository is an integration and learning tool, not a production identity service. Read Security and Limitations before using it with real accounts or identity providers.
- Presents Okta and Microsoft Entra sign-in buttons from one Flask page.
- Initiates an Okta IDP-initiated SAML flow by redirecting to the configured Okta app URL, where Okta posts its
SAMLResponsedirectly to the ACS. - Starts an Entra SP-initiated SAML flow by generating an
AuthnRequestand redirecting to the tenant's SAML endpoint. - Receives Okta and Entra's HTTP-POST responses at dedicated ACS routes.
- Provides a local logout button on each response page that clears the Flask session and returns to the portal.
- Keeps the minimal HTML and alternate Python server examples available for experimentation.
- Open the Flask portal and select Okta SAML Sign In.
- The browser navigates to the Okta URL hard-coded in
app.py. This is an IDP-initiated flow. - Okta authenticates the user (if necessary) and posts its
SAMLResponsedirectly to the configured ACS. In this project,/acsbase64-decodes and displays the XML for inspection; it does not validate that response.
Configure the Okta app's ACS/Single sign-on URL to the public /acs URL if you intend to use this demo route. Update OKTA_APP_EMBED_LINK in app.py to your Okta application launch URL.
- Open the portal on the public HTTPS ngrok URL and select Entra SAML Sign In.
/login/entracreates a SAMLAuthnRequestand redirects the browser tohttps://login.microsoftonline.com/<tenant-id>/saml2. This is an SP-initiated flow.- Entra authenticates the user and posts a
SAMLResponseto/acs/entra. - The handler passes the response to
python3-samland shows a result page. Use Log out to clear this app's Flask session and return to the portal.
The Entra flow is SP-initiated. The configured Entra Sign-on URL must start the flow at /login/entra; the ACS URL is only for receiving the response and must not be used as the sign-on URL.
| Route | Method | Purpose |
|---|---|---|
/ |
GET | Identity portal with Okta and Entra buttons |
/login/entra |
GET | Creates and sends an Entra SAML AuthnRequest (SP-initiated) |
/acs/entra |
POST | Entra SAML assertion consumer endpoint |
/acs |
POST | Okta/demo ACS that decodes and displays the submitted XML (IDP-initiated) |
/logout |
POST | Clears the local Flask session and redirects to / |
| File | Purpose |
|---|---|
app.py |
Primary Flask portal, Entra SAML flow, Okta demo flow, and logout |
requirements.txt |
Flask and python3-saml dependencies |
index.html |
Separate static portal experiment; it is not served by the Flask app |
saml_sp.py |
Older standalone Flask ACS example; it is not the primary app |
server.py |
Static file server alternative; it uses port 8000 like app.py |
Run app.py for the combined portal. Do not run server.py or saml_sp.py at the same time on port 8000.
- Python 3.9 or newer
- The packages listed in
requirements.txt - An Okta SAML application for the Okta demo, and/or an Entra enterprise application configured for SAML
- ngrok or another public HTTPS tunnel when an identity provider must reach the local ACS
From the repository directory, create and activate a virtual environment and install dependencies:
python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txtThe python3-saml package installs its XML and XML-signature dependencies as well.
For the Okta IDP-initiated flow, ensure your Okta SAML app is configured with:
| Okta field | Value |
|---|---|
| Single sign-on URL (Assertion Consumer Service URL) | The public HTTPS URL of /acs (e.g., https://<ngrok-host>/acs) |
Update OKTA_APP_EMBED_LINK in app.py to your Okta application launch URL. This URL is what the Flask portal redirects to, initiating the IDP-initiated flow.
Start ngrok in one terminal and copy its current HTTPS forwarding hostname:
ngrok http 8000In Entra, open the enterprise application and configure Single sign-on > SAML. Set the fields to match the Flask app:
| Entra field | Value |
|---|---|
| Identifier (Entity ID) | Exactly the value of SAML_SP_ENTITY_ID |
| Reply URL (Assertion Consumer Service URL) | Exactly the value of SAML_ACS_URL; path must be /acs/entra |
| Sign-on URL | https://<current-ngrok-host>/login/entra |
| Relay State | Leave blank unless you have a specific use for it |
| Logout URL | Leave blank; SAML Single Logout is not implemented |
| Signing option | Sign SAML response and assertion is supported; the app requires the assertion to be signed |
Assign the test user to the enterprise application. Copy the Entra Issuer, Login URL, and active Certificate (Base64) from the SAML configuration or federation metadata. The tenant-based values must match the environment variables set when you run the Flask app.
Set the environment variables in the PowerShell terminal where Flask will run. Replace the placeholders with your tenant values and current ngrok hostname:
$env:ENTRA_TENANT_ID = "<your-tenant-guid>"
$env:ENTRA_IDP_ENTITY_ID = "https://sts.windows.net/<your-tenant-guid>/"
$env:ENTRA_SAML_SSO_URL = "https://login.microsoftonline.com/<your-tenant-guid>/saml2"
$env:ENTRA_IDP_CERT = "<Base64 signing certificate from Entra>"
$env:SAML_SP_ENTITY_ID = "<same Identifier configured in Entra>"
$env:SAML_ACS_URL = "https://<current-ngrok-host>/acs/entra"
$env:FLASK_SECRET_KEY = (& .\.venv\Scripts\python.exe -c "import secrets; print(secrets.token_urlsafe(32))")Set ENTRA_IDP_CERT using either of these approaches:
- Put the Base64 certificate body on one line, removing PEM header/footer lines and all line breaks.
- Save the complete PEM certificate (including its
BEGIN CERTIFICATEandEND CERTIFICATElines) to a file, then load the file contents into the variable with PowerShell:$env:ENTRA_IDP_CERT = (Get-Content -Raw <path-to-file>).
The variable must contain the certificate text, not the file path. Loading the complete PEM with Get-Content -Raw avoids manually pasting multiline text into a quoted PowerShell assignment. The environment variable handles both formats.
ENTRA_TENANT_ID has a project default. ENTRA_IDP_ENTITY_ID and ENTRA_SAML_SSO_URL are derived from that tenant if not set, but should match Entra's displayed SAML values. ENTRA_IDP_CERT, SAML_SP_ENTITY_ID, and SAML_ACS_URL are required.
Start the Flask app in that same terminal:
python app.pyFor Entra, browse to https://<current-ngrok-host>/ rather than http://localhost:8000/. The SAML request is correlated using the Flask session cookie; starting on localhost and returning to the ngrok HTTPS URL will not match the session.
When ngrok changes its hostname, update SAML_ACS_URL, the Entra Reply URL, and the Entra Sign-on URL together. Keep SAML_SP_ENTITY_ID identical to the Entra Identifier. A stable Entity ID can be configured in Entra to avoid frequent updates.
The Log out buttons submit to /logout, which clears the local Flask session and returns the browser to /. This is not an Entra or Okta Single Logout flow. The identity provider's own browser session remains active; signing in again may not prompt for credentials.
This project is intended for controlled testing only. Do not rely on its result pages to authenticate users or authorize access to protected resources.
The Entra browser flow has been verified end to end with the configured tenant. One implementation limitation remains: the Entra route calls auth.process_response(request_id), but the follow-up validation methods are not implemented. The response is processed and the attributes are displayed, but do not rely on this for production authentication.
Other limitations:
- The Okta
/acsroute only base64-decodes and displays XML. It does not verify signatures, issuer, audience, destination, timestamps, or replay. - The app has no user database, authorization policy, or protected application area.
- The Flask session is client-side signed session data, not a server-side session store. A generated fallback secret changes on restart; set
FLASK_SECRET_KEYto a stable random value for a test session that survives app restarts. - SAML Single Logout, IdP-initiated Entra login, and SP metadata publishing are not implemented.
- The session cookie is configured as Secure and SameSite=None, with a Partitioned attribute. Test SAML through HTTPS, such as the ngrok URL.
- SAML assertions can contain personal information. Do not share raw SAML responses, browser traces, or logs containing identity data.
Install the SAML-tracer browser extension when investigating a SAML flow. It is a diagnostic aid, not a runtime requirement. Start a capture, reproduce the sign-in, and inspect the redirects and POST requests. SAML-tracer shows the encoded/decoded AuthnRequest and SAMLResponse without client-side JavaScript processing.
SAML-tracer captures browser traffic; it does not expose Flask's internal exception details. When the app displays its generic SAML-processing error, also inspect the terminal running python app.py for the traceback.
Never use this project as-is for production authentication or authorization.
| Symptom | Checks |
|---|---|
/login/entra returns 503 |
Confirm ENTRA_IDP_CERT, SAML_SP_ENTITY_ID, and SAML_ACS_URL were set in the same terminal before starting Flask |
| Entra reports Reply URL mismatch | The Entra Reply URL must exactly match SAML_ACS_URL, including HTTPS, hostname, path, and trailing slash behavior; the path is /acs/entra |
| Entra returns an error before posting a response | Check Identifier, Sign-on URL (/login/entra), tenant SAML configuration, and user assignment |
Okta redirects but /acs says login request is missing |
Confirm the Okta app's ACS/Single sign-on URL is set to the public /acs endpoint and matches OKTA_APP_EMBED_LINK in app.py |
| ACS says the login request is missing or expired | Start login and callback on the same ngrok HTTPS hostname; check that browser cookies are enabled and FLASK_SECRET_KEY stays unchanged during the flow |
| SAML response processing fails | Check the Flask terminal for the exception, and verify the Entra Issuer and active signing certificate match the values configured in the environment |
| Certificate/signature errors | Confirm ENTRA_IDP_CERT contains the active Entra signing certificate as one continuous Base64 line, then capture the flow with SAML-tracer to check the browser's submitted value |
| Port 8000 is already in use | Stop the other local server or configure both Flask and ngrok to use a different matching port |
There is no dedicated automated test suite in this repository. The Flask test client can be used for route-level checks, but a real SAML round trip requires matching Entra configuration, certificates, and network routing.