К основному контенту

Customize ASP.NET Identity 2.0

Recently I found a nice explanation of latest Securing (ASP.NET) web API-based architectures from DevWeek about the totally newASP.NET Identity 2.0 in MVC 5.1 and Web API 2.0 and I decided to rework my current project to use it because

1. New system built on open standard OWIN, which allows to take a look 'What's inside?' and gives some flexibility in choosing web-server and OS.

2. Owin-based authentication natively supports security for Web API 2.0.

3. Latest projects contains OAuth 2.0 authentication out of the box.

I think many of ASP.NET MVC 1 - 4 web-developers rework standard Forms authentication provided by Microsoft. I did this too. I had overrided Membership Provider, Roles Provider and Identity, and you may find a lot of examples. But there are no so much information about ASP.NET Identity 2.0, so I think my post will cover it nicely.

As you know, when you create a new project from ASP.NET MVC template, it create default database for you which such a nice list of tables:

My goal is to show how to remove these tables and use my own tables, but still use its standard authentication system with support of external OAuth 2.0 providers and Web API 2.0 security system.

I use nice MVC patterns in my projects where Model, Logic and Presentation layers are different projects. Presentation layer is MVC project and all others are libraries. It allows my to have minimal changes in Model and Logic projects, but rework only Web project from ASP.NET MVC 3.0 to ASP.NET MVC 5.1. As data model I use Reverse Engineered POCO classes, so I can show you my changes in Database Schema and also in Code.

In my database schema I use my own tables User, Roles, UserRoles (x-table), and its relations.

Pic. DB schema.

1.

To start you need to add some tables or columns in your schema. First of all, take a look in the standard tables, which ASP.NET generated by default. You may create a new ASP.NET MVC 5 project, find generated DB file in App_Data folder (Hidden by default, so use Show All Files button in Solution Explorer) and connect to this file to see the tables. In other way you may go to a link with ASP.NET SQL scripts a copy-paste column right in your script or DB model. Compare results for me shows that I need to add some columns in myUsers tables, change type of Id key from INTEGER to NVARCHAR (same for Roles table), and also I needed to add tables UserClaimsand UserLogins.

Pic. New table Users at the left, old one at the right.

Pic. New schema ready to work.

2.

On my next step I generated SQL Script and created new database. After that I generated model classes via Visual Studio plug-in.

Pic. Model classes.

3.

Unload your old ASP.NET MVC 1-4 project from the solution and create a new one, MVC 5.1. Add all necessary dependent projects like Model and Repository in the References on a new web project. And now we gonna write code a little bit.

4.

Some steps described in Overview of Custom Storage Providers for ASP.NET Identity, but it doesn't show anything about IoC engines and uses totally external Storage like MySQL. So I will duplicate information in some parts, but not everywhere.Go to References -> NuGet and do Uninstall for Microsoft ASP.NET Identity EntityFramework. This removes all dependencies like IdentityUser, UserStore, RoleStore and we need to create our own. Now if you compile your project, you will see some problem files:

1. App_Start/Startup.Auth.cs

2. App_Start/IdentityConfig.cs

3. Models/IdentityModels.cs

4. Controllers/AccountController.cs

5.

Let's implement IUserStore, IRoleStore, IClaimStore, IUserRoleStore, IUserLoginStore in our classes, because it is necessary forASP.NET Identity 2.0 system. We may create 2 classes: UserStore, which implements interfaces like IUserStore, IClaimStore, IUserRoleStore, IUserLoginStore) and RoleStore, which implements IRoleStore. My example for RoleStore, which uses IoC Framework (example for MySQL is here):

class RoleStore : IRoleStore<Role>
{
    #region Private fields

    /// <summary>
    /// The _users repository.
    /// </summary>
    private readonly IRepository<PhiUser> _usersRepository;

    /// <summary>
    /// The _roles repository.
    /// </summary>
    private readonly IRepository<Role> _rolesRepository;

    /// <summary>
    /// The _user roles repository.
    /// </summary>
    private readonly IRepository<UserRole> _userRolesRepository;

    #endregion

    /// <summary>
    /// Initializes a new instance of the <see cref="RoleStore"/> class.
    /// </summary>
    /// <param name="usersRepository">The users repository.</param>
    /// <param name="rolesRepository">The roles repository.</param>
    /// <param name="userRolesRepository">The user roles repository.</param>
    public RoleStore(
        IRepository<PhiUser> usersRepository,
        IRepository<Role> rolesRepository,
        IRepository<UserRole> userRolesRepository)
    {
        this._usersRepository = usersRepository;
        this._rolesRepository = rolesRepository;
        this._userRolesRepository = userRolesRepository;
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
    }

