Implementing Just-In-Time (JIT) Provisioning with Logto and Email Domains

Logto's JIT provisioning automatically adds users to organizations and assigns roles on first sign-in when their email domain matches configured rules, using the organization_jit_email_domains table and REST API endpoints under /api/organizations/:id/jit/email-domains.

This guide walks through implementing automatic user provisioning in the logto-io/logto repository. By configuring email domain rules, you can enable seamless organization onboarding where users with corporate email addresses are instantly granted access without manual invitations.


How JIT Email Domain Provisioning Works

When JIT provisioning is enabled, Logto intercepts the authentication flow to evaluate whether a new or existing user should be auto-assigned to an organization based on their email domain. The flow executes in four stages:

  1. Domain Configuration: Administrators define allowed email domains (e.g., example.com) for specific organizations.
  2. Sign-In Interception: During the OIDC flow, Logto extracts the domain from the user's email identifier.
  3. Organization Lookup: The system queries the JIT email domain table to find matching organizations.
  4. Automatic Assignment: Upon a match, Logto creates the user-organization link and applies default roles before completing the sign-in process.

All logic resides in the core package, while the Admin Console provides a management interface for the underlying REST APIs.


Database Schema for JIT Email Domains

The persistence layer uses a dedicated table defined in packages/schemas/src/organization.sql. The organization_jit_email_domains table stores tuples of (organization_id, email_domain), enabling many-to-many relationships between organizations and allowed domains.

-- Schema location: packages/schemas/src/organization.sql
CREATE TABLE organization_jit_email_domains (
  organization_id VARCHAR(32) NOT NULL,
  email_domain VARCHAR(256) NOT NULL,
  PRIMARY KEY (organization_id, email_domain),
  FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
);

This schema ensures that domain rules are automatically cleaned up when an organization is deleted, maintaining referential integrity.


Query Layer Implementation

The EmailDomainQueries class in packages/core/src/queries/organization/email-domains.ts abstracts database interactions. It exposes methods that the user library and API routes consume:

  • getJitOrganizations(emailDomain): Retrieves all organizations that accept the specified domain.
  • insert(organizationId, emailDomain): Adds a new domain rule.
  • delete(organizationId, emailDomain): Removes a specific domain rule.
  • replace(organizationId, emailDomains[]): Performs bulk replacement of domain rules for an organization.

These methods handle the SQL generation and parameterization, ensuring safe queries against the JIT email domains table.


REST API Endpoints for JIT Management

Logto exposes full CRUD operations under the base path /api/organizations/:id/jit/email-domains, implemented in packages/core/src/routes/organization/jit/email-domains.ts.

Method Endpoint Description
GET / Lists configured email domains with pagination support.
POST / Adds a single domain via JSON body { emailDomain: string }.
PUT / Replaces all domains with the provided array { emailDomains: string[] }.
DELETE /:emailDomain Removes a specific domain rule from the organization.

The router is mounted within the JIT sub-router at packages/core/src/routes/organization/jit/index.ts, ensuring consistent authentication and authorization middleware.


Sign-In Flow Integration

The critical integration point occurs in packages/core/src/libraries/user.ts. During the sign-in process, Logto calls organizations.jit.emailDomains.getJitOrganizations() to determine automatic organization membership.

// Located in packages/core/src/libraries/user.ts
const userEmailDomain = email.split('@')[1];
const jitOrgs = await organizations.jit.emailDomains.getJitOrganizations(userEmailDomain);

if (jitOrgs.length > 0) {
  // Auto-join logic executes here
  for (const org of jitOrgs) {
    await organizationMemberships.addUserToOrg(user.id, org.id, org.defaultRoles);
  }
}

This execution happens transparently within the OIDC flow, ensuring users are provisioned before the session is established.


Configuring JIT via the Admin Console

The Admin Console consumes these APIs to provide a visual interface for JIT configuration. The relevant utilities in packages/console/src/pages/OrganizationDetails/utils.ts handle state management and API data transformation.

Administrators can:

The console ensures that domain validation and role assignment are configured through the same REST endpoints available to programmatic clients.


Implementation Examples

Adding an Email Domain via API

Use the Management API to programmatically add domains:

import fetch from 'node-fetch';

async function addJitDomain(organizationId: string, domain: string, token: string) {
  const response = await fetch(
    `http://localhost:3002/api/organizations/${organizationId}/jit/email-domains`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify({ emailDomain: domain })
    }
  );
  
  if (!response.ok) {
    throw new Error(`Failed to add domain: ${response.statusText}`);
  }
  
  return response.json();
}

// Usage
await addJitDomain('org_12345', 'example.com', 'YOUR_ADMIN_TOKEN');

This corresponds to the POST handler in packages/core/src/routes/organization/jit/email-domains.ts.

Simulating the Sign-In Check

To verify which organizations a user would join, replicate the library logic:

// Simulates packages/core/src/libraries/user.ts logic
async function checkJitEligibility(email: string, queries: any) {
  const domain = email.split('@')[1];
  const matchedOrgs = await queries.jit.emailDomains.getJitOrganizations(domain);
  
  return {
    eligible: matchedOrgs.length > 0,
    organizations: matchedOrgs
  };
}

Removing a Domain Rule

Delete rules using the domain-specific endpoint:

async function removeJitDomain(organizationId: string, domain: string, token: string) {
  const encodedDomain = encodeURIComponent(domain);
  await fetch(
    `http://localhost:3002/api/organizations/${organizationId}/jit/email-domains/${encodedDomain}`,
    {
      method: 'DELETE',
      headers: { 'Authorization': `Bearer ${token}` }
    }
  );
}

Testing JIT Provisioning

The integration test suite validates end-to-end JIT functionality. Key test files include:

These tests create organizations, configure JIT rules, simulate sign-ins with matching and non-matching emails, and assert that membership records are created correctly.


Summary

Implementing JIT provisioning with Logto and email domains requires coordination across several layers:

This architecture enables zero-touch onboarding for organizations by simply maintaining a list of approved email domains.


Frequently Asked Questions

What happens if a user's email domain matches multiple organizations?

Logto returns all matching organizations from getJitOrganizations() and provisions the user into each one. The user library in packages/core/src/libraries/user.ts iterates through all matches and assigns the respective default roles for each organization.

Can I use wildcard or subdomain matching for JIT email domains?

Based on the current implementation in packages/core/src/queries/organization/email-domains.ts, the matching performs exact domain string comparison. To support subdomains like sub.example.com, you must explicitly add each domain or implement custom logic in your query layer.

How do I set default roles for JIT-provisioned users?

Default roles are configured through the Admin Console or Management API alongside email domains. When getJitOrganizations() returns matching records, the response includes the organization's default roles, which the user library applies via organizationMemberships.addUserToOrg() during the sign-in flow.

Is JIT provisioning triggered for existing users or only new registrations?

The JIT logic in packages/core/src/libraries/user.ts executes during the sign-in flow regardless of whether the user is new or existing. If an existing user signs in with a newly configured email domain, Logto will automatically create the organization membership at that time, provided one does not already exist.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →