Skip to content

External Identities

Manage links between Mantis users and external identity provider accounts.

External identities link Mantis user accounts to identity provider accounts, enabling SSO authentication and user lifecycle management.

FieldTypeDescription
idUUIDUnique identifier
user_idUUIDLinked Mantis user
provider_idUUIDIdentity provider reference
external_user_idStringUser ID at IdP (sub claim)
external_usernameString?Username at IdP
external_emailString?Email at IdP
access_token_encryptedString?Encrypted access token
refresh_token_encryptedString?Encrypted refresh token
token_expires_atDateTime?Token expiration
last_login_atDateTime?Last SSO login via this link
created_atDateTimeLink creation time

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

User details include linked external identities:

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/admin/users/$USER_ID"

Response:

{
"id": "user_abc123",
"email": "alice@company.com",
"username": "alice",
"external_identities": [
{
"provider_id": "idp_xyz789",
"provider_name": "Okta",
"external_username": "alice.smith",
"external_email": "alice.smith@company.com",
"last_login_at": "2024-01-15T10:30:00Z"
},
{
"provider_id": "idp_abc456",
"provider_name": "Azure AD",
"external_username": "asmith",
"external_email": "asmith@company.onmicrosoft.com",
"last_login_at": "2024-01-10T08:15:00Z"
}
]
}
SELECT
ei.id,
u.email AS mantis_email,
ip.name AS provider_name,
ei.external_username,
ei.external_email,
ei.last_login_at
FROM external_identities ei
JOIN users u ON u.id = ei.user_id
JOIN identity_providers ip ON ip.id = ei.provider_id
WHERE u.email = 'alice@company.com';

When a user logs in via SSO, Mantis checks for existing users by email:

Currently, external identity links are created automatically during SSO. To manually create a link:

INSERT INTO external_identities (
user_id,
provider_id,
external_user_id,
external_username,
external_email,
created_at
) VALUES (
(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()
);
  • User leaving organization
  • Migrating to different IdP
  • Security incident response
  • Account cleanup
-- Delete specific external identity link
DELETE FROM external_identities
WHERE user_id = (SELECT id FROM users WHERE email = 'alice@company.com')
AND provider_id = (SELECT id FROM identity_providers WHERE name = 'Okta');

IdP tokens are stored encrypted for potential future use:

TokenPurpose
access_tokenAPI calls to IdP (user info, etc.)
refresh_tokenObtaining 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)?;

On each SSO login, tokens are updated:

UPDATE external_identities
SET
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;
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)
User "alice@company.com" ← External ID "ext123" (Okta)
User "alice@company.com" ← External ID "ext456" (Azure AD)

Both link to the same Mantis user via email matching.

ScenarioBehavior
Existing external_id + same userUpdate tokens
Existing external_id + different userUse original link
New external_id + existing emailLink to existing user
New external_id + new emailCreate new user (if auto_create)

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_login
FROM external_identities ei
JOIN identity_providers ip ON ip.id = ei.provider_id
WHERE ei.user_id = $user_id
ORDER BY ei.last_login_at DESC;

Find identities not used recently:

SELECT
u.email,
ip.name AS provider,
ei.last_login_at
FROM external_identities ei
JOIN users u ON u.id = ei.user_id
JOIN identity_providers ip ON ip.id = ei.provider_id
WHERE ei.last_login_at < NOW() - INTERVAL '90 days'
ORDER BY ei.last_login_at;
  1. User assigned to IdP groups
  2. First SSO login creates:
    • Mantis user (if auto_create enabled)
    • External identity link
    • Role assignments (from mappings)
  1. Remove user from IdP groups
  2. Disable IdP account
  3. (Optional) Disable Mantis account
  4. (Optional) Remove external identity links
  1. Configure new IdP
  2. Users login via new IdP
  3. New external identity links created automatically
  4. Old links remain (can use both IdPs)
  5. (Optional) Remove old links after migration complete

External identity events are logged:

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"
}

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"
}
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);
  1. Check external identity exists:

    SELECT * FROM external_identities
    WHERE external_user_id = 'sub_from_idp'
    AND provider_id = $provider_id;
  2. Verify linked user is active:

    SELECT u.status FROM users u
    JOIN external_identities ei ON ei.user_id = u.id
    WHERE ei.external_user_id = 'sub_from_idp';
  3. Check provider is enabled:

    SELECT is_enabled FROM identity_providers WHERE id = $provider_id;

This occurs when:

  • External ID was already linked to different user
  • Email changed at IdP

Resolution:

-- Remove incorrect link
DELETE FROM external_identities
WHERE id = $incorrect_link_id;
-- User will be re-linked on next login

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
Error: Failed to decrypt client secret
  1. Verify encryption key hasn’t changed
  2. Check AAD format matches
  3. Re-link identity if key rotated
  1. Audit links regularly - Review inactive links
  2. Monitor login patterns - Detect anomalies
  3. Prompt token rotation - On security events
  4. Limit link age - Consider expiring old links
  1. Document linking behavior for support team
  2. Test migrations before switching IdPs
  3. Monitor link creation for unexpected patterns
  4. Clean up unused external identities
  1. Clear error messages for link issues
  2. Support multiple IdPs per user when needed
  3. Communicate changes when migrating IdPs