Best Practice for Endpoints that Accept Both User Auth and API Keys?

Hi Mythz,

I’m working on a SPA with a ServiceStack API backend. The SPA uses session authentication for users, but I also want to provide API access as well. I’ve been reading the docs about the new ApiKeysFeature in v8.3+ and want to make sure I understand the recommended pattern correctly.

Current Situation:

  • The new ApiKeysFeature requires using [ValidateApiKey] attribute
  • User authentication requires [ValidateIsAuthenticated] attribute
  • These attributes are mutually exclusive - an endpoint can’t accept both

Understanding the Pattern:

Based on the documentation, it seems I need completely separate endpoints, so where previously it would be one GetOrders endpoint, it would now look something like below:

// Base request DTO with shared properties
public abstract class GetOrdersBase : IReturn<List<Order>> 
{
    public DateTime? FromDate { get; set; }
    public string? CustomerId { get; set; }
    public string? Status { get; set; }
}

// For authenticated users (SPA calls this)
[ValidateIsAuthenticated]
[Route("/orders", "GET")]
public class GetOrders : GetOrdersBase { }

// For API key access (external partners)
[ValidateApiKey("orders:read")] // with scope
[Route("/api/orders", "GET")]
public class GetOrdersViaApiKey : GetOrdersBase { }

// Service implementation
public class OrderService : Service
{
    private List<Order> GetOrdersImpl(GetOrdersBase request, string userId)
    {
        // Shared business logic
        var q = Db.From<Order>();
        q.Where(x => x.UserId == userId);

        if (request.FromDate.HasValue)
          q.Where(x => x.CreatedDate >= request.FromDate);
        if (!string.IsNullOrEmpty(request.CustomerId))
          q.Where(x => x.CustomerId == request.CustomerId);

        return Db.Select(q);
    }
    
    public List<Order> Get(GetOrders request)
    {
        // Get userId from authenticated session
        var userId = GetSession().UserAuthId;
        return GetOrdersImpl(request, userId);
    }
    
    public List<Order> Get(GetOrdersViaApiKey request)
    {
        // Get userId from API key (if associated with user)
        var apiKey = Request.GetApiKey();
        if (string.IsNullOrEmpty(apiKey?.UserId))
            throw new UnauthorizedAccessException("API Key must be associated with a user");
            
        return GetOrdersImpl(request, apiKey.UserId);
    }
}

This seems to add a lot of boilerplate:

  • Base DTOs for every endpoint that needs dual auth
  • Duplicate concrete DTOs with no additional properties
  • Duplicate service methods that just delegate to shared implementation
  • Different routes for what is logically the same operation

Am I understanding correctly that this is the intended pattern?

My specific requirement is that I need to avoid authentication requests for API calls because latency is critical for my use case - these APIs are being called by AI tools and I’m fighting to keep latency as low as possible. I need to be able to make machine-to-machine requests without an initial auth request that will add latency to the tool request.

Should I be adding code like my example for all my endpoints where I need API key auth (which is most endpoints), or should I be handling this differently?

Thanks for any guidance!

They’re independent API endpoint validations which should be for 2 different use-cases, i.e. User → API (Auth) vs Machine → API / User Agent → API (API Key).

However you want to structure them so they have a shared implementation is up to you, I’d personally make use of extension methods to reduce duplicated code, something like:

public static class MyExtensions
{
    public static string GetRequiredUserId(this IRequest? req) =>
        req.GetApiKey()?.UserAuthId ??
        req.GetClaimsPrincipal().GetUserId() ?? 
        throw HttpError.Unauthorized("API Key must be associated with a user");
}

Then you should be able to call your existing API:

public class OrderService : Service
{
    public List<Order> Get(GetOrders request)
    {
        var userId = Request.GetRequiredUserId();
        // Shared business logic
        var q = Db.From<Order>();
        q.Where(x => x.UserId == userId);

        if (request.FromDate.HasValue)
          q.Where(x => x.CreatedDate >= request.FromDate);
        if (!string.IsNullOrEmpty(request.CustomerId))
          q.Where(x => x.CustomerId == request.CustomerId);

        return Db.Select(q);
    }
    
    public List<Order> Get(GetOrdersViaApiKey request) => 
        Get(request.ConvertTo<GetOrders>());
}

Have you thought about using AutoQuery to avoid needing to create the implementation, e.g?

[ValidateIsAuthenticated]
[Route("/orders", "GET")]
public class QueryOrders : QueryDb<Order> { }

// For API key access (external partners)
[ValidateApiKey("orders:read")] // with scope
[Route("/api/orders", "GET")]
public class QueryOrdersApiKey : QueryDb<Order> { }

In the latest ServiceStack v8.9 you can now Protect same APIs with API Keys or Identity Auth

To achieve this, users will need to have a valid API Key generated for them which would then need to be added to the apikey Claim in the UserClaimsPrincipalFactory to be included in their Identity Auth Cookie:

// Program.cs
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, 
    AdditionalUserClaimsPrincipalFactory>();

// Add additional claims to the Identity Auth Cookie
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));
      }
      
      // Add Users latest valid API Key to their Auth Cookie's 'apikey' claim
      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;
  }
}

After which Authenticated Users will be able to access [ValidateApiKey] protected APIs where it attaches the API Key in the apikey Claim to the request - resulting in the same behavior had they sent their API Key with the request.