Authenticate users against a database

If users are defined and stored in an authentication database (instead of a file), you can modify the DatabaseUserStore.cs file to

  • Connect JWT server to the database and authenticate the user every time when a token is requested; or

  • Connect JWT server to the database, get users from the database, store them in an array, and then authenticate the user against the users in the array when a token is requested.

    The benefit of populating and caching users in an array is the JWT server does not need to connect to the authentication database every time when a user is authenticated, but the downside is if the users in the authentication database are updated, the users in the array will not be refreshed until the PowerServer Web APIs is restarted.

The following will show you how to connect with the authentication database, populate and cache users in an array, and then authenticate users against the array.

Step 1: Open the DatabaseUserStore.cs file and add scripts to connect with the authentication database, populate users from the database to an array, and authenticate users against the array.

Suppose a SQL Server database will be connected. You should modify the database connection string according to your environment.

using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using IdentityModel;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using SnapObjects.Data;
using SnapObjects.Data.SqlServer;

namespace ServerAPIs
{
    public class DatabaseUserStore : IUserStore
    {
        private readonly ILogger _logger;
        private readonly List<UserModel> _users;
        
        public DatabaseUserStore(ILogger<DatabaseUserStore> logger)
        {
            _logger = logger;
            _users = new List<UserModel>();
            string Constr = @"Data Source=172.16.1.10,1433;Initial Catalog=pb_cloud;Integrated Security=False;User ID=sa;Password=1234;Pooling=True;Min Pool Size=0;Max Pool Size=100;MultipleActiveResultSets=False;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite";
            SqlServerDataContext _context = new SqlServerDataContext(new SqlConnection(Constr));
            string sql = "select username,password from users where isValid = 1";
            var users = _context.SqlExecutor.Select<DynamicModel>(sql);
            foreach (var u in users)
            {
                _users.Add(new UserModel
                {
                    Username = u.GetValue<string>(0),
                    Password = u.GetValue<string>(1),
                    Claims = new[]
                    {
                        new Claim(JwtClaimTypes.Name, u.GetValue<string>(0)),
                        new Claim(JwtClaimTypes.Scope, "serverapi"),  //this script is added because scope is enabled by default
                    },
                });
            };
            
        }
        
        public UserModel ValidateCredentials(string username, string password)
        {
            var user = _users.FirstOrDefault(x => x.Username == username && x.Password == password);
            if (user != null)
            {
                _logger.LogInformation($"User <{username}> logged in.");
                
                return user;
            }
            else
            {
                _logger.LogError($"Invalid login attempt.");
                
                return default;
            }
        }
    }
}

To connect with a database type different from SQL Server, add the following namespace accordingly.

using SnapObjects.Data.MySql;
using SnapObjects.Data.Oracle;
using SnapObjects.Data.PostgreSql;
using SnapObjects.Data.Odbc;

Step 2: Open the AuthenticationExtensions.cs file and modify the script to inject the DatabaseUserStore class instead of the DefaultUserStore class.

//services.AddSingleton<IUserStore, DefaultUserStore>();
services.AddSingleton<IUserStore, DatabaseUserStore>();