I’m using ServiceStack 10.0.6 with IdentityAuth.For<ApplicationUser>() and ApiKeysFeature. I’ve set up the v8.9+ feature where [ValidateApiKey] works for both cookie-authenticated users (via the apikey claim in the Identity cookie) and external API key consumers. That part works great.
I’d like to consolidate my duplicate admin/API endpoints into a single set using [ValidateApiKey], but the issue is that API key requests don’t have a user session. So [RequiredRole], [ValidateHasRole], and SessionAs<T>() don’t work for those requests — even though my API keys have a user_id linked.
I know ApiKeyCredentialsProvider handled this in the older auth system by creating sessions from API keys. What’s the equivalent approach when using IdentityAuth? Is there a built-in way to populate a user session from the API key’s linked user so that session-based attributes and SessionAs<T>() work the same for both auth paths?
Thanks for the link. I’ve already implemented that pattern — UserClaimsPrincipalFactory injects the API key into the cookie claim, and [ValidateApiKey] works for both cookie and API key users. That part’s working well.
My question is specifically about the API key-only path (no cookie). When a request comes in via X-Api-Key header, there’s no user session — so [RequiredRole], [ValidateHasRole], and SessionAs<T>() don’t work, even though the API key has a user_id linked to an Identity user.
Is there a built-in way to populate a user session from the API key’s linked user on IdentityAuth? Or is the intended approach to use API key scopes for authorization and Request.GetApiKey() for user context instead of session-based attributes?
Thanks — yes, I’ve implemented it matching the docs pattern. Here’s my factory:
public class AdditionalUserClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IApiKeySource apiKeySource,
IOptions<IdentityOptions> optionsAccessor)
: UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>(
userManager, roleManager, optionsAccessor)
{
public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
var identity = (ClaimsIdentity)principal.Identity!;
var claims = new List<Claim>();
if (user.ProfileUrl != null)
claims.Add(new Claim(JwtClaimTypes.Picture, user.ProfileUrl));
var latestKey = (await apiKeySource.GetApiKeysByUserIdAsync(user.Id))
.OrderByDescending(x => x.CreatedDate)
.FirstOrDefault();
if (latestKey != null)
claims.Add(new Claim(JwtClaimTypes.ApiKey, latestKey.Key));
identity.AddClaims(claims);
return principal;
}
}
This works — cookie-authenticated users can access [ValidateApiKey] endpoints with a full session, roles, everything.
My question is about the other direction: when a request comes in via X-Api-Key header (no cookie), the API key has a user_id linked to an Identity user, but there’s no session populated. So [RequiredRole], [ValidateHasRole], and SessionAs<T>() all fail.
Is there a built-in way to have the API key’s linked user session populated for header-based API key requests? Or should I be using a different approach for role/permission checks on the API key path?
To clarify — I’m not talking about signing in. The cookie flow works perfectly.
I’m asking about stateless API key requests via the X-Api-Key header, where the user never signs in. For example, an external consumer hits my API with:
GET /api/my-endpoint
X-Api-Key: ak-xxxx
The API key has user_id set to an Identity user. [ValidateApiKey] passes. But there’s no user session — so SessionAs<T>() returns an empty session and [ValidateHasRole("Admin")] returns 401, even though the linked user has the Admin role.
Is there a built-in way to populate a user session from the API key’s linked user for these header-only requests? Or are sessions only available through the cookie path?
I dug into the ServiceStack source to understand what’s happening. I found two issues:
1. Session only populated for Admin-scoped API keys
In ApiKeysFeature.RequestFilterAsync, the session is only created when the API key has the Admin scope:
if (apiKey.HasScope(RoleNames.Admin))
{
if (!req.GetClaimsPrincipal().HasRole(RoleNames.Admin))
{
req.SetItem(Keywords.Session, ToAuthUserSession(apiKey));
}
}
Non-admin API keys with a user_id don’t get a session at all. For my use case I need sessions for all API keys that have a linked user — I want regular users to see their own data and admins to see everything, using the same endpoints.
2. Session auth provider mismatch
Even when the session is created (for admin keys), [ValidateHasRole] and [RequiredRole] still return 401. The issue is in AuthenticateAttribute.AuthenticateAsync:
ToAuthUserSession sets AuthProvider = "apikey", but with IdentityAuth the registered providers are IdentityApplicationAuthProvider, IdentityCredentialsAuthProvider, etc. None match "apikey", so IsAuthorized returns false.
What I’m trying to achieve:
Single endpoints using [ValidateApiKey] for both cookie and API key users
API keys with user_id get a proper user session so SessionAs<T>(), [ValidateHasRole], and [RequiredRole] work
Works for both admin and non-admin users
Is there a recommended way to bridge this gap with IdentityAuth, or is this a feature gap?
You can only use the [ValidateApiKey] to authenticate requests, using anything else like [ValidateHasRole], [RequiredRole], etc. forces the use of Identity Auth requests.
You can get the API Key and UserId with:
var apiKey = Request.GetApiKey();
var userId = apiKey.UserAuthId; //Or
var userId = Request.GetRequiredUserId();
From there you can resolve a ClaimsPrincipal with IIdentityAuthContextManager and check it from there, e.g:
public class MyServices(IIdentityAuthContextManager userManager) : Service
{
public async Task<object> Any(GetAccount request)
{
var apiKey = Request.GetApiKey();
var userId = Request.GetRequiredUserId();
userId = apiKey.UserAuthId!;
var user = await userManager.CreateClaimsPrincipalAsync(userId, Request);
return new GetAccountResponse
{
UserId = userId,
Username = user.GetUserName(),
DisplayName = user.GetDisplayName(),
Email = user.GetEmail(),
Roles = user.GetRoles(),
IsAdmin = user.HasRole(RoleNames.Admin),
};
}
}
Thanks for the IIdentityAuthContextManager tip — that was the missing piece. I’m using it in a global request filter to hydrate a CustomUserSession for API key requests, so SessionAs<T>() works across all services without any per-method boilerplate:
GlobalRequestFiltersAsync.Insert(0, async (req, res, dto) =>
{
var apiKey = req.GetApiKey() as ApiKeysFeature.ApiKey;
if (apiKey == null || string.IsNullOrEmpty(apiKey.UserId))
return;
// Skip cookie-authenticated requests (already have a session)
var claimsPrincipal = req.GetClaimsPrincipal();
if (claimsPrincipal?.Identity?.IsAuthenticated == true)
return;
var authContextManager = req.TryResolve<IIdentityAuthContextManager>();
if (authContextManager == null) return;
var user = await authContextManager.CreateClaimsPrincipalAsync(apiKey.UserId, req);
if (user == null) return;
var session = new CustomUserSession
{
UserAuthId = apiKey.UserId,
UserName = user.GetUserName(),
UserAuthName = user.GetUserName(),
DisplayName = user.GetDisplayName(),
Email = user.GetEmail(),
IsAuthenticated = true,
AuthProvider = "credentials",
Roles = user.GetRoles().ToList(),
};
req.SetItem(Keywords.Session, session);
});
Now I can use [ValidateApiKey] on endpoints and SessionAs<CustomUserSession>() in services — works identically for both cookie and API key auth. Roles, permissions, everything comes through from Identity via CreateClaimsPrincipalAsync.
One thing worth noting: I had to check ClaimsPrincipal.Identity.IsAuthenticated rather than session.IsAuthenticated to decide whether to skip. ApiKeysFeature creates an AuthUserSession for admin-scoped keys which breaks SessionAs<CustomUserSession>() — checking the claims principal correctly identifies cookie auth without being tripped up by that.