External Identities
External Identities
Section titled “External Identities”Manage links between Mantis users and external identity provider accounts.
Overview
Section titled “Overview”External identities link Mantis user accounts to identity provider accounts, enabling SSO authentication and user lifecycle management.
External Identity Model
Section titled “External Identity Model”| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
user_id | UUID | Linked Mantis user |
provider_id | UUID | Identity provider reference |
external_user_id | String | User ID at IdP (sub claim) |
external_username | String? | Username at IdP |
external_email | String? | Email at IdP |
access_token_encrypted | String? | Encrypted access token |
refresh_token_encrypted | String? | Encrypted refresh token |
token_expires_at | DateTime? | Token expiration |
last_login_at | DateTime? | Last SSO login via this link |
created_at | DateTime | Link creation time |
How External Identities Work
Section titled “How External Identities Work”Identity Linking Flow
Section titled “Identity Linking Flow”Multiple Identities
Section titled “Multiple Identities”A single Mantis user can have multiple external identities:
This allows users to:
- Login via different IdPs
- Migrate between IdPs
- Use personal and work accounts
Viewing External Identities
Section titled “Viewing External Identities”User Details
Section titled “User Details”User details include linked external identities:
curl -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"Response:
{ "id": "019b937d-f30f-8946-ab14-f85563b8c7e4", "email": "alice@company.com", "username": "alice", "external_identities": [ { "provider_id": "019b937d-e394-85ca-a36e-f75e92a53aff", "provider_name": "Okta", "external_username": "alice.smith", "external_email": "alice.smith@company.com", "last_login_at": "2024-01-15T10:30:00Z" }, { "provider_id": "019b937d-1d8f-8d66-a6cc-5f5f84ac1d10", "provider_name": "Azure AD", "external_username": "asmith", "external_email": "asmith@company.onmicrosoft.com", "last_login_at": "2024-01-10T08:15:00Z" } ]}Direct Query
Section titled “Direct Query”SELECT ei.id, u.email AS mantis_email, ip.name AS provider_name, ei.external_username, ei.external_email, ei.last_login_atFROM external_identities eiJOIN users u ON u.id = ei.user_idJOIN identity_providers ip ON ip.id = ei.provider_idWHERE u.email = 'alice@company.com';Identity Link Scenarios
Section titled “Identity Link Scenarios”Automatic Linking by Email
Section titled “Automatic Linking by Email”When a user logs in via SSO, Mantis checks for an existing user by email — but
linking to that existing account is not automatic. It requires all four
of the following, and otherwise fails closed with a 403. They are checked in
this order, and the first one to fail is the one you are told about:
- the provider has
auto_create_usersenabled — otherwise403 User auto-creation is disabled for this provider, before the address is looked up at all, - the existing account is in the provider’s own tenant — an address held outside
it is refused with
403 Email is already associated with an account outside this provider's tenant, - the provider has
allow_email_linking = true, and - the ID token asserts
email_verified: true.
Conditions 3 and 4 failing gives 403 Email is already associated with another account, naming which of the two was the blocker.
:::caution System-wide providers cannot provision A system-wide (NULL-tenant) provider never creates a new user. Provisioning a NULL-tenant account on every external login would silently grant cross-tenant visibility, so only a tenant-scoped provider can create one. A system-wide IdP can still authenticate an already-linked user, and can link to an existing account by email — the user’s tenant is already fixed in that case. :::
Manual Linking
Section titled “Manual Linking”Currently, external identity links are created automatically during SSO. To manually create a link:
INSERT INTO external_identities ( id, user_id, provider_id, external_user_id, external_username, external_email, created_at) VALUES ( gen_random_uuid(), (SELECT id FROM users WHERE email = 'alice@company.com'), (SELECT id FROM identity_providers WHERE name = 'Okta'), 'okta_external_sub_id', 'alice.smith', 'alice.smith@company.com', NOW());Unlinking Identities
Section titled “Unlinking Identities”When to Unlink
Section titled “When to Unlink”- User leaving organization
- Migrating to different IdP
- Security incident response
- Account cleanup
Via the API
Section titled “Via the API”The account’s links, with each provider_id, are in GET /api/v1/admin/users/$USER_ID
under external_identities. Removing one needs users:update and is audited as
user.external_identity_unlinked. It also revokes the roles the provider’s group
mappings granted, in either role sync mode, and leaves roles granted by hand; that needs
roles:manage and the authority to confer each of them, and is audited as
user.roles_removed:
curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ "$API_URL/api/v1/admin/users/$USER_ID/external-identities/$PROVIDER_ID"Token Storage
Section titled “Token Storage”Encrypted Tokens
Section titled “Encrypted Tokens”IdP tokens are stored encrypted for potential future use:
| Token | Purpose |
|---|---|
access_token | API calls to IdP (user info, etc.) |
refresh_token | Obtaining new access tokens |
Tokens are encrypted using AES-256-GCM with provider-specific AAD:
let aad = format!("external_identity:{}:{}", provider_id, external_user_id);let encrypted = encryptor.encrypt_string(token, &aad)?;Token Refresh
Section titled “Token Refresh”On each SSO login, tokens are updated:
UPDATE external_identitiesSET access_token_encrypted = $new_access_token, refresh_token_encrypted = $new_refresh_token, token_expires_at = $expiration, last_login_at = NOW(), updated_at = NOW()WHERE id = $identity_id;Identity Conflicts
Section titled “Identity Conflicts”Same External ID, Different Users
Section titled “Same External ID, Different Users”External ID "ext123" → User "alice"New login with "ext123" → Always maps to "alice"External identity links are unique per provider:
UNIQUE (provider_id, external_user_id)Same Email, Different External IDs
Section titled “Same Email, Different External IDs”User "alice@company.com" ← External ID "ext123" (Okta)User "alice@company.com" ← External ID "ext456" (Azure AD)Both link to the same Mantis user only where that provider permits it: linking an
incoming identity to an existing account by email needs auto_create_users and
allow_email_linking on the provider and email_verified: true in the ID token.
Without all three the second provider’s login fails closed with 403 Email is already associated with another account — see
Automatic Linking by Email.
Conflict Resolution
Section titled “Conflict Resolution”| Scenario | Behavior |
|---|---|
| Existing external_id + same user | Update tokens |
| Existing external_id + different user | Use original link |
| New external_id + existing email | Link only if auto_create_users + allow_email_linking + token email_verified; else 403 |
| New external_id + new email | Create new user (if auto_create and tenant-scoped provider) |
Last Login Tracking
Section titled “Last Login Tracking”Purpose
Section titled “Purpose”last_login_at tracks the most recent SSO login via each provider:
SELECT ip.name, ei.last_login_at, NOW() - ei.last_login_at AS time_since_loginFROM external_identities eiJOIN identity_providers ip ON ip.id = ei.provider_idWHERE ei.user_id = $user_idORDER BY ei.last_login_at DESC;Inactive Identity Detection
Section titled “Inactive Identity Detection”Find identities not used recently:
SELECT u.email, ip.name AS provider, ei.last_login_atFROM external_identities eiJOIN users u ON u.id = ei.user_idJOIN identity_providers ip ON ip.id = ei.provider_idWHERE ei.last_login_at < NOW() - INTERVAL '90 days'ORDER BY ei.last_login_at;User Lifecycle
Section titled “User Lifecycle”Onboarding
Section titled “Onboarding”- User assigned to IdP groups
- First SSO login creates:
- Mantis user (if auto_create enabled)
- External identity link
- Role assignments (from mappings)
Offboarding
Section titled “Offboarding”- Remove user from IdP groups
- Disable IdP account
- (Optional) Disable Mantis account
- (Optional) Remove external identity links
IdP Migration
Section titled “IdP Migration”- Configure new IdP
- Users login via new IdP
- New external identity links created automatically
- Old links remain (can use both IdPs)
- (Optional) Remove old links after migration complete
Audit Trail
Section titled “Audit Trail”External identity events are logged:
Link Created (first SSO login)
Section titled “Link Created (first SSO login)”When a new external identity link is created during SSO, Mantis emits a single event covering both the new user/link creation and the resulting login:
{ "event": "auth.sso_user_created", "category": "authentication", "severity": "security", "actor": { "user_id": "...", "username": "alice" }, "resource": { "type": "user", "id": "..." }, "metadata": { "provider_id": "...", "provider_name": "Okta", "external_user_id": "00u1234567890", "created": true }, "outcome": "success"}Login via External Identity
Section titled “Login via External Identity”Subsequent logins via an existing external identity link emit:
{ "event": "auth.sso_login", "category": "authentication", "severity": "security", "actor": { "user_id": "...", "username": "alice" }, "resource": { "type": "user", "id": "..." }, "metadata": { "provider_id": "019b937d-4862-8001-a001-000000000001", "provider_name": "Okta", "external_user_id": "00u1234567890", "created": false }, "outcome": "success"}Database Schema
Section titled “Database Schema”CREATE TABLE external_identities ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, provider_id UUID NOT NULL REFERENCES identity_providers(id) ON DELETE CASCADE, external_user_id TEXT NOT NULL, external_username TEXT, external_email TEXT, access_token_encrypted TEXT, refresh_token_encrypted TEXT, token_expires_at TIMESTAMP, last_login_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP, UNIQUE (provider_id, external_user_id));
CREATE INDEX idx_external_identities_user_id ON external_identities(user_id);CREATE INDEX idx_external_identities_provider_id ON external_identities(provider_id);CREATE INDEX idx_external_identities_external_user_id ON external_identities(external_user_id);Troubleshooting
Section titled “Troubleshooting”User Cannot Login via SSO
Section titled “User Cannot Login via SSO”-
Check external identity exists:
SELECT * FROM external_identitiesWHERE external_user_id = 'sub_from_idp'AND provider_id = $provider_id; -
Verify linked user is active:
SELECT u.status FROM users uJOIN external_identities ei ON ei.user_id = u.idWHERE ei.external_user_id = 'sub_from_idp'; -
Check provider is enabled:
SELECT is_enabled FROM identity_providers WHERE id = $provider_id;
Wrong User Linked
Section titled “Wrong User Linked”This occurs when:
- External ID was already linked to different user
- Email changed at IdP
Resolution:
curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ "$API_URL/api/v1/admin/users/$USER_ID/external-identities/$PROVIDER_ID"The next login through that provider links the account again only when the provider
has auto_create_users and allow_email_linking set and the IdP asserts the email as
verified. Otherwise that login is refused, or, where no account has the asserted email,
provisions a new one. Unlinking also revokes the roles the provider’s group mappings
granted, in either role sync mode.
Duplicate Users Created
Section titled “Duplicate Users Created”Occurs when:
- Email not returned in ID token
- Different email at IdP than Mantis
Prevention:
- Ensure email scope is requested
- Configure correct email claim
Resolution:
- Merge users manually
- Update external identity links
Token Decryption Fails
Section titled “Token Decryption Fails”Error: Failed to decrypt client secret- Verify encryption key hasn’t changed
- Check AAD format matches
- Re-link identity if key rotated
Best Practices
Section titled “Best Practices”Security
Section titled “Security”- Audit links regularly - Review inactive links
- Monitor login patterns - Detect anomalies
- Prompt token rotation - On security events
- Limit link age - Consider expiring old links
Operations
Section titled “Operations”- Document linking behavior for support team
- Test migrations before switching IdPs
- Monitor link creation for unexpected patterns
- Clean up unused external identities
User Experience
Section titled “User Experience”- Clear error messages for link issues
- Support multiple IdPs per user when needed
- Communicate changes when migrating IdPs
Next Steps
Section titled “Next Steps”- Overview - Identity provider overview
- OIDC Configuration - Provider setup
- Role Mappings - Group-to-role mapping
- Users - User management
