Identity federation: trusting another organization's IdP without managing their users

Identity federation lets Organization A's users authenticate with Organization A's identity provider to access a service run by Organization B, without Organization B needing to maintain any user credentials. A government contractor can authenticate their employees into a vendor portal using their corporate Active Directory. A university can let students access a third-party learning platform using their campus credentials. The vendor and the platform never touch the users' passwords or manage their lifecycle — they simply trust the external IdP's assertions.

Trust establishment

Before federation can work, the two parties must establish a trust relationship. In SAML federations, this involves exchanging metadata: each party gets the other's entity ID, signing certificate, and endpoint URLs. The metadata tells your service which certificate to use to validate assertions from that specific IdP, and tells the IdP where to send assertions.

For bilateral federation (two organizations directly), this is typically a manual process: IT administrators from both sides exchange metadata XML files or URLs. For multilateral federation (many organizations in a trust framework), a federation hub or broker maintains a registry of all participants and their metadata, which each party downloads and trusts collectively. This is how InCommon (US academic federation), edugain (international academic federation), and many government federations operate.

<!-- IdP metadata — what your service needs from the partner org's IdP -->
<EntityDescriptor
  entityID="https://idp.partnerorg.com/saml/metadata"
  xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
  <IDPSSODescriptor
    protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <KeyDescriptor use="signing">
      <ds:KeyInfo>
        <ds:X509Data>
          <ds:X509Certificate>
            MIIEGTCCAwGgAwIBAgIJALz8gD...
          </ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </KeyDescriptor>
    <SingleSignOnService
      Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
      Location="https://idp.partnerorg.com/saml/sso"/>
  </IDPSSODescriptor>
</EntityDescriptor>

Store each federated IdP's metadata in your database, keyed by entity ID. When a SAML assertion arrives, look up the IdP by the assertion's issuer, retrieve its stored certificate, and validate the signature. Never trust an assertion without looking up the issuer's expected signing certificate.

Attribute mapping across federation boundaries

Each organization has its own identity data model. One org sends first name as givenName, another as urn:oid:2.5.4.42 (the OID used in academic federations), and a third as a custom attribute named FirstName. Your service needs to map all of these to your internal user model.

// Federation attribute mapping — per-connection configuration
interface FederationConnection {
  entity_id: string;
  display_name: string;
  attribute_mapping: {
    // Your field: their attribute name
    email: string;
    first_name: string;
    last_name: string;
    employee_id?: string;
    groups?: string;
    department?: string;
  };
  // Static attributes to add for all users from this federation
  static_attributes?: Record<string, string>;
}

// Example connections for different partner types
const connections: FederationConnection[] = [
  {
    entity_id: 'https://idp.university.edu/saml',
    display_name: 'State University',
    attribute_mapping: {
      // Academic federation uses OIDs from eduPerson schema
      email: 'urn:oid:0.9.2342.19200300.100.1.3',      // mail
      first_name: 'urn:oid:2.5.4.42',                   // givenName
      last_name: 'urn:oid:2.5.4.4',                     // sn
      groups: 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'       // eduPersonEntitlement
    }
  },
  {
    entity_id: 'https://sts.windows.net/tenant-id/',
    display_name: 'Partner Corp (Azure AD)',
    attribute_mapping: {
      email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
      first_name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
      last_name: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
      groups: 'http://schemas.microsoft.com/ws/2008/06/identity/claims/groups'
    },
    static_attributes: { org_type: 'partner' }
  }
];

Account linking

Federation introduces the question: when a federated user authenticates for the first time, what account do they get in your system? There are three options:

  • Just-in-time provisioning: create a new account automatically using the attributes from the assertion. Simple, but creates duplicate accounts if the same person previously had a local account.
  • Link to existing account: if an account with a matching email already exists, link the federated identity to it. Requires verifying that the email in the assertion is confirmed by the external IdP.
  • Manual account linking: create the federated account and prompt the user to optionally link it to an existing local account by entering their local password. Most secure, but adds friction.
// Account linking on first federated login
async function handleFederatedLogin(
  federatedUser: FederatedUserAttributes,
  connection: FederationConnection
): Promise<User> {
  // Check for existing linked account
  const existingLink = await db.federatedIdentities.findOne({
    provider_entity_id: connection.entity_id,
    provider_user_id: federatedUser.nameId
  });

  if (existingLink) {
    return db.users.findById(existingLink.user_id);
  }

  // Check for existing account with same email (JIT linking)
  const emailMatch = await db.users.findByEmail(federatedUser.email);

  if (emailMatch && emailMatch.email_verified) {
    // Link the federated identity to the existing account
    await db.federatedIdentities.insert({
      user_id: emailMatch.id,
      provider_entity_id: connection.entity_id,
      provider_user_id: federatedUser.nameId,
      linked_at: new Date()
    });
    return emailMatch;
  }

  // Create new account via JIT provisioning
  const newUser = await db.users.insert({
    email: federatedUser.email,
    first_name: federatedUser.firstName,
    last_name: federatedUser.lastName,
    email_verified: true,  // trusted because assertion came from verified IdP
    provisioned_by: connection.entity_id,
    created_at: new Date()
  });

  await db.federatedIdentities.insert({
    user_id: newUser.id,
    provider_entity_id: connection.entity_id,
    provider_user_id: federatedUser.nameId,
    linked_at: new Date()
  });

  return newUser;
}
When linking a federated identity to an existing local account based on email address, ensure the email in the assertion comes from a source you trust to verify email ownership. An IdP that allows users to self-declare their email address without verification could be used to hijack another user's account through email-based linking.

Attribute release policies

In academic and government federations, IdPs may not release all attributes to all service providers. A university's IdP might release email and name to all SP members, but only release student ID numbers to services that have signed a data use agreement. You need to handle cases where expected attributes are absent without crashing — and in some cases, the missing attribute may mean the user is not authorized to access your service at all.

Design your federation attribute processing to distinguish between required attributes (without which you cannot provision or authenticate the user) and optional attributes (which enrich the profile but are not required for access). Log when expected optional attributes are absent — this helps debug federation configuration issues without blocking users.

← Back to blog Try Bastionary free →