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. This will start a SuperTokens subscription.
If you are using the development environment of our managed service you can use this feature for free. If you want to enable this feature for the production environment, please email us
Integration Steps
#
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
Instead, if you are following the example guide, run:
yarn dev
in the root directory of the project and this will start the SAML jackson server on http://localhost:5225
(along with a SuperTokens core + postgresql server)
#
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", {
thirdPartyEnabled: true,
});
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 TenantConfig
async def some_func():
result = await create_or_update_tenant("public", TenantConfig(
third_party_enabled=True,
))
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 TenantConfig
result = create_or_update_tenant("public", TenantConfig(
third_party_enabled=True,
))
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
curl --location --request PUT '/recipe/multitenancy/tenant' \
--header 'api-key: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"tenantId": "customer1",
"thirdPartyEnabled": true
}'
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: "SAML Login",
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: "SAML Login",
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="SAML Login",
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="SAML Login",
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": "SAML Login",
"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": "SAML Login",
"clients": [
{
"clientId": "<TODO: GENERATED FROM PREVIOUS STEP>",
"clientSecret": "<TODO: GENERATED FROM PREVIOUS STEP>",
"additionalConfig": {
"boxyURL": "http://localhost:5225"
}
}
]
}
}'
- 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 examlpe, 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.