Search code examples
asp.netentity-frameworkasp.net-identityblazorasp.net-core-identity

Identity Core role authorization with custom Tables and Entity Framework Core


I'm almost there, I implemented my custom authentication using Core Identity because I already have some tables with Users, Roles and a join table with Role assignment to users.

Don't mind bad Model property names, sadly I have to deal with a very badly designed database with not encrypted passwords saved (I did a workaround as you can see in my code below, in order to skip password hashing and it works).

I also started with a very simple hardcoded test for Roles, but I can't reach that code. I called my DbContext as "OracleContext", so my models are scaffolded from existing db.

I'm using .Net Core 3.1 with Blazor Server, I've created my UserStore object implementing the very minimum amount of interfaces: UserStore.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace core_identity_utenza_bms.Data
{
    public class UserStore : IUserStore<Geutenti>, IUserPasswordStore<Geutenti>, IUserRoleStore<Geutenti>
    {
        private readonly OracleContext _oracleContext;

        public UserStore(OracleContext oracleContext)
        {
            _oracleContext = oracleContext;
        }
        public void Dispose()
        {
            //throw new NotImplementedException();
        }

        public async Task<string> GetUserIdAsync(Geutenti user, CancellationToken cancellationToken)
        {
            return user.Cuser.ToString();
        }

        public async Task<string> GetUserNameAsync(Geutenti user, CancellationToken cancellationToken)
        {
            return user.Ruser;
        }

        public async Task SetUserNameAsync(Geutenti user, string userName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<string> GetNormalizedUserNameAsync(Geutenti user, CancellationToken cancellationToken)
        {
            return user.Tuser;
        }

        public async Task SetNormalizedUserNameAsync(Geutenti user, string normalizedName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<IdentityResult> CreateAsync(Geutenti user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<IdentityResult> UpdateAsync(Geutenti user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<IdentityResult> DeleteAsync(Geutenti user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<Geutenti> FindByIdAsync(string userId, CancellationToken cancellationToken)
        {
            var userQuery =
                await _oracleContext.Geutenti.Where(
                    u =>
                        u.Cuser.ToString().Equals(userId) &&
                        u.Bstor.Equals("A") &&
                        u.Csoci.Equals("MASM")).FirstOrDefaultAsync(cancellationToken);
            return userQuery;
        }

        public async Task<Geutenti> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
        {
            var userQuery =
                await _oracleContext.Geutenti.Where(
                        u =>
                            u.Ruser.Contains("ARDILLOI") &&
                            u.Bstor.Equals("A") &&
                            u.Csoci.Equals("MASM"))
                    .FirstOrDefaultAsync(cancellationToken);
            return userQuery;
        }

        public async Task SetPasswordHashAsync(Geutenti user, string passwordHash, CancellationToken cancellationToken)
        {
            //throw new NotImplementedException();
        }

        public async Task<string> GetPasswordHashAsync(Geutenti user, CancellationToken cancellationToken)
        {
            return user.CpaswDuser;
        }

        public async Task<bool> HasPasswordAsync(Geutenti user, CancellationToken cancellationToken)
        {
            return !(user.CpaswDuser == null || user.CpaswDuser.Equals(string.Empty));
        }

        /*
         * UserRole
         */
        public async Task AddToRoleAsync(Geutenti user, string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task RemoveFromRoleAsync(Geutenti user, string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<IList<string>> GetRolesAsync(Geutenti user, CancellationToken cancellationToken)
        {
            //var userRoles = _oracleContext.Geruxute.
            return new List<string> {"BURBERO", "BARBARO"};
        }

        public async Task<bool> IsInRoleAsync(Geutenti user, string roleName, CancellationToken cancellationToken)
        {
            return new List<string> { "BURBERO", "BARBARO" }.Contains(roleName);

        }

        public async Task<IList<Geutenti>> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
    }
}

Then I implemented a class for skipping password hashing: NoPasswordHasher.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;

namespace core_identity_utenza_bms.Data
{
    public class NoPasswordHasher : IPasswordHasher<Geutenti>
    {
        public string HashPassword(Geutenti user, string password)
        {
            return password;
        }

        public PasswordVerificationResult VerifyHashedPassword(Geutenti user, string hashedPassword, string providedPassword)
        {
            return hashedPassword.Equals(providedPassword) ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
        }
    }
}

I created a RoleStore.cs file for retrieving my Roles on db:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace core_identity_utenza_bms.Data
{
    public class RoleStore : IRoleStore<Geruoliz>
    {
        private readonly OracleContext _oracleContext;

        public RoleStore(OracleContext oracleContext)
        {
            _oracleContext = oracleContext;
        }
        public void Dispose()
        {
            //throw new NotImplementedException();
        }

        public async Task<IdentityResult> CreateAsync(Geruoliz role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<IdentityResult> UpdateAsync(Geruoliz role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<IdentityResult> DeleteAsync(Geruoliz role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<string> GetRoleIdAsync(Geruoliz role, CancellationToken cancellationToken)
        {
            return role.Crule.ToString();
        }

        public async Task<string> GetRoleNameAsync(Geruoliz role, CancellationToken cancellationToken)
        {
            return role.Rrule;
        }

        public async Task SetRoleNameAsync(Geruoliz role, string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<string> GetNormalizedRoleNameAsync(Geruoliz role, CancellationToken cancellationToken)
        {
            return role.Rrule;
        }

        public async Task SetNormalizedRoleNameAsync(Geruoliz role, string normalizedName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<Geruoliz> FindByIdAsync(string roleId, CancellationToken cancellationToken)
        {
            var role = await _oracleContext.Geruoliz.Where(
                    r =>
                        r.Crule.ToString().Equals(roleId) &&
                        r.Bstor.Equals("A") &&
                        r.Csoci.Equals("MASM"))
                .FirstOrDefaultAsync(cancellationToken: cancellationToken);
            return role;
        }

        public async Task<Geruoliz> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
        {
            var role = await _oracleContext.Geruoliz.Where(
                    r =>
                        r.Rrule.Equals(normalizedRoleName) &&
                        r.Bstor.Equals("A") &&
                        r.Csoci.Equals("MASM"))
                .FirstOrDefaultAsync(cancellationToken: cancellationToken);
            return role;
        }
    }
}

And finally i edited Startup.cs:

services.AddDbContext<OracleContext>();
services
    .AddDefaultIdentity<Geutenti>()
    .AddUserStore<UserStore>();
services.AddScoped<IPasswordHasher<Geutenti>, NoPasswordHasher>();

By now I can't even enter the breakpoints in UserStore.GetRolesAsync and UserStore.IsInRoleAsync, as I imagine I should pass by these checks in order to determine if a view is visible or not, for example by employing:

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<AuthorizeView>
    <p>sei loggato.</p>
</AuthorizeView>
<AuthorizeView Roles="BURBERO">
    <Authorized>
        Sei un burbero!
    </Authorized>
    <NotAuthorized>
        Sei una brava persona.
    </NotAuthorized>
</AuthorizeView>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

I only succeed in Authorize view not specifying any role. The last thing I tried is to edit services like:

services.AddDbContext<OracleContext>();
services
    .AddDefaultIdentity<Geutenti>()
    .AddUserStore<UserStore>()
    .AddRoleStore<RoleStore>();
services.AddScoped<IPasswordHasher<Geutenti>, NoPasswordHasher>();

But it gives me exception:

System.InvalidOperationException: 'No RoleType was specified, try AddRoles<TRole>().'

By now I honestly feel overwhelmed by scattered information around the web. Any help? Thank you very much for your time!


Solution

  • I found my way around. It was both a problem cache related but also as Startup setting. All the code above is working, I had to set in Startup:

    // Add identity types
    services.AddDbContext<OracleContext>();
    services
        .AddIdentity<Geutenti, Geruoliz>()
        .AddRoles<Geruoliz>()
        .AddDefaultTokenProviders();
    // Identity Services
    services.AddTransient<IUserStore<Geutenti>, UserStore>();
    services.AddTransient<IRoleStore<Geruoliz>, RoleStore>();
    services.AddScoped<IPasswordHasher<Geutenti>, NoPasswordHasher>();
    

    The objects here are:

    • OracleContext: it's the database context object from Entity Framework Core that wraps up my db that contains my three user tables
    • Geutenti: Users table, with all related data (username, password, email, etc.)
    • Geruoliz: Roles table
    • UserStore and RoleStore: objects that implement those interfaces that inform Core Identity how to access user-related data for authentication and authorization
    • NoPasswordHasher: workaround to skip password hashing. (DESCLAIM: this introduces a security hole, you should never save passwords as-is, but their hash)

    I had to logout/erase cookies and restart the project, login et voilà!