    /// <summary>
    /// Inserts role in asynchronous mode.
    /// </summary>
    /// <param name="role">The role.</param>
    /// <returns>Task result.</returns>
    /// <exception cref="System.ArgumentNullException">Role is null.</exception>
    public Task CreateAsync(Role role)
    {
        if (role == null)
        {
            throw new ArgumentNullException("role");
        }

        _rolesRepository.Insert(role);

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

    /// <summary>
    /// Deletes role in asynchronous mode.
    /// </summary>
    /// <param name="role">The role.</param>
    /// <returns>Task result.</returns>
    /// <exception cref="System.ArgumentNullException">Role is null.</exception>
    public Task DeleteAsync(Role role)
    {
        if (role == null)
        {
            throw new ArgumentNullException("user");
        }

        _rolesRepository.Delete(role);

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

    /// <summary>
    /// Searches role by Id in asynchronous mode.
    /// </summary>
    /// <param name="role">The role Id.</param>
    /// <returns>Task result.</returns>
    /// <exception cref="System.ArgumentNullException">Role Id is null.</exception>
    public Task<Role> FindByIdAsync(string roleId)
    {
        if (String.IsNullOrEmpty(roleId))
        {
            throw new ArgumentNullException("roleId");
        }

        Role result = _rolesRepository.GetById(roleId);

        return Task.FromResult<Role>(result);
    }


    /// <summary>
    /// Searches role by name in asynchronous mode.
    /// </summary>
    /// <param name="role">The role name.</param>
    /// <returns>Task result.</returns>
    /// <exception cref="System.ArgumentNullException">Rolename is null.</exception>
    public Task<Role> FindByNameAsync(String roleName)
    {
        if (String.IsNullOrEmpty(roleName))
        {
            throw new ArgumentNullException("roleName");
        }

        Role result = this._rolesRepository.Table.ToList().FirstOrDefault(x => x.Name == roleName);

        return Task.FromResult<Role>(result);
    }

    /// <summary>
    /// Updates role in asynchronous mode.
    /// </summary>
    /// <param name="role">The role.</param>
    /// <returns>Task result.</returns>
    /// <exception cref="System.ArgumentNullException">Role is null.</exception>
    public Task UpdateAsync(Role role)
    {
        if (role == null)
        {
            throw new ArgumentNullException("user");
        }

        _rolesRepository.Update(role);

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

Cutted example of UserStore, just to show you implemented interfaces, because there are no Rocket Science, just do it like in RoleStore (example for MySQL is here):

public class UserStore : IUserStore<PhiUser>,
                         IUserClaimStore<PhiUser>,
                         IUserLoginStore<PhiUser>,
                         IUserRoleStore<PhiUser>,
                         IUserPasswordStore<PhiUser>
{
    #region Private fields

    /// <summary>
    /// The _users repository.
    /// </summary>
    private readonly IRepository<PhiUser> _usersRepository;

    ...

    #endregion

    #region Constructor

    /// <summary>
    /// Initializes a new instance of the <see cref="UserStore"/> class.
    /// </summary>
    /// <param name="usersRepository">The users repository.</param>
    /// ...
    public UserStore(IRepository<PhiUser> usersRepository, ... )
    {
        this._usersRepository = usersRepository;
        ...
    }

    #endregion

    #region IUserStore
    
    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
    }

    /// <summary>
    /// Insert a new PhiUser in the UserTable.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User is null.</exception>
    public Task CreateAsync(PhiUser user)
    {
        ...
    }

    /// <summary>
    /// Deletes a user.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <returns>Task with result.</returns>
    public Task DeleteAsync(PhiUser user)
    {
        ...
    }

    /// <summary>
    /// Returns an PhiUser instance based on a userId query.
    /// </summary>
    /// <param name="userId">The user's Id.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User id is null.</exception>
    public Task<PhiUser> FindByIdAsync(String userId)
    {
        ...
    }

    /// <summary>
    /// Returns an PhiUser instance based on a userName query.
    /// </summary>
    /// <param name="userName">The user's nam.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">Username is null.</exception>
    public Task<PhiUser> FindByNameAsync(String userName)
    {
        ...
    }

    /// <summary>
    /// Updates the UsersTable with the PhiUser instance values.
    /// </summary>
    /// <param name="user">PhiUser to be updated.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User is null.</exception>
    public Task UpdateAsync(PhiUser user)
    {
        ...
    }

    #endregion

    #region IUserClaimStore

    /// <summary>
    /// Inserts a claim to the UserClaimsTable for the given user.
    /// </summary>
    /// <param name="user">User to have claim adde.</param>
    /// <param name="claim">Claim to be adde.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User or claim is null.</exception>
    public Task AddClaimAsync(PhiUser user, Claim claim)
    {
        ...
    }

    /// <summary>
    /// Returns all claims for a given user.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User is null.</exception>
    public Task<IList<Claim>> GetClaimsAsync(PhiUser user)
    {
        ...
    }

    /// <summary>
    /// Removes a claim froma user.
    /// </summary>
    /// <param name="user">User to have claim removed.</param>
    /// <param name="claim">Claim to be removed.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User or claim is null.</exception>
    public Task RemoveClaimAsync(PhiUser user, Claim claim)
    {
        ...
    }

    #endregion

    #region IUserLoginStore

    /// <summary>
    /// Inserts a Login in the UserLoginsTable for a given User.
    /// </summary>
    /// <param name="user">User to have login added.</param>
    /// <param name="login">Login to be added.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User or login is null.</exception>
    public Task AddLoginAsync(PhiUser user, UserLoginInfo login)
    {
        ...
    }

    /// <summary>
    /// Returns an PhiUser based on the Login info.
    /// </summary>
    /// <param name="login">User login.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">Login is null.</exception>
    public Task<PhiUser> FindAsync(UserLoginInfo login)
    {
        ...
    }

    /// <summary>
    /// Returns list of UserLoginInfo for a given PhiUser.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User is null.</exception>
    public Task<IList<UserLoginInfo>> GetLoginsAsync(PhiUser user)
    {
        ...
    }

    /// <summary>
    /// Deletes a login from UserLoginsTable for a given PhiUser.
    /// </summary>
    /// <param name="user">User to have login remove.</param>
    /// <param name="login">Login to be remove.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User or login is null.</exception>
    public Task RemoveLoginAsync(PhiUser user, UserLoginInfo login)
    {
        ...
    }

    #endregion

    #region IUserRoleStore

    /// <summary>
    /// Inserts a entry in the UserRoles table.
    /// </summary>
    /// <param name="user">User to have role adde.</param>
    /// <param name="roleName">Name of the role to be added to use.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User or rolename is null.</exception>
    public Task AddToRoleAsync(PhiUser user, String roleName)
    {
        ...
    }

    /// <summary>
    /// Returns the roles for a given PhiUser.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User is null.</exception>
    public Task<IList<String>> GetRolesAsync(PhiUser user)
    {
        ...
    }

    /// <summary>
    /// Verifies if a user is in a role.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <param name="role">Current role.</param>
    /// <returns>Task with result.</returns>
    /// <exception cref="ArgumentNullException">User or role is null.</exception>
    public Task<Boolean> IsInRoleAsync(PhiUser user, String role)
    {
        ...
    }

    /// <summary>
    /// Removes a user from a role.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <param name="role">Current role.</param>
    /// <returns>Throw exception.</returns>
    /// <exception cref="NotImplementedException">Always thrown.</exception>
    public Task RemoveFromRoleAsync(PhiUser user, String role)
    {
        throw new NotImplementedException();
    }

    #endregion

    #region IUserPasswordStore

    /// <summary>
    /// Returns the PasswordHash for a given PhiUser.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <returns>Task with result.</returns>
    public Task<String> GetPasswordHashAsync(PhiUser user)
    {
        ...
    }

    /// <summary>
    /// Verifies if user has password.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <returns>Task with result.</returns>
    public Task<Boolean> HasPasswordAsync(PhiUser user)
    {
        ...
    }
    
    /// <summary>
    /// Sets the password hash for a given PhiUser.
    /// </summary>
    /// <param name="user">Current user.</param>
    /// <param name="passwordHash">Password hash.</param>
    /// <returns>Task with result.</returns>
    public Task SetPasswordHashAsync(PhiUser user, String passwordHash)
    {
        ...
    }

    #endregion
}

6.

Next, if you use IoC Engine, then register all created classes in it, so it could instantiate it.I use Simple Injector, so in my case it looks like this:

...
   _instance.Register<IUserStore<PhiUser>, UserStore>();
   _instance.Register<IUserClaimStore<PhiUser>, UserStore>();
   _instance.Register<IUserLoginStore<PhiUser>, UserStore>();
   _instance.Register<IUserRoleStore<PhiUser>, UserStore>();
   _instance.Register<IUserPasswordStore<PhiUser>, UserStore>();
   _instance.Register<IRoleStore<Role>, RoleStore>();
...

7.

Next, we may remove or comment all code inside 3. Models/IdentityModels.cs.

ApplicationUser should be renamed to User everywhere. ApplicationUserManager will be renamed in 2. App_Start/IdentityConfig.cs to MyUserManager, or something like this. Rename parameter in the constructor call for UserManager. For Simple Injector it looks like this:

...
   var manager = new PhiUserManager(ModelContainer.Instance.GetInstance<UserStore>());
...

8.

Now, fix the 4. Controllers/AccountController.cs by the same way:

... 
  
public AccountController(IUserStore<PhiUser> userStore)
  {
   UserManager = new PhiUserManager(userStore);
  }
...

9.

Also, we need to reimplement method GenerateUserIdentityAsync. I did this by Static Helper class:

...
    public class MyUserHelpers
    {
        public static async Task<ClaimsIdentity> GenerateUserIdentityAsync(PhiUser user, UserManager<PhiUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }
    }
...

10.

And last one. In 1. App_Start/Startup.Auth.cs we need to specify methods for creation of Db context and User manager. So I did this:

...
    public static phiContext CreateContext()
    {
        return ModelContainer.Instance.GetInstance<phiContext>();
    }

    // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
    public void ConfigureAuth(IAppBuilder app)
    {
        // Configure the db context and user manager to use a single instance per request
        app.CreatePerOwinContext(CreateContext);
        app.CreatePerOwinContext<PhiUserManager>(PhiUserManager.Create);
...

11.

Now, it should be done. Compile and run. You should be able to register a new user and login with its credentials, but now you may control this information, since all classes implemented by you.

What to read:

1. What is Katana and Owin

2. Video introduction to Katana

3. What's new in ASP.NET MVC 5

4. How to use MySQL storage for ASP.NET Identity

5. Ready SQL scripts for ASP.NET project

Комментарии

Популярные сообщения из этого блога

Делаем себе бесплатный VPN на Amazon EC2

Читать этот пост в Telegraph. Другие посты в канале в Telegram. Кто только не расписывал уже пошаговые инструкции по этой теме. Однако, время идёт, ПО меняется, инструкции нуждаются в обновлении, а люди в современной России всё больше нуждаются в применении VPN. Я собираюсь описать все шаги для создания бесплатного сервера на Amazon EC2 с операционной системой Linux и необходимые команды для настройки VPN сервера на нём. Чтобы не повторяться о деталях, которые были много раз описаны на русскоязычных и англоязычных ресурсах, по ходу статьи я просто приведу целую кипу ссылок, где можно почерпнуть необходимую информацию, а где информация устарела - опишу подробнее что нужно сдеать. В итоге, сервер будет доступен для вас из любой точки планеты, с любой операционной системы, и бесплатно (с определёнными ограничениями по трафику). Шаг первый - Регистрируемся на Amazon AWS Нужно зайти на сайт https://aws.amazon.com/ru и сразу перейти к Регистрации, нажав одноимённую кнопку. При р

В помощь программисту: инструкции по работе с Ubuntu сервером

Программистам чаще приходится писать код и заботиться о его чистоте, правильных абстракциях в коде, корректных зависимостях и прочих сложностях профессии. При этом, настройка и обслуживание серверов, хоть и связанная область - это отдельный навык, необходимый не каждому, и помнить о котором в деталях сложно. Поэтому, я делаю ряд микро-инструкций, которыми буду пользоваться и сам, когда необходимо. Это не статьи, а пошаговые помощники, которые я буду дополнять и наполнять по мере надобности. Делаем бесплатный VPN на Amazon EC2 Создание ключей SSH Подключение к серверу через SSH Передача файла с Linux сервера наWindows машину Делаем VPN сервер на Ubuntu 20.04 используя OpenVPN и EasyRSA  Отображение GUI с Linux сервера на Windows машине

Выбираем все плюсы из трех парадигм Entity Framework

Между парадигмами разработки с Entity Framework (Code First, Model First, Database First) я выбрал промежуточную, потому что ни одна меня не устраивала полностью. В Code First меня радуют чистые POCO классы, но не устраивает невозможность моделирования базы. В Database First и Model First мне не нравится генерация EDMX и другого всего лишнего. Таким образом, я нашел для себя такое решение: 1. Я моделирую схему в любой удобной программе (тут любая внешняя программа моделирования, генерирующая SQL Server-совместимые скрипты генерации базы) Рис. Смоделированная схема БД. 2. Создаю базу в SQL Management Studio 3. Делаю Reverse Engineering базы в POCO классы (как в Code First) с помощью плагина Entity Framework Power Tools Рис. Установленный плагин для Reverse Engineer. Рис. Вот так делается Reverse Engineer базы данных в POCO классы. Рис. Результат генерации POCO классов на основе базы данных: папочка Models с готовым контекстом, классами объектов и маппинг-классами.