How the generate API keys on existing user without new registration

Is there a way to create new api keys on existing users? It looks api keys are only generated automatically on new user registrations (OnRegistered event).

Seems like you would want to generate keys during init database of apiKeys for any existing users.

Thanks,
Brian

You can basically just do what the ApiKeyAuthProvider does to generate new keys for new users, you’ll just need to do this for each user missing keys.

We don’t have the necessary APIs to do this for all supported API Key Auth Repositories, but here’s an example of how you could create API Keys for existing users without API Keys by registering an AfterInitCallback in your AppHost (so it’s run after all plugins are registered):

AfterInitCallbacks.Add(host =>
{
    var authProvider = (ApiKeyAuthProvider)
        AuthenticateService.GetAuthProvider(ApiKeyAuthProvider.Name);
    using (var db = host.TryResolve<IDbConnectionFactory>().Open())
    {
        var userWithKeysIds = db.Column<string>(db.From<ApiKey>()
            .SelectDistinct(x => x.UserAuthId)).Map(int.Parse);

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

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

If you’re using a different Auth Repository you’ll just need to change it to get the existing UserIds that don’t have keys.

I tried this and the authProvider is null here:

            var authProvider = (ApiKeyAuthProvider)
                AuthenticateService.GetAuthProvider(ApiKeyAuthProvider.Name);

Here’s my config auth section:

        /// <summary>
    /// Creates E2HUserSession, initializes JWT,APIKey,and E2H authproviders, initializes db auth repository.
    /// </summary>
    /// <param name="container"></param>
    private void ConfigureAuth(Container container)
    {
        // Register AuthFeature with custom user session and custom auth provider
        Plugins.Add(new AuthFeature(
            () => new E2HUserSession(),
            new IAuthProvider[]
            {
                //new JwtAuthProvider(),
                new BasicAuthProvider(),
                new ApiKeyAuthProvider() {},
                new E2HAuthProvider(Resolve<IUserService>())
            }

        )
        { HtmlRedirect = "~/", MaxLoginAttempts = 0, IncludeRegistrationService = true });


        ////save auth info to memory
        //var authRep = new InMemoryAuthRepository();
        //container.Register<IAuthRepository>(authRep);

        //save auth info to tables
        var authRep = new OrmLiteAuthRepository(Resolve<IDbConnectionFactory>()) { UseDistinctRoleTables = true };
        container.Register<IAuthRepository>(authRep);
        authRep.InitSchema();

        MigrateE2HUsers(container);

        AfterInitCallbacks.Add(host =>
        {
            var authProvider = (ApiKeyAuthProvider)
                AuthenticateService.GetAuthProvider(ApiKeyAuthProvider.Name);
            using (var db = host.TryResolve<IDbConnectionFactory>().Open())
            {
                var userWithKeysIds = db.Column<string>(db.From<ApiKey>()
                    .SelectDistinct(x => x.UserAuthId)).Map(int.Parse);

                var userIdsMissingKeys = db.Column<string>(db.From<UserAuth>()
                    .Where(x => !userWithKeysIds.Contains(x.Id))
                    .Select(x => x.Id));

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

ok cool I’ll look into why that is, but here’s another way you can fetch the ApiKeyAuthProvider:

AfterInitCallbacks.Add(host =>
{
    var authProvider = (ApiKeyAuthProvider) AuthenticateService.GetAuthProviders()
        .First(x => x is ApiKeyAuthProvider);
    using (var db = host.TryResolve<IDbConnectionFactory>().Open())
    {
        var userWithKeysIds = db.Column<string>(db.From<ApiKey>()
            .SelectDistinct(x => x.UserAuthId)).Map(int.Parse);

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

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

Whilst you can also just reuse the instance you registered in theAuthFeature, e.g:

var authProvider = new ApiKeyAuthProvider(AppSettings);

Plugins.Add(new AuthFeature(
    () => new E2HUserSession(),
    new IAuthProvider[] {
        new BasicAuthProvider(),
        new ApiKeyAuthProvider() {},
        new E2HAuthProvider(Resolve<IUserService>())
    }
);

AfterInitCallbacks.Add(host => {
    //...
    var apiKeys = authProvider.GenerateNewApiKeys(userId.ToString());
});

ok the issue was due to not passing in an AppSettings when registering the Auth Provider, e.g:

    new IAuthProvider[] {
        new BasicAuthProvider(),
        new ApiKeyAuthProvider() {}, 
        new E2HAuthProvider(Resolve<IUserService>())
    }

Instead of:

    new IAuthProvider[] {
        new BasicAuthProvider(AppSettings),
        new ApiKeyAuthProvider(AppSettings) {}, 
        new E2HAuthProvider(Resolve<IUserService>())
    }

It’s a good idea to initialize it with an AppSettings even if you’re not using it as it later lets you customize it using any AppSettings without recompiling or redeploying, but I’ve changed it to call the same base constructor in this commit which will let you fetch the AuthProvider by name in the next release.

Awesome! That worked.

I had to break up your missing keys linq query because the where condition was evaluating always false for some reason.

This worked:

        AfterInitCallbacks.Add(host =>
        {
            var authProvider = (ApiKeyAuthProvider)
                AuthenticateService.GetAuthProvider(ApiKeyAuthProvider.Name);
            using (var db = host.TryResolve<IDbConnectionFactory>().Open())
            {
                var userWithKeysIds = db.Column<string>(db.From<ApiKey>()
                    .SelectDistinct(x => x.UserAuthId)).Map(int.Parse);

                var userIds = db.Column<int>(db.From<UserAuth>()
                    //.Where(x => !userWithKeysIds.Contains(x.Id))
                    .Select(x => x.Id));

                var userIdsMissingKeys = userIds.Where(x => !userWithKeysIds.Contains(x));

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

Thanks as always for the timely advice!
-br

1 Like

Ok I’ve been able to repro it, it wont work when you have an empty userWithKeysIds collection since the generated query returns 0 results:

SELECT "Id" 
FROM "UserAuth"
WHERE NOT ("Id" In (NULL))

Which illogically in RDBMS-land also returns 0 rows for the inverse condition, i.e

SELECT "Id" 
FROM "UserAuth"
WHERE "Id" In (NULL)

As a workaround you can just add a guard for the empty collection case:

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

Otherwise your modified query also works, just means the filtering is performed on the client instead of in the RDBMS, but it only needs to be run once so either option would work well.