Identity Providers¶
Identity providers are external authentication systems that the PostgreSQL Permissions Model integrates with to authenticate users. The system is provider-agnostic -- it works with any identity provider that can supply a user identifier, and optionally group and role information. Providers are registered in the auth.provider table, and each user can have multiple identities linked to different providers through the auth.user_identity table.
This architecture separates authentication (who is the user?) from authorization (what can the user do?). The identity provider handles authentication; the permissions model handles everything else.
Provider Configuration¶
Every external authentication system that your application integrates with must be registered as a provider. Providers are stored in the auth.provider table with capability flags that control what the system allows for each provider.
The auth.provider Table¶
| Column | Type | Default | Description |
|---|---|---|---|
provider_id |
integer | auto-generated | Primary key |
code |
text | -- | Unique short identifier (e.g., azure_ad, google, email) |
is_active |
boolean | true |
Whether the provider can be used for authentication |
allows_group_mapping |
boolean | false |
Whether external group/role mappings can reference this provider |
allows_group_sync |
boolean | false |
Whether background member synchronization is enabled |
Capability Flags¶
The two capability flags control how the provider participates in the group system:
-
allows_group_mapping-- Whentrue, group mappings inauth.user_group_mappingcan reference this provider. This enables external and hybrid groups to resolve membership based on the provider's group and role data. -
allows_group_sync-- Whentrue, background synchronization processes can pull the provider's member lists into the database. This flag requiresallows_group_mappingto also betrue, enforced by theprovider_sync_requires_mappingconstraint.
Constraint: sync requires mapping
The database enforces that allows_group_sync can only be true if allows_group_mapping is also true. Attempting to set sync without mapping will violate the provider_sync_requires_mapping check constraint.
Creating and Managing Providers¶
-- create a provider with group mapping enabled
select * from auth.create_provider(
'admin', -- _created_by
1, -- _user_id
null, -- _correlation_id
'azure_ad', -- _provider_code
'Azure Active Directory', -- _provider_name
_allows_group_mapping := true,
_allows_group_sync := true
);
-- idempotent: create if missing, return existing if found
select * from auth.ensure_provider(
'admin', 1, null,
'google', 'Google Authentication',
_allows_group_mapping := true
);
-- returns: __provider_id, __is_new (true on first call, false on subsequent)
-- enable or disable a provider
select * from auth.enable_provider('admin', 1, null, 'azure_ad');
select * from auth.disable_provider('admin', 1, null, 'azure_ad');
Disabling a provider
When a provider is disabled (is_active = false), any attempt to authenticate through that provider will fail with error 33010 ("Provider is not in active state"). Existing user identities linked to the provider are not deleted, but they cannot be used for login until the provider is re-enabled.
User Identity¶
The auth.user_identity table links users to their external provider accounts. A single user in auth.user_info can have multiple identities -- one per provider they have authenticated with. This means the same person can log in through AzureAD at work, Google from a personal device, or email/password from a registration form, and all of these map to a single user record.
The auth.user_identity Table¶
| Column | Type | Default | Description |
|---|---|---|---|
user_identity_id |
bigint | auto-generated | Primary key |
provider_code |
text | -- | FK to auth.provider; identifies which provider issued this identity |
user_id |
bigint | -- | FK to auth.user_info; the user this identity belongs to |
uid |
text | -- | Provider-specific user identifier (e.g., email address, internal ID) |
provider_oid |
text | -- | Provider object ID (e.g., AzureAD object GUID); globally unique |
provider_groups |
text[] | -- | Array of group identifiers from the provider (e.g., AzureAD group GUIDs) |
provider_roles |
text[] | -- | Array of role names from the provider (e.g., global_admin, reader) |
user_data |
jsonb | -- | Arbitrary provider-specific data (e.g., profile fields, claims) |
password_hash |
text | -- | Hashed password (used only by the email provider for local authentication) |
password_salt |
text | -- | Password salt (used only by the email provider) |
is_active |
boolean | true |
Whether this identity can be used for authentication |
Unique Constraints¶
The table has two important uniqueness rules:
-
uq_user_identity-- Unique on(provider_code, uid), ensuring that a given provider-specific user ID maps to exactly one identity record. Thecoalesce(uid, '-1')in the index allows null UIDs without violating uniqueness. -
uq_user_identity_provider_oid-- Unique onprovider_oid, ensuring that each provider object ID is globally unique across all providers.
Multiple Identities per User¶
A user can accumulate identities over time as they authenticate through different providers:
auth.user_info (user_id: 1001, username: "jane.doe")
|
+-- auth.user_identity (provider: "azure_ad", uid: "jane@corp.com",
| provider_oid: "aad-guid-123", provider_groups: ["IT", "Admins"])
|
+-- auth.user_identity (provider: "google", uid: "jane.doe@gmail.com",
| provider_oid: "google-id-456", provider_groups: [])
|
+-- auth.user_identity (provider: "email", uid: "jane@example.com",
password_hash: "...", password_salt: "...")
Each identity is independent. You can enable or disable individual identities without affecting the others:
-- disable a specific provider identity for a user
select * from auth.disable_user_identity('admin', 1, null, 1001, 'google');
-- re-enable it later
select * from auth.enable_user_identity('admin', 1, null, 1001, 'google');
Provider Groups and Provider Roles¶
The provider_groups and provider_roles arrays are the bridge between external identity providers and the internal group system. These arrays are populated by your application when the user authenticates, typically from the identity provider's token claims or API response.
-
provider_groups-- Typically contains group identifiers such as AzureAD group GUIDs, LDAP group DNs, or Google Workspace group IDs. Matched againstauth.user_group_mapping.mapped_object_id. -
provider_roles-- Typically contains role names such asglobal_admin,user, orbilling_manager. Matched againstauth.user_group_mapping.mapped_role.
The system uses these arrays during permission resolution to determine which external and hybrid groups the user belongs to, without requiring explicit membership rows in auth.user_group_member.
Last Used Provider¶
The auth.user_info table has a last_used_provider_code column that tracks which provider the user most recently authenticated through. This is a critical piece of the permission resolution system.
Why It Matters¶
When a user has multiple identities, each identity may carry different provider_groups and provider_roles. The system needs a deterministic way to decide which identity's group/role data to use for permission calculations. The answer is simple: the last provider the user logged in with wins.
When a user authenticates:
- The system updates
auth.user_info.last_used_provider_codeto the current provider - Permission resolution reads the
provider_groupsandprovider_rolesfrom the identity matching this provider - Those arrays are matched against
auth.user_group_mappingto determine external group membership - The resulting group memberships (plus any direct/internal memberships) feed into the full permission calculation
This means that if a user logs in via AzureAD, they get permissions based on their AzureAD groups. If the same user later logs in via Google, their permissions shift to reflect their Google groups instead.
Practical impact
In most deployments, users authenticate through a single provider consistently (e.g., always AzureAD for corporate users). The last-used-provider mechanism primarily matters in environments where the same user genuinely switches between providers, which is uncommon but fully supported.
How It Is Updated¶
The last_used_provider_code is set automatically by the unsecure.update_last_used_provider() function, which is called internally by both auth.ensure_user_from_provider() and auth.register_user(). You do not need to update it manually.
-- this is called internally; shown here for understanding only
update auth.user_info
set last_used_provider_code = _provider_code
where user_id = _target_user_id;
Supported Provider Types¶
The system does not have a hardcoded list of supported providers. Any authentication system can be integrated by registering it as a provider and supplying the appropriate identifiers during user login. Common examples include:
| Provider | Typical code |
Identity Data |
|---|---|---|
| Azure Active Directory | azure_ad |
Object GUID as provider_oid, group GUIDs in provider_groups, role names in provider_roles |
| Google Workspace / OAuth | google |
Google user ID as provider_oid, workspace group IDs in provider_groups |
facebook |
Facebook user ID as provider_oid |
|
| KeyCloak | keycloak |
KeyCloak user UUID as provider_oid, realm roles in provider_roles |
| LDAP / Active Directory | ldap |
Distinguished name or GUID as provider_oid, group DNs in provider_groups |
| Windows Authentication | windows |
Windows SID or GUID as provider_oid, AD group SIDs in provider_groups |
| Email / Password | email |
Email address as both uid and provider_oid, password_hash and password_salt for local auth |
| SAML | saml |
NameID as provider_oid, SAML attributes mapped to provider_groups / provider_roles |
Provider-agnostic design
The permissions model has no knowledge of any specific provider's API or protocol. Your application layer is responsible for authenticating the user with the provider and then passing the resulting identifiers, groups, and roles to the database functions. This keeps the database layer clean and independent of any particular authentication technology.
Login Flow¶
The primary function for provider-based authentication is auth.ensure_user_from_provider(). This function handles both first-time user provisioning (creating a new user) and returning user authentication (updating existing records) in a single call. Your application calls this function after successfully authenticating a user with an external provider.
How ensure_user_from_provider Works¶
The function follows this logic:
-
Reject email provider -- The
emailprovider usesauth.register_user()instead; callingensure_user_from_providerwithemailraises an error. -
Validate provider -- Check that the provider is active (
is_active = true). If not, raise error 33010. -
Look up existing identity -- Search
auth.user_identityfor a matchingprovider_codeand eitheruidorprovider_oid. -
If no identity found (new user):
- Check the user blacklist for the provider identity
- Create a new
auth.user_inforecord - Create a new
auth.user_identityrecord linking the user to the provider - Log a
user_registeredevent
-
If identity found (returning user):
- Update the identity's
uidandprovider_oidif they have changed - Update basic user data (username, display name, email) if changed
- Validate the user can log in (
can_login = true), is active (is_active = true), and the identity is active (is_active = true)
- Update the identity's
-
Clean up duplicate UIDs -- Remove any other identities for the same user and provider that have a different
uid(handles provider-side ID changes). -
Update last used provider -- Set
auth.user_info.last_used_provider_codeto the current provider. -
Log login event -- Create a
user_logged_inevent inauth.user_event. -
Return user data -- Return the user's
user_id,code,uuid,username,email, anddisplay_name.
Sequence Diagram¶
sequenceDiagram
participant App as Application
participant DB as auth.ensure_user_from_provider()
participant Provider as auth.provider
participant Identity as auth.user_identity
participant User as auth.user_info
participant Event as auth.user_event
App->>DB: call with provider_code, uid, oid, username, etc.
DB->>Provider: validate provider is active
Provider-->>DB: OK
DB->>Identity: look up by provider_code + uid/oid
alt New user (no identity found)
DB->>DB: check user blacklist
DB->>User: create user_info record
DB->>Identity: create user_identity record
DB->>Event: log user_registered
else Returning user (identity found)
DB->>Identity: update uid/oid if changed
DB->>User: update username/display_name/email if changed
DB->>DB: validate can_login, is_active, identity is_active
end
DB->>Identity: clean up duplicate UIDs for same provider
DB->>User: set last_used_provider_code
DB->>Event: log user_logged_in
DB-->>App: return user_id, code, uuid, username, email, display_name
Example Usage¶
-- user logs in via AzureAD; application has already verified the token
select * from auth.ensure_user_from_provider(
'app', -- _created_by
3, -- _user_id (svc_authenticator service account)
'corr-abc-123', -- _correlation_id
'azure_ad', -- _provider_code
'jane@corp.com', -- _provider_uid
'aad-guid-12345', -- _provider_oid
'jane.doe', -- _username
'Jane Doe', -- _display_name
'jane@corp.com', -- _email
'{"department": "IT"}'::jsonb -- _user_data (optional)
);
After this call, your application should update the user's provider_groups and provider_roles on the auth.user_identity record with the data from the provider's token, so that group mapping resolution has up-to-date information.
Email Registration Flow¶
For email/password-based registration, use auth.register_user() instead. This function is specifically designed for the built-in email provider and handles password hashing storage:
select * from auth.register_user(
'app', -- _created_by
2, -- _user_id (svc_registrator service account)
null, -- _correlation_id
'jane@example.com', -- _email (used as both username and uid)
'hashed_password_here', -- _password_hash
'Jane Doe' -- _display_name
);
Provider separation
The email provider and external providers (AzureAD, Google, etc.) use different registration paths. Calling auth.ensure_user_from_provider() with provider_code = 'email' raises error 33006 ("User cannot be ensured for email provider"). This separation exists because email registration requires a password hash, while external providers handle authentication entirely outside the database.
Provider Events¶
All provider lifecycle operations are logged to the journal with event codes in the 16001--16005 range:
| Event Code | Event | Description |
|---|---|---|
| 16001 | provider_created |
New provider was created |
| 16002 | provider_updated |
Provider was updated |
| 16003 | provider_deleted |
Provider was deleted |
| 16004 | provider_enabled |
Provider was enabled |
| 16005 | provider_disabled |
Provider was disabled |
Identity-related events are logged under 10030--10034:
| Event Code | Event | Description |
|---|---|---|
| 10030 | identity_created |
User identity was created |
| 10031 | identity_updated |
User identity was updated |
| 10032 | identity_deleted |
User identity was deleted |
| 10033 | identity_enabled |
User identity was enabled |
| 10034 | identity_disabled |
User identity was disabled |
See Also¶
- Provider Functions Reference -- complete function reference for provider CRUD
- User Functions Reference -- user registration and identity management functions
- Group Types -- how providers feed into external and hybrid group membership
- Permission Model -- how provider data flows into permission resolution