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:
- Domain Configuration: Administrators define allowed email domains (e.g.,
example.com) for specific organizations. - Sign-In Interception: During the OIDC flow, Logto extracts the domain from the user's email identifier.
- Organization Lookup: The system queries the JIT email domain table to find matching organizations.
- 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:
- View and modify allowed email domains.
- Select default roles to assign to JIT-provisioned users.
- Access documentation via links defined in
packages/console/src/consts/external-links.ts.
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:
packages/integration-tests/src/tests/api/organization/organization-jit.test.ts: Covers CRUD operations for email domains and role assignments.packages/integration-tests/src/tests/api/experience-api/register-interaction/organization-jti.test.ts: Verifies that users are automatically joined to organizations during the registration interaction flow.
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:
- Database: The
organization_jit_email_domainstable stores domain-to-organization mappings. - Queries:
EmailDomainQueriesinpackages/core/src/queries/organization/email-domains.tshandles data access. - API: REST endpoints in
packages/core/src/routes/organization/jit/email-domains.tsprovide management interfaces. - Sign-In Logic:
packages/core/src/libraries/user.tsexecutes automatic provisioning during authentication. - Admin UI: The Console provides visual management tools backed by these APIs.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →