Enterprise SSO

Configure SAML and OIDC single sign-on connections.

Overview

Enterprise Single Sign-On (SSO) allows users to authenticate using their organization's identity provider (IdP). Transactional Auth supports both SAML 2.0 and OpenID Connect (OIDC) protocols.

Supported Protocols

ProtocolUse CaseCommon Providers
OIDCModern IdPs, social loginOkta, Auth0, Azure AD
SAML 2.0Enterprise IdPsOkta, Azure AD, OneLogin, ADFS

Pre-configured Providers

Quick setup for popular providers:

ProviderProtocolsSetup Time
OktaOIDC, SAML5 minutes
Azure ADOIDC, SAML5 minutes
Google WorkspaceOIDC3 minutes
Auth0OIDC, SAML5 minutes
OneLoginOIDC, SAML5 minutes

Creating an SSO Connection

Via Dashboard

  1. Go to Auth > SSO Connections
  2. Click Add Connection
  3. Select provider or protocol
  4. Follow the configuration wizard

Via API

TypeScript:

// OIDC Connection
const connection = await client.auth.connections.create({
  name: 'Acme Corp SSO',
  type: 'OIDC',
  provider: 'OKTA',
  config: {
    issuerUrl: 'https://acme.okta.com',
    clientId: 'your-okta-client-id',
    clientSecret: 'your-okta-client-secret',
    scopes: ['openid', 'profile', 'email'],
  },
  domains: ['acme.com'],
  jitProvisioning: true,
});

Python:

connection = client.auth.connections.create(
    name="Acme Corp SSO",
    type="OIDC",
    provider="OKTA",
    config={
        "issuer_url": "https://acme.okta.com",
        "client_id": "your-okta-client-id",
        "client_secret": "your-okta-client-secret",
        "scopes": ["openid", "profile", "email"],
    },
    domains=["acme.com"],
    jit_provisioning=True,
)

OIDC Configuration

Required Settings

SettingDescription
issuerUrlIdP's issuer URL (discovery endpoint)
clientIdOAuth client ID from IdP
clientSecretOAuth client secret from IdP

Optional Settings

SettingDescription
scopesAdditional scopes (default: openid, profile, email)
responseTypeOAuth response type
authorizationEndpointCustom auth endpoint (if no discovery)
tokenEndpointCustom token endpoint

Example: Okta OIDC

const connection = await client.auth.connections.create({
  name: 'Okta SSO',
  type: 'OIDC',
  provider: 'OKTA',
  config: {
    issuerUrl: 'https://your-domain.okta.com',
    clientId: '0oaxxxxxxxxxxxx',
    clientSecret: 'your-client-secret',
    scopes: ['openid', 'profile', 'email', 'groups'],
  },
});

SAML Configuration

Required Settings

SettingDescription
idpEntityIdIdP's entity ID
ssoUrlIdP's SSO URL
idpCertificateIdP's signing certificate (X.509)

Optional Settings

SettingDescription
sloUrlSingle Logout URL
requestBindingHTTP-POST or HTTP-Redirect
signRequestSign SAML requests
attributeMappingMap IdP attributes to user profile

Example: Azure AD SAML

const connection = await client.auth.connections.create({
  name: 'Azure AD SSO',
  type: 'SAML',
  provider: 'AZURE_AD',
  config: {
    idpEntityId: 'https://sts.windows.net/{tenant-id}/',
    ssoUrl: 'https://login.microsoftonline.com/{tenant-id}/saml2',
    idpCertificate: `-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAu...
-----END CERTIFICATE-----`,
    attributeMapping: {
      email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
      firstName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
      lastName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
    },
  },
});

Domain-Based Routing

Automatically route users to SSO based on email domain:

const connection = await client.auth.connections.create({
  // ... connection config
  domains: ['acme.com', 'acme.org'],
  domainVerified: true,
});

When a user enters user@acme.com:

  1. Check if domain has SSO connection
  2. Redirect to Acme's IdP
  3. User authenticates with IdP
  4. Return to your application

Verify Domain Ownership

// Get verification record
const verification = await client.auth.connections.getDomainVerification('conn_xxx', 'acme.com');
 
// Add TXT record to DNS
// _transactional-verify.acme.com TXT "verify-xxx"
 
// Verify
await client.auth.connections.verifyDomain('conn_xxx', 'acme.com');

Just-In-Time (JIT) Provisioning

Automatically create user accounts on first SSO login:

const connection = await client.auth.connections.create({
  // ... connection config
  jitProvisioning: true,
  defaultRole: 'MEMBER',
  defaultOrganizationId: 'org_xxx', // Optional
});

JIT provisioning will:

  1. Create a user account with IdP-provided attributes
  2. Link the SSO identity to the user
  3. Optionally add user to a default organization

Attribute Mapping

Map IdP attributes to user profile:

attributeMapping: {
  // Standard attributes
  email: 'email',
  firstName: 'given_name',
  lastName: 'family_name',
  displayName: 'name',
  picture: 'picture',
 
  // Custom attributes (stored in metadata)
  'metadata.department': 'department',
  'metadata.employeeId': 'employee_id',
}

Managing Connections

List Connections

const connections = await client.auth.connections.list();
 
for (const conn of connections) {
  console.log(`${conn.name}: ${conn.type} (${conn.status})`);
}

Get Connection

const connection = await client.auth.connections.get('conn_xxx');

Update Connection

await client.auth.connections.update('conn_xxx', {
  name: 'Updated Name',
  config: {
    scopes: ['openid', 'profile', 'email', 'groups'],
  },
});

Delete Connection

await client.auth.connections.delete('conn_xxx');

Enable/Disable Connection

// Disable (users can't log in via this connection)
await client.auth.connections.update('conn_xxx', {
  isActive: false,
});
 
// Enable
await client.auth.connections.update('conn_xxx', {
  isActive: true,
});

SSO Login Flow

Initiate SSO Login

// Option 1: By connection ID
const authUrl = `https://auth.usetransactional.com/sso/authorize?` + new URLSearchParams({
  connection_id: 'conn_xxx',
  client_id: 'your_client_id',
  redirect_uri: 'https://yourapp.com/callback',
});
 
// Option 2: By email domain (auto-discovery)
const authUrl = `https://auth.usetransactional.com/auth?` + new URLSearchParams({
  client_id: 'your_client_id',
  redirect_uri: 'https://yourapp.com/callback',
  login_hint: 'user@acme.com', // Domain triggers SSO
});

Handle SSO Callback

app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
 
  // Exchange code for tokens (same as regular OAuth)
  const tokens = await exchangeCode(code);
 
  // User info includes SSO identity
  const user = await getUserInfo(tokens.accessToken);
  console.log('SSO Provider:', user.identities[0]?.provider);
 
  req.session.user = user;
  res.redirect('/dashboard');
});

Organization SSO

Connect SSO to specific organizations:

// Create organization-specific SSO
const connection = await client.auth.connections.create({
  name: 'Acme Internal SSO',
  type: 'SAML',
  organizationId: 'org_xxx', // Specific to this org
  // ... config
});

Users who authenticate through this connection are automatically added to the organization.

Testing SSO

Test Connection

const result = await client.auth.connections.test('conn_xxx');
 
if (result.success) {
  console.log('Connection working');
  console.log('Sample user:', result.sampleUser);
} else {
  console.log('Connection failed:', result.error);
}

Test with Specific User

const result = await client.auth.connections.testUser('conn_xxx', {
  email: 'test@acme.com',
});

Troubleshooting

Common Issues

IssueCauseSolution
Invalid signatureCertificate mismatchRe-import IdP certificate
User not foundJIT disabledEnable JIT provisioning
Attribute missingMapping incorrectCheck attribute names
Redirect loopMisconfigured URIsVerify redirect URIs

Debug Mode

// Enable debug logging
const connection = await client.auth.connections.update('conn_xxx', {
  debugMode: true,
});
 
// Check logs
const logs = await client.auth.connections.getLogs('conn_xxx');

Next Steps