Does new ASP.NET MVC identity framework work without Entity Framework and SQL Server?

26,707

Solution 1

Not that simple. Not that hard either.

You'll have to write your custom implementation of:

  1. IUserStore<TUser>
  2. IUserPasswordStore<TUser>
  3. IUserTwoFactorStore<TUser>
  4. IUserClaimStore<TUser>
  5. IRoleStore<TRole>
  6. IUserSecurityStampStore<TUser, string>
  7. IUserRoleStore<TUser, string>
  8. UserManager<TUser>

Then create your own user implementation, from IUser<TKey>, like:

public class MyUser : IUser<string>
{
    public string Id { get; set; }
    public string UserName { get; set; }
}

Finally, from NuGet, remove AspNet.Identity.EntityFramework, which will remove EntityFramework too if you're not using it elsewhere.

Wherever your code breaks, rewrite it to use your custom implementations.

Tip

Create a MyUserRepository which implements items from 1 to 7.

Then, create a MyUserManager which implements item 8.

It will be damn easy to wire that up in place of default AspNet.Identity.EntityFramework classes.

Solution 2

To piggy-back on what ALMMa said, when I was working on my own custom implementation, I found this article to be invaluable:

Overview of Custom Storage Providers for ASP.NET Identity

It details no only what Interfaces need to be implemented, but goes into detail on how to implement them and gives code sample references to an actual MySQL implementation.

Solution 3

You just need to override some classes in the following manner to get basic role based authentication working without Entity Framework and SQL..

public partial class Startup
{
    public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
    public void ConfigureAuth(IAppBuilder app)
    {
        // Configure the db context, user manager and role manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

        // Enable the application to use a cookie to store information for the signed in user
        // and to use a cookie to temporarily store information about a user logging in with a third party login provider
        app.UseCookieAuthentication(new CookieAuthenticationOptions());
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Configure the application for OAuth based flow
        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/Token"),
            Provider = new ApplicationOAuthProvider(),
            AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            // In production mode set AllowInsecureHttp = false
            AllowInsecureHttp = true
        };

        // Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(OAuthOptions);
    }
}

public class ApplicationUser : IUser
{
    public ApplicationUser()
    {
        Id = Guid.NewGuid().ToString();
        Roles = new List<string>();
    }

    public virtual string Email { get; set; }
    public List<string> Roles { get; set; }
    public virtual string Password { get; set; }
    public DateTime CreatedTime { get; set; }

    public DateTime UpdatedTime { get; set; }

    public string Id { get; }
    public string UserName { get; set; }

    public virtual void AddRole(string role)
    {
        Roles.Add(role);
    }

    public virtual void RemoveRole(string role)
    {
        Roles.Remove(role);
    }
}

public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store)
            : base(store)
        {
        }

        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options,
            IOwinContext context)
        {
            var manager =
                new ApplicationUserManager(
                    new UserStoreService<ApplicationUser>(context.Get<ApplicationDbContext>().Users));
            manager.PasswordHasher = new FusionPasswordHasher();

            // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = false,
                RequireDigit = false,
                RequireLowercase = false,
                RequireUppercase = false
            };

            // Configure user lockout defaults
            manager.UserLockoutEnabledByDefault = true;
            manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
            manager.MaxFailedAccessAttemptsBeforeLockout = 5;

            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
                manager.UserTokenProvider =
                    new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
            return manager;
        }

        public virtual async Task<IdentityResult> AddUserToRolesAsync(string userId, IList<string> roles)
        {
            var userRoleStore = (IUserRoleStore<ApplicationUser, string>) Store;

            var user = await FindByIdAsync(userId).ConfigureAwait(false);
            if (user == null)
                throw new InvalidOperationException("Invalid user Id");

            var userRoles = await userRoleStore.GetRolesAsync(user).ConfigureAwait(false);
            // Add user to each role using UserRoleStore
            foreach (var role in roles.Where(role => !userRoles.Contains(role)))
                await userRoleStore.AddToRoleAsync(user, role).ConfigureAwait(false);

            // Call update once when all roles are added
            return await UpdateAsync(user).ConfigureAwait(false);
        }

        public virtual async Task<IdentityResult> RemoveUserFromRolesAsync(string userId, IList<string> roles)
        {
            var userRoleStore = (IUserRoleStore<ApplicationUser, string>) Store;

            var user = await FindByIdAsync(userId).ConfigureAwait(false);
            if (user == null)
                throw new InvalidOperationException("Invalid user Id");

            var userRoles = await userRoleStore.GetRolesAsync(user).ConfigureAwait(false);
            // Remove user to each role using UserRoleStore
            foreach (var role in roles.Where(userRoles.Contains))
                await userRoleStore.RemoveFromRoleAsync(user, role).ConfigureAwait(false);

            // Call update once when all roles are removed
            return await UpdateAsync(user).ConfigureAwait(false);
        }
    }

