Identity Base: Header-Based Organization Context
Published: November 8, 2025
Identity Base 0.6.3 removes the IsPrimary membership flag and replaces it with per-request organization context driven by the X-Organization-Id header. This is a significant architectural change that affects how your application handles organization switching and permission resolution.
The fundamental shift: organization context is no longer embedded in tokens. Instead, clients specify which organization they're operating within on each request via a header. The token carries membership information (which organizations you belong to), while the header establishes context (which organization you're currently working in).
⚠️ Breaking Changes
IsPrimary flag removed: The IsPrimary property no longer exists on organization memberships. Membership DTOs, list endpoints, and queries no longer expose or filter by this flag.
Organization switching endpoints removed: Endpoints like POST /users/me/organizations/{id}/set-primary or similar that updated the active organization are removed. Organization context must now be set via the X-Organization-Id header on each request.
Client code requires updates: Applications that relied on server-side "active organization" state or the IsPrimary flag need to migrate to header-based context. See the "Migrating from IsPrimary-Based Patterns" section below.
Token claims changed: Tokens now include an org:memberships claim listing all organizations. Individual organization context no longer appears in token claims.
The Problem with Token-Based Context
In Identity Base versions prior to 0.6.3, organization context relies on an IsPrimary flag on organization memberships. One organization gets marked as "primary," and that organization's permissions merge into token claims at issuance time.
This creates several friction points during integration. When users need to switch organizations, developers face two options: maintain the current "primary" organization server-side and issue new tokens when it changes, or force users to refresh their tokens to switch context. The first approach breaks statelessness - the backend must track which organization each user currently operates in. The second approach creates latency and poor user experience - every context switch triggers authentication flows.
Both approaches caused problems when users had your application open in multiple browser tabs. If one tab switched organizations and updated server-side state, other tabs became desynchronized. If switching required token refresh, you had to coordinate refresh across all tabs or accept that different tabs might be operating in different organizational contexts without the UI reflecting it.
Organization membership changes compounded these issues. When a user was invited to a new organization, their token didn't include that organization until they logged out and back in, or your application forced a token refresh. This meant users were confused about why they couldn't see organizations they'd just been invited to, and your code had to include explicit token refresh logic whenever memberships changed.
How Header-Based Context Works
The new model separates membership from context. Your JWT token carries an org:memberships claim that lists all organizations you belong to. This claim updates when the token is refreshed (typically on expiry or explicit refresh requests), but it doesn't change during normal application usage.
When you make a request that requires organization context - like fetching permissions, listing organization members, or accessing organization-specific resources - you include the X-Organization-Id header with the organization's UUID. Middleware reads this header, validates that you're a member of that organization (by checking against the org:memberships claim in your token), and establishes context for the request.
If the organization ID in the header matches one of your memberships, the request proceeds with that organizational context. If it doesn't match, or if you're not a member of that organization, the middleware rejects the request with a 403 Forbidden response.
This happens per-request. Different requests can operate in different organizational contexts without any server-side state changes or token updates. Your SPA can have one tab viewing Organization A's data and another tab viewing Organization B's data, each with different header values, using the same token.
Here's what this looks like in practice:
# Fetch permissions for a specific organization
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-Organization-Id: org_3f2f8d9a..." \
https://your-identity-base.example.com/users/me/permissions
# Returns: merged permissions from your user-level roles plus
# roles assigned within org_3f2f8d9a
Without the header, the same endpoint returns only your user-level permissions:
# Fetch permissions without organization context
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://your-identity-base.example.com/users/me/permissions
# Returns: only user-level permissions, no organization-scoped permissions
The middleware that enables this is registered in your ASP.NET Core pipeline:
var builder = WebApplication.CreateBuilder(args);
// ... service configuration
var app = builder.Build();
// Middleware order matters: authentication must run first
app.UseAuthentication();
app.UseAuthorization();
app.UseOrganizationContextFromHeader(); // After auth, before endpoints
app.MapControllers();
app.Run();
The placement in the pipeline is important. UseOrganizationContextFromHeader() must run after UseAuthentication() (so user claims are available) but before your endpoint handlers (so context is established when your code runs).
Implementing Context Switching in Client Applications
From a client application perspective, organization switching becomes a state management problem instead of an authentication problem. You track which organization the user is currently viewing in your application state, and you include that organization ID in headers when making API calls.
Here's a simple TypeScript helper that wraps fetch with organization context:
export async function fetchWithOrgContext(
path: string,
orgId?: string,
options?: RequestInit
) {
const headers: Record<string, string> = {
'Authorization': `Bearer ${getAccessToken()}`,
...options?.headers
};
if (orgId) {
headers['X-Organization-Id'] = orgId;
}
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers
});
if (!response.ok) {
throw new Error(`Request failed: ${response.statusText}`);
}
return response.json();
}
// Usage - fetch permissions for a specific org
const permissions = await fetchWithOrgContext(
'/users/me/permissions',
currentOrgId
);
// Usage - fetch permissions without org context
const globalPermissions = await fetchWithOrgContext('/users/me/permissions');
In a React application, you might manage current organization in context or state:
function OrganizationProvider({ children }: { children: React.ReactNode }) {
const [currentOrgId, setCurrentOrgId] = useState<string | null>(null);
const switchOrganization = useCallback((orgId: string) => {
// No API call needed - just update local state
setCurrentOrgId(orgId);
// UI re-renders, subsequent API calls use new org context
}, []);
return (
<OrgContext.Provider value={{ currentOrgId, switchOrganization }}>
{children}
</OrgContext.Provider>
);
}
// In components that fetch data
function useOrgPermissions() {
const { currentOrgId } = useOrgContext();
return useQuery(['permissions', currentOrgId], () =>
fetchWithOrgContext('/users/me/permissions', currentOrgId)
);
}
When currentOrgId changes, the query refetches with the new header value. No token refresh, no authentication flow, no server-side state update - just a state change that triggers a new API call with a different header.
Migrating from IsPrimary-Based Patterns
If your application used the IsPrimary flag to track active organizations, you'll need to migrate your client code to use headers instead.
Remove organization switching endpoints. If you had an API endpoint that updated which organization was "primary" (something like POST /users/me/organizations/{id}/set-primary), you can remove it. Organization switching is now entirely client-side state management.
Update permission fetching. If you called /users/me/permissions and expected it to automatically include the current organization's permissions, you'll need to explicitly pass the organization ID via header:
// Old approach (implicitly used IsPrimary)
const permissions = await fetch('/users/me/permissions', {
headers: { 'Authorization': `Bearer ${token}` }
});
// New approach (explicitly specify org context)
const permissions = await fetch('/users/me/permissions', {
headers: {
'Authorization': `Bearer ${token}`,
'X-Organization-Id': currentOrgId
}
});
Handle organization membership changes. When a user's organization membership changes (they're invited to a new org, removed from an existing org, or their roles change), you need to refresh their token to update the org:memberships claim. The header-based model doesn't change this - membership information still lives in token claims.
The difference is that you only need to refresh when membership actually changes, not when the user switches between existing organizations.
async function acceptInvitation(invitationId: string) {
// Accept the invitation via API
await fetch(`/users/me/invitations/${invitationId}/accept`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
// Membership changed - refresh token to update claims
await refreshAccessToken();
// Now the new organization appears in org:memberships
// User can switch to it by setting the header
}
Test organization context validation. Verify that your application correctly handles 403 responses when users try to access organizations they're not members of:
async function fetchOrgData(orgId: string) {
try {
return await fetchWithOrgContext('/users/me/permissions', orgId);
} catch (error) {
if (error.status === 403) {
// User is not a member of this organization
// Redirect to org list or show error
handleNotAuthorized(orgId);
}
throw error;
}
}
Performance and Caching Implications
Header-based context changes how you think about caching API responses. With token-based context, you could cache permission checks or organization data based on the user ID alone - the token determined organization context implicitly.
With header-based context, cache keys need to include the organization ID. Two requests to the same endpoint with the same token but different X-Organization-Id headers should return different data:
// Cache key must include org context
const cacheKey = `permissions:${userId}:${orgId}`;
// Same user, different orgs, different cache entries
const orgAPermissions = cache.get('permissions:user123:orgA');
const orgBPermissions = cache.get('permissions:user123:orgB');
This applies to React Query, SWR, or any other client-side caching layer you're using. The organization ID should be part of your query key:
// Query key includes orgId to prevent cache collisions
const { data } = useQuery(
['org-members', orgId],
() => fetchWithOrgContext(`/organizations/${orgId}/members`, orgId)
);
Server-side caching follows the same pattern. If you're caching permission checks or organization queries, include the organization ID from the header in your cache key.
Security Model and Validation
The middleware performs validation at two levels. First, it verifies that the X-Organization-Id header contains a valid UUID format. Malformed headers are rejected before any database queries run.
Second, it validates that the organization ID matches one of the org:memberships claims in the user's JWT. This check happens against the token claims, not against the database, so it's fast and doesn't require additional queries.
If validation passes, the organization context is available to your endpoint handlers through dependency injection or HttpContext. If validation fails, the request is rejected with a 403 Forbidden response before reaching your endpoint code.
This means your application code can assume that if a request has organization context, the user is a verified member of that organization. You don't need to re-validate membership in every endpoint.
[HttpGet("/api/data")]
public IActionResult GetData([FromServices] IOrganizationContext orgContext)
{
if (orgContext.OrganizationId.HasValue)
{
// User is operating in org context
// They are guaranteed to be a member (middleware validated this)
var orgId = orgContext.OrganizationId.Value;
return Ok(GetDataForOrganization(orgId));
}
else
{
// No org context - return user-level data only
return Ok(GetDataForUser());
}
}
When to Use Organization Context
Not every API call requires organization context. Some operations are naturally user-scoped (updating profile, changing password, listing available organizations), while others are organization-scoped (inviting teammates, viewing organization members, accessing organization resources).
A good rule of thumb: if the operation affects or queries data within a specific organization, require the header. If it operates at the user level, make the header optional and adjust behavior based on whether it's present.
The /users/me/permissions endpoint demonstrates this pattern. Without the header, it returns user-level permissions. With the header, it merges organization-scoped permissions for the specified organization. The same endpoint serves both use cases based on header presence.
Troubleshooting Common Issues
"403 Forbidden" when setting X-Organization-Id
This indicates the organization ID in the header doesn't match any of your org:memberships claims. Call /users/me/organizations to get your current organization list and verify you're using a valid ID.
If you were recently invited to an organization, your token might not include it yet. Refresh your access token to update claims, then try again.
Permissions vary between requests unexpectedly
If permissions seem to change randomly, check whether you're consistently setting the X-Organization-Id header. Requests without the header return user-level permissions only. Requests with the header return merged permissions. This can look like inconsistent behavior if header inclusion varies across your application.
Middleware order causes authentication errors
UseOrganizationContextFromHeader() must run after UseAuthentication(). If you register it before authentication middleware, user claims won't be available and validation will fail. Check your middleware registration order in Program.cs.
Related Documentation
Guides:
- Identity Permissions Claim - How permissions merge with organization context
- Full-Stack Integration Guide - Implementing header-based context in React applications
Playbooks:
- Admin API Smoke Test - Testing organization context validation
Package References:
- Identity.Base.Organizations - Configuration options for organization context middleware
Changelog:
- CHANGELOG.md - Breaking changes in 0.6.3
Questions about migrating to header-based organization context? Start a discussion: https://github.com/Amaretto-Software-Labs/identity-base/discussions