Identity Base: Two-Tier Permission Model
Published: November 5, 2025
Identity Base 0.6.1 introduces a clear separation between platform-level administration and user-driven organization management. This separation happens through a two-tier permission model: admin.organizations.* permissions protect platform operations, while user.organizations.* permissions enable self-service organization management.
This isn't just a naming convention - it's enforced at multiple layers. Platform admin endpoints require specific OAuth scopes before permission checks even run. User self-service endpoints operate without elevated scopes, using permission checks scoped to individual organizations. The result is a permission model where platform operators and regular users can't accidentally cross boundaries, even if permission assignment goes wrong.
⚠️ Breaking Changes
Permission namespace changed: The legacy organization.* permissions are renamed to admin.organizations.*. If your role seeds or permission checks reference organization.*, update them to use the new namespace.
Admin endpoints relocated: Organization CRUD, membership, role, and invitation endpoints now live under /admin/organizations/* instead of top-level paths. Update your API calls accordingly.
Scope enforcement added: Admin endpoints now require the identity.admin OAuth scope (or your configured Organizations:Authorization:AdminRequiredScope) before permission checks run. Tokens without this scope receive 403 responses even if they have the correct permissions.
New user permission namespace: Parallel user.organizations.* permissions are introduced for self-service operations. Update role seeds to include these permissions for organization-scoped roles like OrgOwner and OrgAdmin.
Why Two Permission Tiers
Before this update, organization management permissions work like other application permissions. Administrators with organization management permissions perform operations globally, while regular users have no organization management capabilities. This works for applications where platform administrators manage organization structure, but creates problems when users need to manage their own teams.
The challenge: how do you let users create organizations, invite teammates, and assign roles without giving them platform-wide administrative access? You can't just grant organizations.* permissions to regular users - that would let them see and modify all organizations in the system. You need a way to scope organization management permissions to only the organizations a user belongs to.
The two-tier model solves this by separating permission namespaces and enforcement mechanisms. Platform operations use admin.organizations.* permissions and require the identity.admin OAuth scope. User operations use user.organizations.* permissions and don't require elevated scopes. The middleware enforces these boundaries automatically based on which API surface you're calling.
Platform Administration Tier
Platform-level operations use the /admin/organizations/* API surface and are protected by admin.organizations.* permissions. These permissions grant access to operations that work across all organizations in your system:
- Viewing all organizations regardless of membership
- Creating organizations on behalf of users
- Modifying organization settings for any organization
- Viewing and modifying memberships across all organizations
- Deleting organizations
- Accessing billing and usage data across organizations
Platform admin endpoints also require the identity.admin OAuth scope (configurable via Organizations:Authorization:AdminRequiredScope). This scope requirement is enforced before permission checks run. If a request to an admin endpoint lacks the required scope, it's rejected with a 403 response before the permission system evaluates the user's admin.organizations.* permissions.
Here's what this looks like in practice:
# Attempt to access admin endpoint without required scope
# Even if user has admin.organizations.* permissions, this fails
curl -H "Authorization: Bearer $USER_TOKEN_WITHOUT_ADMIN_SCOPE" \
https://your-identity-base.example.com/admin/organizations
# HTTP 403 Forbidden
# Response: "Missing required scope: identity.admin"
# With admin scope, permission checks run normally
curl -H "Authorization: Bearer $ADMIN_TOKEN_WITH_IDENTITY_ADMIN_SCOPE" \
https://your-identity-base.example.com/admin/organizations
# HTTP 200 OK (if user also has admin.organizations.read permission)
This dual-layer protection (scope + permission) prevents accidental privilege escalation. Even if you mistakenly assign admin.organizations.* permissions to a regular user, they can't access admin endpoints because their tokens don't carry the required scope. Scopes are controlled by your authentication configuration and only issued to users who authenticate through admin-specific flows.
Self-Service User Tier
User-level operations use the /users/me/organizations/* API surface and are protected by user.organizations.* permissions. These permissions grant access to organization management operations scoped to organizations the user belongs to:
- Creating new organizations (user becomes owner)
- Viewing organizations where the user is a member
- Inviting teammates to organizations where the user has invite permissions
- Assigning roles within organizations where the user has role management permissions
- Viewing and managing memberships in the user's organizations
User endpoints do not require special OAuth scopes. Any authenticated user can access these endpoints - the permission checks determine what they can actually do.
The scoping happens through organization membership. When you call a user endpoint that operates on a specific organization (like inviting a member or assigning a role), the middleware verifies you're a member of that organization before checking permissions. If you're not a member, the request fails regardless of what permissions you have.
# Create a new organization - available to any authenticated user
curl -X POST \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"My Team"}' \
https://your-identity-base.example.com/users/me/organizations
# HTTP 201 Created
# User automatically becomes OrgOwner of the new organization
# Try to invite someone to an organization the user doesn't belong to
curl -X POST \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","role":"OrgMember"}' \
https://your-identity-base.example.com/users/me/organizations/org_not_member_of/invitations
# HTTP 403 Forbidden
# Response: "User is not a member of this organization"
The permission namespace separation (admin.* vs user.*) prevents permission overlap. A user with user.organizations.* permissions can manage their organizations, but they can't access admin endpoints or see organizations they're not members of. An administrator with admin.organizations.* can view all organizations through admin endpoints, but they still need user.organizations.* permissions if they want to use the self-service APIs.
Permission Resolution with Organization Context
The /users/me/permissions endpoint demonstrates how the two tiers interact. This endpoint returns the permissions available to the current user, but what it returns depends on whether you include the X-Organization-Id header.
Without organization context, it returns user-level permissions only:
# Fetch permissions without org context
curl -H "Authorization: Bearer $USER_TOKEN" \
https://your-identity-base.example.com/users/me/permissions
# Returns: permissions from user-level roles only
# Example: ["user.profile.write", "user.organizations.create"]
With organization context, it merges permissions from user-level roles and organization-scoped roles:
# Fetch permissions for a specific organization
curl -H "Authorization: Bearer $USER_TOKEN" \
-H "X-Organization-Id: org_abc123" \
https://your-identity-base.example.com/users/me/permissions
# Returns: user-level permissions + org-scoped permissions
# Example: [
# "user.profile.write",
# "user.organizations.create",
# "user.organizations.members.write", // from OrgOwner role in org_abc123
# "user.organizations.roles.write" // from OrgOwner role in org_abc123
# ]
This lets your client application query effective permissions for the current organizational context and use that to show/hide UI elements or enable/disable features.
The admin permission tier doesn't appear in this endpoint's response even if you have admin permissions - this endpoint is user-scoped, so it only returns user.* permissions. To check admin permissions, you'd query through admin endpoints.
Configuring the Admin Scope Requirement
The required OAuth scope for admin endpoints is configured in your application settings:
{
"Organizations": {
"Authorization": {
"AdminRequiredScope": "identity.admin"
},
"RoleOptions": {
"Seeds": ["OrgOwner", "OrgAdmin", "OrgMember"]
}
}
}
The AdminRequiredScope value determines which scope must be present in the access token to access admin endpoints. The default is identity.admin, but you can customize this to match your OAuth configuration.
This scope is requested during authentication flows intended for administrators:
// Regular user authentication - doesn't request admin scope
const authUrl = buildAuthUrl({
clientId: 'your-app',
scopes: ['openid', 'profile', 'email'],
redirectUri: 'https://your-app.com/callback'
});
// Admin authentication - includes admin scope
const adminAuthUrl = buildAuthUrl({
clientId: 'admin-panel',
scopes: ['openid', 'profile', 'email', 'identity.admin'],
redirectUri: 'https://admin.your-app.com/callback'
});
Your identity provider (the OIDC server) determines whether to grant the identity.admin scope based on the user's roles and the client application's configuration. Regular users authenticating through the admin panel won't receive the scope if they lack admin roles, even though the client requested it.
Seeding Roles and Permissions
The permission tiers are only useful if you assign them to roles correctly. Your role seeds should distinguish between platform administrators and organization users.
A typical configuration includes separate role hierarchies:
Platform Roles (assigned at the user level, not organization-scoped):
PlatformAdmin- Fulladmin.*permissions includingadmin.organizations.*PlatformSupport- Read-onlyadmin.*.readpermissions for customer supportPlatformBilling-admin.organizations.readandadmin.billing.*for billing operations
Organization Roles (assigned within specific organizations):
OrgOwner- Fulluser.organizations.*permissions within the organizationOrgAdmin- Mostuser.organizations.*except ownership transfer or deletionOrgMember- Basic collaboration permissions, no team management
When a user creates an organization through the self-service API, they're automatically assigned the role specified in your configuration (defaults to OrgOwner). This role should include permissions like:
user.organizations.read- View organization detailsuser.organizations.write- Modify organization settingsuser.organizations.members.read- List membersuser.organizations.members.write- Invite and remove membersuser.organizations.roles.write- Assign roles to membersuser.organizations.invitations.*- Manage invitations
The Seed Roles and Default Organization playbook provides detailed examples of role configurations and permission assignments.
Authorization Flow in Practice
Here's how the two-tier model works during a typical request:
Admin endpoint request:
- Middleware checks for
Authorizationheader and validates token - Scope validation middleware checks for required scope (e.g.,
identity.admin) - If scope is missing, request fails with 403 before endpoint handler runs
- If scope is present, endpoint handler runs and checks
admin.*permissions - Permission check determines whether specific operation is allowed
User endpoint request:
- Middleware checks for
Authorizationheader and validates token - No scope check - any authenticated user can reach user endpoints
- If endpoint is org-scoped, middleware validates
X-Organization-Idheader - Membership validation ensures user belongs to specified organization
- Endpoint handler runs and checks
user.*permissions scoped to that organization
This flow ensures that even if permission assignment is misconfigured, the scope layer prevents privilege escalation through admin endpoints.
Common Permission Patterns
Read-Only Organization Member
A user who can view organization details and member lists but can't modify anything:
{
"role": "OrgViewer",
"permissions": [
"user.organizations.read",
"user.organizations.members.read"
]
}
Team Manager
A user who can invite members and assign roles but can't modify organization settings:
{
"role": "OrgTeamLead",
"permissions": [
"user.organizations.read",
"user.organizations.members.read",
"user.organizations.members.write",
"user.organizations.invitations.*",
"user.organizations.roles.write"
]
}
Organization Administrator
A user who can do everything within their organization except delete it or transfer ownership:
{
"role": "OrgAdmin",
"permissions": [
"user.organizations.*"
]
}
Note: wildcard permissions (user.organizations.*) grant all permissions in that namespace. You can then override specific permissions if needed.
Testing Permission Boundaries
Verify that the two-tier model is working correctly by testing permission boundaries:
Test 1: Regular user can't access admin endpoints
# Regular user token (lacks identity.admin scope)
curl -i -H "Authorization: Bearer $REGULAR_USER_TOKEN" \
https://your-identity-base.example.com/admin/organizations
# Expected: HTTP 403 Forbidden
# Response should indicate missing scope, not missing permission
Test 2: Admin token without permissions fails
# Admin token (has identity.admin scope) but user lacks admin.organizations.read permission
curl -i -H "Authorization: Bearer $ADMIN_TOKEN_NO_PERMISSIONS" \
https://your-identity-base.example.com/admin/organizations
# Expected: HTTP 403 Forbidden
# Response should indicate missing permission (passed scope check, failed permission check)
Test 3: User can't access organizations they're not members of
# User token, valid organization ID, but user is not a member
curl -i -H "Authorization: Bearer $USER_TOKEN" \
-H "X-Organization-Id: org_not_a_member" \
https://your-identity-base.example.com/users/me/organizations/org_not_a_member/members
# Expected: HTTP 403 Forbidden
# Response should indicate user is not a member of specified organization
Test 4: Organization owner can manage their org
# User token for organization owner
curl -i -X POST \
-H "Authorization: Bearer $ORG_OWNER_TOKEN" \
-H "X-Organization-Id: org_they_own" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","role":"OrgMember"}' \
https://your-identity-base.example.com/users/me/organizations/org_they_own/invitations
# Expected: HTTP 201 Created
# Invitation should be created successfully
Security Implications
The two-tier model provides defense in depth against permission misconfiguration. Even if you accidentally assign powerful permissions to the wrong users, the scope layer prevents them from accessing admin endpoints.
This is particularly important during incidents or when making bulk permission changes. If a migration script incorrectly grants admin.organizations.* to all users, they still can't access admin endpoints because their tokens lack the required scope. The blast radius of the misconfiguration is limited to whatever admin.* permissions allow outside the admin API surface (which should be nothing).
The organizational membership validation provides a similar safety mechanism for user endpoints. Even if a user somehow acquires user.organizations.* permissions globally, they still can't operate on organizations they're not members of because the membership check happens before permission evaluation.
These layered validations make the permission model resilient to configuration errors and reduce the risk of privilege escalation bugs.
Related Documentation
Guides:
- Identity Permissions Claim - Detailed explanation of permission claims and resolution
- Admin Operations Guide - Using admin endpoints for platform operations
Playbooks:
- Seed Roles and Default Organization - Configuring roles and permissions
Package References:
- Identity.Base.Roles - Role and permission configuration
- Identity.Base.Organizations - Organization permission configuration
Changelog:
- CHANGELOG.md - Full version history
Questions about implementing role-based access control with the two-tier model? Start a discussion: https://github.com/Amaretto-Software-Labs/identity-base/discussions