If you want to read all the users in one short and store in the memory than you use the below style. And I strongly recommend you to read user only at the time of login for the you need to add your logic in "UserStoreService" class.

public class ApplicationDbContext : IDisposable
{
    private ApplicationDbContext(IList<ApplicationUser> users)
    {
        Users = users;
    }

    public IList<ApplicationUser> Users { get; set; }

    public void Dispose()
    {
    }

    public static ApplicationDbContext Create()
    {
        //You can use any database and hook it here

        var users = new List<ApplicationUser>
        {
            new ApplicationUser
            {
                UserName = "[email protected]",
                Email = "[email protected]",
                Password = "test",
                Roles = new List<string> {"Admin", "Admin2"}
            },
            new ApplicationUser
            {
                UserName = "[email protected]",
                Email = "[email protected]",
                Password = "test2",
                Roles = new List<string> {"Admin"}
            }
        };

        return new ApplicationDbContext(users);
    }
}

public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();

        var user = await userManager.FindAsync(context.UserName.ToLower(), context.Password);

        if (user == null)
        {
            context.SetError("invalid_grant", "The user name or password is incorrect.");
            return;
        }

        try
        {
            var oAuthIdentity = await userManager.CreateIdentityAsync(user, context.Options.AuthenticationType);
            var cookiesIdentity = await userManager.CreateIdentityAsync(user,
                CookieAuthenticationDefaults.AuthenticationType);
            var props = new AuthenticationProperties(new Dictionary<string, string>
            {
                {
                    "client_id", context.ClientId == null ? string.Empty : context.ClientId
                },
                {
                    "userName", context.UserName
                }
            });
            var ticket = new AuthenticationTicket(oAuthIdentity, props);
            context.Validated(ticket);
            context.Request.Context.Authentication.SignIn(cookiesIdentity);
        }
        catch (Exception ex)
        {
            Trace.TraceError("FUSION Error ::: " + ex.Message + ex.InnerException);
            Trace.TraceError(ex.Message);
        }
    }

    public override Task TokenEndpoint(OAuthTokenEndpointContext context)
    {
        foreach (var property in context.Properties.Dictionary)
            if (property.Value != null)
                context.AdditionalResponseParameters.Add(property.Key, property.Value);

        return Task.FromResult<object>(null);
    }

    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        // Resource owner password credentials does not provide a client ID.
        if (context.ClientId == null)
            context.Validated();

        return Task.FromResult<object>(null);
    }
}

public class AppPasswordHasher : IPasswordHasher
{
    public string HashPassword(string password)
    {
        return password;
    }

    public PasswordVerificationResult VerifyHashedPassword
        (string hashedPassword, string providedPassword)
    {
        if (hashedPassword == HashPassword(providedPassword))
            return PasswordVerificationResult.Success;
        return PasswordVerificationResult.Failed;
    }
}

Method like "FindByNameAsync" ; where you need to read user from db on demand/login

