SAML
Add SAML authentication to your application
Overview
The following guide shows you how to configure SAML with your SuperTokens integration.
SAML, or Security Assertion Markup Language, is an open protocol that exchanges information between the authentication server and the client application.
How does it work?
Your SAML identity provider (IdP) has a metadata file (.xml) that you or your end users need to upload to the SAML service provider (SP).
The .xml metadata file contains (amongst other things):
- A unique entity ID that identifies the IdP. It is an identifier, not a secret, and may be shared as part of SAML metadata.
- A public certificate that verifies the signature attached to the incoming
SAMLresponse. This ensures the response is coming from the expected Identity Provider. - Information about where to redirect the end user to when they click on the login button in your application.
This URL is to a website controlled by the
SAMLprovider and asks the end user for their credentials.
Before you start
Steps
1. Get the SAML metadata from your identity provider
Before configuring SuperTokens, you need to obtain the SAML metadata XML from your identity provider (IdP). This is typically available in your IdP’s admin console as a downloadable XML file or a metadata URL.
Common locations for metadata:
- Azure AD: Enterprise Applications > Your App > Single sign-on > Federation Metadata XML
- Okta: Applications > Your App > Sign On > SAML Metadata
- Google Workspace: Apps > Web and mobile apps > Your App > Download metadata
2. Initialize the SAML recipe in the backend SDK
import SuperTokens from "supertokens-node";
import Saml from "supertokens-node/recipe/saml";
SuperTokens.init({
supertokens: {
connectionURI: "<SUPERTOKENS_CONNECTION_URI>",
apiKey: "<SUPERTOKENS_API_KEY>",
},
appInfo: {
appName: "App name",
apiDomain: "<API_DOMAIN>",
websiteDomain: "<WEBSITE_DOMAIN>",
},
recipeList: [
// other recipes
Saml.init(),
],
});from supertokens_python import InputAppInfo, SupertokensConfig, init
from supertokens_python.recipe import saml
init(
supertokens_config=SupertokensConfig(
connection_uri="<SUPERTOKENS_CONNECTION_URI>",
api_key="<SUPERTOKENS_API_KEY>",
),
app_info=InputAppInfo(
app_name="App name",
api_domain="<API_DOMAIN>",
website_domain="<WEBSITE_DOMAIN>",
),
framework="fastapi",
recipe_list=[
# Other recipes
saml.init(),
],
)3. Create a new SAML client
Use the metadata XML obtained in step 1 to create a SAML client.
The redirectURIs should point to your application’s callback URL where users will be redirected after authentication.
import Saml from "supertokens-node/recipe/saml";
async function createSamlClient() {
const result = await Saml.createOrUpdateClient({
tenantId: "<TENANT_ID>",
clientId: "<CLIENT_ID>",
clientSecret: "<CLIENT_SECRET>",
redirectURIs: ["https://your-app.com/auth/callback"],
defaultRedirectURI: "https://your-app.com/auth/callback",
metadataXML: "<METADATA_XML_FROM_IDP>",
allowIDPInitiatedLogin: true,
enableRequestSigning: true,
});
if (result.status === "OK") {
// Save the clientId for use in the ThirdParty provider configuration
console.log("Client ID:", result.clientId);
}
}from supertokens_python.recipe.saml.asyncio import create_or_update_client
async def create_saml_client():
result = await create_or_update_client(
tenant_id="<TENANT_ID>",
client_id="<CLIENT_ID>",
client_secret="<CLIENT_SECRET>",
redirect_uris=["https://your-app.com/auth/callback"],
default_redirect_uri="https://your-app.com/auth/callback",
metadata_xml="<METADATA_XML_FROM_IDP>",
allow_idp_initiated_login=True,
enable_request_signing=True,
)
if result.status == "OK":
# Save the client ID for use in the ThirdParty provider configuration
print("Client ID:", result.client.client_id)| Name | Type | Description | Required |
|---|---|---|---|
tenantId |
string |
The unique identifier of the tenant for which the SAML client is being created or updated. | Yes |
clientId |
string |
The unique identifier for the SAML client. If provided, updates the existing client; if omitted, creates a new client. | No |
clientSecret |
string |
The secret key associated with the SAML client for authentication purposes. | No |
redirectURIs |
string[] |
An array of URIs where the user agent should be redirected after successful authentication. | Yes |
defaultRedirectURI |
string |
The default URI to redirect to if no specific redirect URI is specified. | Yes |
metadataXML |
string |
The SAML metadata XML string containing configuration details for the Identity Provider. | Yes |
allowIDPInitiatedLogin |
boolean |
A flag indicating whether login requests initiated by the Identity Provider are allowed. | No |
enableRequestSigning |
boolean |
A flag indicating whether SAML requests should be digitally signed for security. | No |
userContext |
Record<string, any> |
An optional object containing additional context or metadata for the operation. | No |
4. Configure your identity provider
After creating the SAML client, you need to configure your identity provider with your application’s service provider (SP) details.
On the IdP side, configure the following properties:
- Entity ID: Should match the
saml_sp_entity_idvalue used in your tenant configuration. The default value ishttps://saml.supertokens.com. - ACS URL (Assertion Consumer Service URL):
<API_DOMAIN>/auth/<TENANT_ID>/saml/callback
5. Add the ThirdParty provider
Update your SuperTokens initialization to include the ThirdParty recipe with your SAML provider.
The thirdPartyId must start with saml- followed by your custom identifier.
import SuperTokens from "supertokens-node";
import Saml from "supertokens-node/recipe/saml";
import ThirdParty from "supertokens-node/recipe/thirdparty";
SuperTokens.init({
supertokens: {
connectionURI: "<SUPERTOKENS_CONNECTION_URI>",
apiKey: "<SUPERTOKENS_API_KEY>",
},
appInfo: {
appName: "App name",
apiDomain: "<API_DOMAIN>",
websiteDomain: "<WEBSITE_DOMAIN>",
},
recipeList: [
Saml.init(),
ThirdParty.init({
signInAndUpFeature: {
providers: [
{
config: {
// Name that will be shown on the login page
name: "Azure SAML",
// Must start with "saml-"
thirdPartyId: "saml-azure",
clients: [
{
// The clientId from step 3
clientId: "<CLIENT_ID>",
},
],
},
},
],
},
}),
],
});from supertokens_python import InputAppInfo, SupertokensConfig, init
from supertokens_python.recipe import saml, thirdparty
from supertokens_python.recipe.thirdparty import (
ProviderClientConfig,
ProviderConfig,
ProviderInput,
SignInAndUpFeature,
)
init(
supertokens_config=SupertokensConfig(
connection_uri="<SUPERTOKENS_CONNECTION_URI>",
api_key="<SUPERTOKENS_API_KEY>",
),
app_info=InputAppInfo(
app_name="App name",
api_domain="<API_DOMAIN>",
website_domain="<WEBSITE_DOMAIN>",
),
framework="fastapi",
recipe_list=[
saml.init(),
thirdparty.init(
sign_in_and_up_feature=SignInAndUpFeature(
providers=[
ProviderInput(
config=ProviderConfig(
# Must start with "saml-"
third_party_id="saml-azure",
# Name shown on the login page
name="Azure SAML",
clients=[
ProviderClientConfig(
# The client ID from step 3
client_id="<CLIENT_ID>",
)
],
)
)
]
)
),
],
)6. Test rejection paths before production
Use an isolated test tenant and test IdP to verify that invalid SAML responses fail closed. At minimum, confirm that authentication is rejected for:
- A response that has neither a valid response signature nor a valid signature on every assertion, including an assertion modified after signing and a response containing a duplicate or wrapping assertion.
- An assertion with a future
NotBeforevalue or an expiredNotOnOrAftervalue.
Core 12.1.1 is not expected to reject a mismatched Response.Destination, and its audience validation is insufficient for responses containing multiple assertions. If a trusted upstream validator supplies the missing controls, test destination and per-assertion audience rejection at that layer before production.
Do not disable signature or time-condition validation to make negative tests pass. Keep failure details in server-side logs and return a generic authentication error to the browser.
Map identity provider attributes
By default, SuperTokens picks the user ID and email from the SAML assertion with built-in rules. If your identity
provider sends them in other claims, or you want the user’s first and last name, set an attributeMapping on the SAML
client. It requires Core 12.5.0 or later.
The backend SDKs don’t pass attributeMapping yet, so set it by calling the core directly. The request replaces the
whole client, so send the client’s other fields as well:
curl --location --request PUT '<CORE_API_ENDPOINT>/<TENANT_ID>/recipe/saml/clients' \
--header 'api-key: <YOUR_API_KEY>' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"clientId": "<CLIENT_ID>",
"redirectURIs": ["https://your-app.com/auth/callback"],
"defaultRedirectURI": "https://your-app.com/auth/callback",
"metadataXML": "<METADATA_XML_FROM_IDP>",
"attributeMapping": {
"userId": ["employeeId", "NameID"],
"email": ["mail"],
"firstName": ["givenName"],
"lastName": ["sn"]
}
}'
Each key is optional and takes an ordered list of 1 to 10 claim names. For each field, SuperTokens uses the first claim
in the list that is present with a non-blank value. NameID stands for the assertion’s Subject NameID. For email,
a NameID only counts if it contains @. Claim names match exactly, so they can’t have leading or trailing spaces.
An invalid mapping is rejected with a 400 that names the key.
| Key | Used as | Default when the key is left out |
|---|---|---|
userId |
The user’s ID from the identity provider | NameID, http://schemas.microsoft.com/identity/claims/objectidentifier, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name |
email |
The user’s email | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, mail, email, NameID |
firstName |
firstName in the user info |
None: only returned when mapped |
lastName |
lastName in the user info |
None: only returned when mapped |
The mapping is returned by the client’s PUT response and by GET <CORE_API_ENDPOINT>/<TENANT_ID>/recipe/saml/clients/list.
The core’s BoxyHQ-compatible legacy endpoint /recipe/saml/legacy/userinfo uses the same mapping for the id it
returns.
Clients without an attributeMapping keep the built-in rules unchanged. If those rules would give a client a different
user ID or email than the default lists above (for example, because the assertion’s NameID is empty), the core logs a
warning with the app, tenant, client and field, but never the claim values. Use it to check whether a client needs a
mapping before you set one.
Update a client that has an attribute mapping
Once a client has an attributeMapping or acsURLs, a PUT that leaves the key out doesn’t clear it:
- From CDI
5.8, leaving out a key that’s set is rejected with a400. Send the current value to keep it, ornull({}forattributeMapping,[]foracsURLs) to clear it. - Below CDI
5.8, which the current SDKs use, the stored value is kept. Updating a client through the SDK keeps its mapping.