J Pollack
J Pollack

Reputation: 2858

Minimum API Key ServiceStack authentication + authorization

I would like to use API key to access secured ServiceStack web service simply as possible:

I am able to call a service with Bearer token (API key). It returns 200 Forbidden.

ApiKeyAuthProvider.AuthenticateAsync():

// authRepo is ServiceStack.Auth.OrmLiteAuthRepositoryMultitenancy
var userAuth = await authRepo.GetUserAuthAsync(apiKey.UserAuthId, token).ConfigAwait();

userAuth is NULL and this will throw this exception:

throw HttpError.Unauthorized(ErrorMessages.UserForApiKeyDoesNotExist.Localize(authService.Request));

I store my API keys at the 'ApiKey' table in SQL database:

public override void Configure(Container container) { string connectionString = GetConnectionStringByName("Main"); // Create and register an OrmLite DB Factory configured to use Live DB by default var dbFactory = new OrmLiteConnectionFactory(connectionString, SqlServerDialect.Provider); container.Register(dbFactory);

// Tell ServiceStack you want to persist User Auth Info in SQL Server
container.Register<IAuthRepository>(c => new OrmLiteAuthRepository(dbFactory) { UseDistinctRoleTables = true });

// It’s safe to always call this in your AppHost as it’s just ignored if you already have the tables created
container.Resolve<IAuthRepository>().InitSchema();

Plugins.Add(new AuthFeature(
    () => new AuthUserSession(),
    new IAuthProvider[]
    {
        new ApiKeyAuthProvider(AppSettings) {RequireSecureConnection = false}
    }));

}

Upvotes: 1

Views: 644

Answers (1)

mythz
mythz

Reputation: 143399

The API Key AuthProvider may not suit your use-case as it's designed to generate API Keys for registered users to provide an alternative way for them to call protected APIs.

To be able to model this using ServiceStack's built-in Auth API Key Auth Providers I would still have a registered AuthProvider and users representing the client that would use the API Keys.

But instead of providing User registration functionality, add them into the database manually then Generating API Keys for Existing Users.

You'll need to configure your preferred RDBMS to store the API Keys and Users in:

[assembly: HostingStartup(typeof(MyApp.ConfigureDb))]
public class ConfigureDb : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices((context, services) => 
            services.AddSingleton<IDbConnectionFactory>(
                new OrmLiteConnectionFactory(                  
                    context.Configuration.GetConnectionString("DefaultConnection"),
                    SqliteDialect.Provider)));
}

Configure ServiceStack's Auth Feature configured with the API Key AuthProvider:

[assembly: HostingStartup(typeof(MyApp.ConfigureAuth))]
public class ConfigureAuth : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureAppHost(appHost =>
        {
            appHost.Plugins.Add(new AuthFeature(() => new AuthUserSession(),
                new IAuthProvider[] {
                    new ApiKeyAuthProvider(appHost.AppSettings) {
                       RequireSecureConnection = false,
                       SessionCacheDuration = TimeSpan.FromMinutes(10),
                    }
                }));
        });
}

Then configure an RDBMS OrmLiteAuthRepository pre-populated with the clients you want to allow access to, then generate any missing API Keys for them on startup:

[assembly: HostingStartup(typeof(MyApp.ConfigureAuthRepository))]
public class ConfigureAuthRepository : IHostingStartup
{
    public void Configure(IWebHostBuilder builder) => builder
        .ConfigureServices(services => services.AddSingleton<IAuthRepository>(c =>
            new OrmLiteAuthRepository(c.Resolve<IDbConnectionFactory>())))
        .ConfigureAppHost(appHost => {
            var authRepo = appHost.Resolve<IAuthRepository>();
            authRepo.InitSchema();
            CreateUser(authRepo, "[email protected]", "Admin User", "p@55wOrd", 
                roles: new[] { RoleNames.Admin });
            CreateUser(authRepo, "[email protected]", "Client Admin", "p@55wOrd", 
                roles: new[] { "ClientAdmin", "Client" });
            CreateUser(authRepo, "[email protected]", "Client User", "p@55wOrd", 
                roles: new[] { "Client" });
        }, 
        afterAppHostInit: appHost => {
            var authProvider = (ApiKeyAuthProvider)
                AuthenticateService.GetAuthProvider(ApiKeyAuthProvider.Name);
            
            using var db = appHost.TryResolve<IDbConnectionFactory>().Open();
            var userWithKeysIds = db.Column<string>(db.From<ApiKey>()
                .SelectDistinct(x => x.UserAuthId)).Map(int.Parse);

            // Use custom UserAuth if configured
            var userIdsMissingKeys = db.Column<string>(db.From<UserAuth>()
                .Where(x => userWithKeysIds.Count == 0 || !userWithKeysIds.Contains(x.Id))
                .Select(x => x.Id));

            var authRepo = (IManageApiKeys)appHost.TryResolve<IAuthRepository>();
            foreach (var userId in userIdsMissingKeys)
            {
                var apiKeys = authProvider.GenerateNewApiKeys(userId);
                authRepo.StoreAll(apiKeys);
            }
        });

    // Add initial Users to the configured Auth Repository
    public void CreateUser(IAuthRepository authRepo, string email, string name, string password, string[] roles)
    {
        if (authRepo.GetUserAuthByUserName(email) == null)
        {
            var newAdmin = new AppUser { Email = email, DisplayName = name };
            var user = authRepo.CreateUserAuth(newAdmin, password);
            authRepo.AssignRoles(user, roles);
        }
    }
}

This will let you protect access to different APIs using role-based auth:

[ValidateIsAdmin]
public class AdminOnly { ... }

[ValidateHasRole("ClientAdmin")]
public class ClientAdminOnly { ... }

[ValidateHasRole("Client")]
public class AnyClient { ... }

Note: The Admin is a super user role that can access any protected API

If you don't want all these Auth components for your App you'll have to create your own Custom Auth Provider that implements its own Authentication which doesn't need to use any other components as it has full control over how the request is authenticated.

You can refer to the existing ApiKeyAuthProvider.cs for a guide on how to implement an API Key IAuthWithRequest Auth Provider that validates the BearerToken in its PreAuthenticateAsync() method.

Upvotes: 1

Related Questions