Paid Feature
This is a paid feature.
For self hosted users, Sign up to get a license key and follow the instructions sent to you by email. Creation of tenants is free on the dev license key.
This feature is already enabled for managed service users. Creation of additional tenant is free on the provided development environment.
Integration Steps
#
Using Dashboardcaution
Currently, this is only available with our Node and Python SDKs.
#
1) Generate the XML metadata file from your SAML providerYour SAML provider will allow you to download a .xml
file that you can upload to SAML Jackson. During this process, you will need to provide it:
- the SSO URL and;
- the Entity ID.
You can learn more about these in the SAML Jackson docs.
In our example app, we use mocksaml.com which is a free SAML provider that we can use for testing. When you navigate to the site, you will see a "Download metadata" button which you should click on to get the .xml
file.
#
2) Start the SAML Jackson serviceYou can run SAML Jackson with or without Docker.
docker run \
-p 5225:5225 \
-e JACKSON_API_KEYS="secret" \
-e DB_ENGINE="sql" \
-e DB_TYPE="postgres" \
-e DB_URL="postgres://postgres:postgres@postgres:5432/postgres" \
-d boxyhq/jackson
This will start the SAML jackson server on http://localhost:5225
.
important
If you are using our SuperTokens managed service, we will host the Boxy HQ server for you (reach out to us to activate your instance).
#
3) Create a new tenant in SuperTokens (if not done already)#
4) Configure the SAML provider for the tenantTo configure SAML login with SuperTokens, ensure that you use the correct provider name in the third-party config. Make sure to specify provider name with one of the following:
- Microsoft Entra ID
- Microsoft AD FS
- Okta
- Auth0
- Google
- OneLogin
- PingOne
- JumpCloud
- Rippling
- SAML
Make sure to replace http://localhost:5225
with the correct value for where you have hosted the boxy hq server. If you are using our SuperTokens managed service, we will host the Boxy HQ server for you (reach out to us to activate your instance).
success
You have now successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users, but we have two common UX flows documented in the "Common UX flows" section.
#
5) Adding multiple SAML connections to a single tenantIf you have just one SAML connection for a tenant, then the Third Party Id
for that connection can be boxy-saml
. This will also display a single "SAML Login" button on our pre built UI.
Whilst this works well, if you want to add a second SAML connection for the same tenant, you need to follow the same steps as above, but also use the Add Suffix
option for the Third Party Id.
For example, if a tenant has Active Directory and Okta login (both with SAML), you can create the Active Directory provider using "boxy-saml"
as the Third Party Id
, and for Okta, you could use okta
as a suffix to make the Third Party Id
equal to "boxy-saml-okta"
. You can also give them different names - instead of "SAML Login" (that's shown above), you can use "Active Directory" and "Okta" so that the button on the pre built UI shows the right name.
#
Via Boxy API#
1) Generate the XML metadata file from your SAML providerYour SAML provider will allow you to download a .xml
file that you can upload to SAML Jackson. During this process, you will need to provide it:
- the SSO URL and;
- the Entity ID.
You can learn more about these in the SAML Jackson docs.
In our example app, we use mocksaml.com which is a free SAML provider that we can use for testing. When you navigate to the site, you will see a "Download metadata" button which you should click on to get the .xml
file.
.xml
file to base64#
2) Convert the You can use an online base64 encoder to do this. First copy the contents of the .xml
file, and then put it in the encoder tool. The output string will be the base64 version of the .xml file.
For example, with an input .xml
file (obtained from mocksaml.com):
<?xml version="1.0" encoding="UTF-8" standalone="no"?><EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://saml.example.com/entityid" validUntil="2026-06-22T18:39:53.000Z"><IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><KeyDescriptor use="signing"><KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><X509Data><X509Certificate>MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV
SzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4
MjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK
DAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0
RuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd
4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V
pwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b
2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ
NfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF
AAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW
5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4
khuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX
UjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L
r/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M
m0eo2USlSRTVl7QHRTuiuSThHpLKQQ==
</X509Certificate></X509Data></KeyInfo></KeyDescriptor><NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat><SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://mocksaml.com/api/saml/sso"/><SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://mocksaml.com/api/saml/sso"/></IDPSSODescriptor></EntityDescriptor>
The base64 output is:
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PEVudGl0eURlc2NyaXB0b3IgeG1sbnM6bWQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDptZXRhZGF0YSIgZW50aXR5SUQ9Imh0dHBzOi8vc2FtbC5leGFtcGxlLmNvbS9lbnRpdHlpZCIgdmFsaWRVbnRpbD0iMjAyNi0wNi0yMlQxODozOTo1My4wMDBaIj48SURQU1NPRGVzY3JpcHRvciBXYW50QXV0aG5SZXF1ZXN0c1NpZ25lZD0iZmFsc2UiIHByb3RvY29sU3VwcG9ydEVudW1lcmF0aW9uPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxLZXlEZXNjcmlwdG9yIHVzZT0ic2lnbmluZyI+PEtleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxYNTA5RGF0YT48WDUwOUNlcnRpZmljYXRlPk1JSUM0akNDQWNvQ0NRQzMzd255YlQ1UVpEQU5CZ2txaGtpRzl3MEJBUXNGQURBeU1Rc3dDUVlEVlFRR0V3SlYKU3pFUE1BMEdBMVVFQ2d3R1FtOTRlVWhSTVJJd0VBWURWUVFEREFsTmIyTnJJRk5CVFV3d0lCY05Nakl3TWpJNApNakUwTmpNNFdoZ1BNekF5TVRBM01ERXlNVFEyTXpoYU1ESXhDekFKQmdOVkJBWVRBbFZMTVE4d0RRWURWUVFLCkRBWkNiM2g1U0ZFeEVqQVFCZ05WQkFNTUNVMXZZMnNnVTBGTlREQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dFUEFEQ0NBUW9DZ2dFQkFMR2ZZZXR0TXNjdDFUNnRWVXdUdWROSkg1UG5iOUdHbmtYaTlady9lNng0NUREMApSdVJPTmJGbEoyVDRSakFFL3VHK0FqWHhYUThvMlNaZmI5K0dnbUNIdVRKRk5nSG9aMW5GVlhDbWIvSGc4SHBkCjR2T0FHWG5kaXhhUmVPaXEzRUg1WHZwTWpNa0ozKzgrOVZZTXpNWk9qa2dRdEFxTzM2ZUFGRmZOS1g3ZFRqM1YKcHdMa3Z6Ni9LRkNxOE9Bd1krQVVpNGVabTVKNTdEMzFHempId2ZqSDlXVGVYME15bmRtbk5CMXFWNzVxUVIzYgoyL1c1c0dIUnYrOUFhcmdnSmtGK3B0VWtYb0x0VkE1MXdjZlltNmhJTHB0cGRlNUZRQzhSV1kxWXJzd0JXQUVaCk5meXJSNEplU3dlRWxOSGc0TlZPczRUd0dqT1B3V0dxelRmZ1RsRUNBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0YKQUFPQ0FRRUFBWVJsWWZsU1hBV29acEZmd05pQ1FWRTVkOXpaMERQek5kV2hBeWJYY1R5TWYwejVtRGY2RldCVwo1R3lvaTl1M0VNRURuekxjSk5rd0pBQWMzOUFwYTRJMi90bWwrSnkyOWRrOGJUeVg2bTkzbmdtQ2dkTGg1WmE0CmtodVUzQU0zTDYzZzdWZXhDdU83a3dramgvK0xxZGNJWHNWR082WERmdTJRT3MxWHBlOXpJekxwd20vUk5ZZVgKVWpiU2o1Y2UvamVrcEF3N3F5VlZMNHhPeWg4QXRVVzFlazN3SXcxTUp2RWdFUHQwZDE2b3NoV0pwb1MxT1Q4TApyLzIyU3ZZRW8zRW1TR2RUVkdnazN4M3MrQTBxV0FxVGN5anI3UTRzL0dLWVJGZm9tR3d6MFRaNEl3MVpOOTlNCm0wZW8yVVNsU1JUVmw3UUhSVHVpdVNUaEhwTEtRUT09CjwvWDUwOUNlcnRpZmljYXRlPjwvWDUwOURhdGE+PC9LZXlJbmZvPjwvS2V5RGVzY3JpcHRvcj48TmFtZUlERm9ybWF0PnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzczwvTmFtZUlERm9ybWF0PjxTaW5nbGVTaWduT25TZXJ2aWNlIEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVJlZGlyZWN0IiBMb2NhdGlvbj0iaHR0cHM6Ly9tb2Nrc2FtbC5jb20vYXBpL3NhbWwvc3NvIi8+PFNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vbW9ja3NhbWwuY29tL2FwaS9zYW1sL3NzbyIvPjwvSURQU1NPRGVzY3JpcHRvcj48L0VudGl0eURlc2NyaXB0b3I+
#
3) Start the SAML Jackson serviceYou can run SAML Jackson with or without Docker.
docker run \
-p 5225:5225 \
-e JACKSON_API_KEYS="secret" \
-e DB_ENGINE="sql" \
-e DB_TYPE="postgres" \
-e DB_URL="postgres://postgres:postgres@postgres:5432/postgres" \
-d boxyhq/jackson
This will start the SAML jackson server on http://localhost:5225
.
important
If you are using our SuperTokens managed service, we will host the Boxy HQ server for you (reach out to us to activate your instance).
#
4) Upload the base64 xml string to SAML JacksonTake the string generated in step (2) and run:
curl --location --request POST 'http://localhost:5225/api/v1/saml/config' \
--header 'Authorization: Api-Key secret' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'encodedRawMetadata=<STRING_FROM_STEP_2>' \
--data-urlencode 'defaultRedirectUrl=<REDIRECT_URI>' \
--data-urlencode 'redirectUrl=["<REDIRECT_URI>"]' \
--data-urlencode 'tenant=<TENANT_ID>' \
--data-urlencode 'product=<PRODUCT_NAME>' \
--data-urlencode 'name=demo-config' \
--data-urlencode 'description=Demo SAML config'
You can learn more about the config values in the SAML Jackson docs.
For our example app, you can see this command here. This is provided in a helper script called addTenant.sh
. You can run it like:
./addTenant.sh <TENANT_ID>
# example
./addTenant.sh customer1
./addTenant.sh customer2
The output of this command will provide you the client_id
and client_secret
for this tenant. You will need to provide these values to SuperTokens for this tenant when configuring this tenant's providers (see below).
#
5) Create a new tenant in SuperTokens (if not done already)- NodeJS
- GoLang
- Python
- cURL
Important
import Multiteancy from "supertokens-node/recipe/multitenancy";
async function createTenant() {
let resp = await Multiteancy.createOrUpdateTenant("customer1", {
firstFactors: ["thirdparty"]
});
if (resp.createdNew) {
// new tenant was created
} else {
// existing tenant's config was modified.
}
}
import (
"github.com/supertokens/supertokens-golang/recipe/multitenancy"
"github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels"
)
func main() {
tenantId := "customer1"
thirdPartyEnabled := true
resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{
ThirdPartyEnabled: &thirdPartyEnabled,
})
if err != nil {
// handle error
}
if resp.OK.CreatedNew {
// new tenant was created
} else {
// existing tenant's config was modified.
}
}
- Asyncio
- Syncio
from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
async def some_func():
result = await create_or_update_tenant(
"public", TenantConfigCreateOrUpdate(first_factors=["thirdparty"])
)
if result.status != "OK":
print("handle error")
elif result.created_new:
print("new tenant was created")
else:
print("existing tenant's config was modified.")
from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant
from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate
def some_func():
result = create_or_update_tenant(
"public", TenantConfigCreateOrUpdate(first_factors=["thirdparty"])
)
if result.status != "OK":
print("handle error")
elif result.created_new:
print("new tenant was created")
else:
print("existing tenant's config was modified.")
- Single app setup
- Multi app setup
- Core version >= 9.1.0
- Core version <= 9.0.2
curl --location --request PUT '/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"firstFactors": ["thirdparty"]
}'
curl --location --request PUT '/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"thirdPartyEnabled": true
}'
- Core version >= 9.1.0
- Core version <= 9.0.2
curl --location --request PUT '/recipe/multitenancy/tenant/v2' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"firstFactors": ["thirdparty"]
}'
curl --location --request PUT '/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"thirdPartyEnabled": true
}'
#
6) Configure the SAML provider for the tenant- NodeJS
- GoLang
- Python
- cURL
Important
import Multitenancy from "supertokens-node/recipe/multitenancy"
async function addThirdPartyConfigToTenant() {
let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", {
thirdPartyId: "boxy-saml",
name: "<provider-name>",
clients: [{
clientId: "<TODO: GENERATED FROM PREVIOUS STEP>",
clientSecret: "<TODO: GENERATED FROM PREVIOUS STEP>",
additionalConfig: {
"boxyURL": "http://localhost:5225",
}
}]
});
if (resp.createdNew) {
// SAML Login added to customer1
} else {
// Existing SAML Login config overwritten for customer1
}
}
import (
"github.com/supertokens/supertokens-golang/recipe/multitenancy"
"github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels"
)
func main() {
tenantId := "customer1"
resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{
ThirdPartyId: "boxy-saml",
Name: "<provider-name>",
Clients: []tpmodels.ProviderClientConfig{
{
ClientID: "<TODO: GENERATED FROM PREVIOUS STEP>",
ClientSecret: "<TODO: GENERATED FROM PREVIOUS STEP>",
AdditionalConfig: map[string]interface{}{
"boxyURL": "http://localhost:5225",
},
},
},
}, nil)
if err != nil {
// handle error
}
if resp.OK.CreatedNew {
// SAML Login added to customer1
} else {
// Existing SAML Login config overwritten for customer1
}
}
- Asyncio
- Syncio
from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config
from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig
async def some_func():
result = await create_or_update_third_party_config("customer1", ProviderConfig(
third_party_id="boxy-saml",
name="<provider-name>",
clients=[
ProviderClientConfig(
client_id="<TODO: GENERATED FROM PREVIOUS STEP>",
client_secret="<TODO: GENERATED FROM PREVIOUS STEP>",
additional_config={
"boxyURL": "http://localhost:5225",
}
),
],
))
if result.status != "OK":
print("handle error")
elif result.created_new:
print("SAML Login added to customer1")
else:
print("Existing SAML Login config overwritten for customer1")
from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config
from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig
result = create_or_update_third_party_config("customer1", ProviderConfig(
third_party_id="boxy-saml",
name="<provider-name>",
clients=[
ProviderClientConfig(
client_id="<TODO: GENERATED FROM PREVIOUS STEP>",
client_secret="<TODO: GENERATED FROM PREVIOUS STEP>",
additional_config={
"boxyURL": "http://localhost:5225",
}
),
],
))
if result.status != "OK":
print("handle error")
elif result.created_new:
print("SAML Login added to customer1")
else:
print("Existing SAML Login config overwritten for customer1")
- Single app setup
- Multi app setup
curl --location --request PUT '/<TENANT_ID>/recipe/multitenancy/config/thirdparty' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"config": {
"thirdPartyId": "boxy-saml",
"name": "<provider-name>",
"clients": [
{
"clientId": "<TODO: GENERATED FROM PREVIOUS STEP>",
"clientSecret": "<TODO: GENERATED FROM PREVIOUS STEP>",
"additionalConfig": {
"boxyURL": "http://localhost:5225"
}
}
]
}
}'
curl --location --request PUT '/<TENANT_ID>/recipe/multitenancy/config/thirdparty' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"config": {
"thirdPartyId": "boxy-saml",
"name": "<provider-name>",
"clients": [
{
"clientId": "<TODO: GENERATED FROM PREVIOUS STEP>",
"clientSecret": "<TODO: GENERATED FROM PREVIOUS STEP>",
"additionalConfig": {
"boxyURL": "http://localhost:5225"
}
}
]
}
}'
To configure SAML login with SuperTokens, ensure that you use the correct provider name in the third-party config.
Make sure to replace <provider-name>
in the code snippet above with one of the following
- Microsoft Entra ID
- Microsoft AD FS
- Okta
- Auth0
- Google
- OneLogin
- PingOne
- JumpCloud
- Rippling
- SAML
Make sure to replace http://localhost:5225
with the correct value for where you have hosted the boxy hq server. If you are using our SuperTokens managed service, we will host the Boxy HQ server for you (reach out to us to activate your instance).
success
You have now successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users, but we have two common UX flows documented in the "Common UX flows" section.
#
7) Adding multiple SAML connections to a single tenantIf you have just one SAML connection for a tenant, then the thirdPartyId
for that connection can be boxy-saml
. This will also display a single "SAML Login" button on our pre built UI.
Whilst this works well, if you want to add a second SAML connection for the same tenant, you need to follow the same steps as above, but instead of using "boxy-saml"
as the thirdPartyId
, you need to set it to another value that starts with "boxy-saml"
(for step 6).
For example, if a tenant has Active Directory and Okta login (both with SAML), you can create the Active Directory provider using "boxy-saml"
as the thirdPartyId
, and for Okta, you could use "boxy-saml-okta"
(it's important that the string starts with "boxy-saml"
). You can also give them different names - instead of "SAML Login" (that's shown above), you can use "Active Directory" and "Okta" so that the button on the pre built UI shows the right name.