public class UserStoreService<TUser> : IUserStore<TUser>,
    IUserPasswordStore<TUser>,
    IUserRoleStore<TUser>
    where TUser : ApplicationUser
{
    private readonly IList<TUser> _users;


    public UserStoreService(IList<TUser> users)
    {
        _users = users;
    }

    public virtual Task SetPasswordHashAsync(TUser user, string passwordHash)
    {
        user.Password = passwordHash;
        return Task.FromResult(0);
    }

    public virtual Task<string> GetPasswordHashAsync(TUser user)
    {
        return Task.FromResult(user.Password);
    }

    public virtual Task<bool> HasPasswordAsync(TUser user)
    {
        return Task.FromResult(user.Password != null);
    }

    public virtual Task AddToRoleAsync(TUser user, string roleName)
    {
        user.AddRole(roleName);
        return Task.FromResult(0);
    }

    public virtual Task RemoveFromRoleAsync(TUser user, string roleName)
    {
        user.RemoveRole(roleName);
        return Task.FromResult(0);
    }

    public virtual Task<IList<string>> GetRolesAsync(TUser user)
    {
        return Task.FromResult((IList<string>) user.Roles);
    }

    public virtual Task<bool> IsInRoleAsync(TUser user, string roleName)
    {
        return Task.FromResult(user.Roles.Contains(roleName));
    }

    public virtual void Dispose()
    {
    }

    public virtual Task CreateAsync(TUser user)
    {
        user.CreatedTime = DateTime.Now;
        user.UpdatedTime = DateTime.Now;
        _users.Add(user);
        return Task.FromResult(true);
    }

    public virtual Task UpdateAsync(TUser user)
    {
        // todo should add an optimistic concurrency check
        user.UpdatedTime = DateTime.Now;
        _users.Remove(user);
        _users.Add(user);
        return Task.FromResult(true);
    }

    public virtual Task DeleteAsync(TUser user)
    {
        return Task.FromResult(_users.Remove(user));
    }

    public virtual Task<TUser> FindByIdAsync(string userId)
    {
        return Task.FromResult(_users.FirstOrDefault(u => u.Id == userId));
    }

    public virtual Task<TUser> FindByNameAsync(string userName)
    {
        // todo exception on duplicates? or better to enforce unique index to ensure this
        return Task.FromResult(_users.FirstOrDefault(u => u.Email == userName));
    }
}

[Authorize(Roles = "Admin")]
public class RolesController : ApiController
{
    public IEnumerable<string> Get()
    {
        return new[] {"value3", "value4"};
    }
}

Source Code (github)

Solution 4

It is bound to Entity Framework and SQL Server by default, but you can easily plug in other data stores such as SharePoint, Windows Azure Storage Table Service, NoSQL databases, etc., and you get to retain control of the database schema.

Further Reading
Introduction to ASP.NET Identity

Share:
26,707
Aref
Author by

Aref

I am the Principle Enterprise Architect in a major telecom company. That role involves overseeing the architecture of our integrations with other telecom companies and all the sub-brands that use our telecom network, metering and billing, and CRM systems. Since I enjoy building software as well I have launched a few SaaS businesses such as Save On Clouds (saveonclouds.com), Interview Manager (interviewman.com), and My Secret Babe (mysecretbabe.com).

Updated on October 09, 2020

Comments

  • Aref
    Aref over 3 years

    I am new to ASP.NET MVC 5 and so I am trying to use it as much as possible to learn it by practice.

    So I am thinking of using the new OWIN implementation of ASP.NET MVC to implement the authentication and authorization of my project. That said, I am building the project in a way that it can work with various types of databases.

    So far I have used generic ADO.NET elements (e.g. DbDataReader etc) and I have refused to use any ORM. So I am wondering if I can go ahead with using the new identity system of ASP.NET or will I be bound to Entity Framework and SQL Server if I do so?

  • ThisGuyKnowsCode
    ThisGuyKnowsCode over 8 years
    IRoleStore and IUserStore both implement a FindByIdAsync method with the same signature. Might it be a good idea to implement IRoleStore and IUserRoleStore in their own repositories?
  • McGuireV10
    McGuireV10 over 6 years
    Step by step for the latest version... markjohnson.io/articles/…