2015年10月21日 星期三

Token Authentication with ASP.NET OWIN Identity (4) – Role Management and authorization

 ASP.NET   IDENTITY   Token based authentication


背景

延續先前的實作, 我們將在OWIN Identity Authentication 加入Role(腳色)的設定, 達成在需要認證的類別或方法中,可進一步用Role來設定權限。



相關文章




環境

l   Visual Studio 2015
l   WEB API 2.2
l   Entity Framework 6


實作


新增Role DTO類別

OWIN Identity Code first的資料庫中,已實作並建立好Role相關資料表:
l   AspNetRoles
l   AspNetUserRoles



有興趣可稍微看一下它的Entity Framework POCO :



 


稍後會提到Microsoft.AspNet.Identity.RoleManager 這個類別,它已提供了大部分的Role操作(CRUD)方法。
所以我們只要實作提供介接的介面(Web Api)即可。 至於Web ApiRole DTO設計如下:

/// <summary>
/// Role DTO
/// </summary>
public class RoleModel
{
        /// <summary>
        /// Role Name
        /// </summary>
        public String Name { get; set; }
}




Web Api

Web Api的部分沒什麼好說的,就是提供CRUDAPI,因為暫時沒有更新(Update)的需求,所以實作如下;
PS. 我們稍後會將實際操作資料庫的方法包裝成一個Role Repository

l   RoleController

public class RoleController : BaseController
{
        public RoleController()
        {
        }
        [HttpGet]
        public async Task<IQueryable<RoleModel>> GetAllRoles()
        {
            var identityRoles = await RoleRepositorySingleton.GetInstance.GetRoles(x => true);
           
            return identityRoles.Select(x => new RoleModel
            {
                Name = x.Name
            });
        }
       
        [HttpPost]
        public async Task<HttpResponseMessage> Create(RoleModel roleModel)
        {
            #region Model Validation
            var isModelValidationOk = base.ValidateDtoModel();

            if (!isModelValidationOk)
            {
                return new HttpResponseMessage(HttpStatusCode.BadRequest);

            }
            #endregion

            #region Create the role into database
            var result = await RoleRepositorySingleton.GetInstance.Create(new IdentityRole(roleModel.Name));
           

            if (!result.Succeeded)
            {
                if (result.Errors != null)
                {
                    LogUtility.LogErrorMsg(result.Errors);
                }
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
            else
            {
                LogUtility.Logger.Info(String.Format("Created a role : {0}", roleModel.Name));
                return new HttpResponseMessage(HttpStatusCode.Created);
            }
            #endregion
        }
        [HttpPost]
        public async Task<HttpResponseMessage> Remove(IEnumerable<RoleModel> roleModels)
        {
            foreach (var roleModel in roleModels)
            {
                var result = await RoleRepositorySingleton.GetInstance.Delete(roleModel.Name);

                if (!result.Succeeded)
                {
                    if (result.Errors != null)
                    {
                        LogUtility.LogErrorMsg(result.Errors);
                    }
                    return new HttpResponseMessage(HttpStatusCode.InternalServerError);
                }
                else
                {
                    LogUtility.Logger.Info(String.Format("The role is removed : {0}", roleModel.Name));
                }
            }

            return new HttpResponseMessage(HttpStatusCode.OK);

        }
}



l   BaseController

public class BaseController : ApiController
{
        protected bool ValidateDtoModel()
        {
            bool isSuccess = false;

            try
            {
                if (!ModelState.IsValid) //userModel的驗證失敗
                {
                    if (ModelState.Values != null && ModelState.Values.Count > 0)
                    {
                        foreach (var errorMsg in ModelState.Values)
                        {
                            Logger.Error(
                                String.Format("Model Validation Error : {0}", errorMsg.Errors.FirstOrDefault().ErrorMessage));
                        }
                    }

                    isSuccess = false;
                }
                else
                {
                    isSuccess = true;
                }
                return isSuccess;

            }
            catch (Exception)
            {
                throw;
            }

        }
}



Role Repository

接下來開始時做Role Repository,將前面提到的Microsoft.AspNet.Identity.RoleManager 封裝在此類別,另外考慮到效能,在此將其建立為Singleton類別。

l   Role Repository

public class RoleRepositorySingleton : BaseRepository
{
        public static RoleRepositorySingleton GetInstance
        {
            get
            { return InnerClass.instance; }
        }

        private class InnerClass
        {
            static InnerClass() { }
            internal static readonly RoleRepositorySingleton instance =
                new RoleRepositorySingleton();
        }

        private RoleRepositorySingleton():base() {}

        /// <summary>
        /// Create a role
        /// </summary>
        /// <param name="role">IdentityRole object</param>
        /// <returns>IdentityResult</returns>
        public async Task<IdentityResult> Create(IdentityRole role)
        {
            return await base._roleManager.CreateAsync(role);
        }

        /// <summary>
        /// Delete a role and the relations between users and it
        /// </summary>
        /// <param name="roleName">New Role Name</param>
        /// <returns>IdentityResult</returns>
        public async Task<IdentityResult> Delete(String roleName)
        {
            var role = await this.FindRole(roleName);
            return await base._roleManager.DeleteAsync(role);

        }

        /// <summary>
        /// Get all roles of an user
        /// </summary>
        /// <param name="filter">Filter</param>
        /// <returns>IdentityRole collections</returns>
        public async Task<IQueryable<IdentityRole>> GetRoles(Func<IdentityRole,bool> filter)
        {
            var roles = base._roleManager.Roles;

            if (filter!=null)
            {
                 roles = roles.Where(filter).AsQueryable();
            }

            return roles;
        }

        /// <summary>
        /// Find the
        /// </summary>
        /// <param name="roleName"></param>
        /// <returns></returns>
        public async Task<IdentityRole> FindRole(String roleName)
        {
            var role = await base._roleManager.FindByNameAsync(roleName);
            return role;
        }

        /// <summary>
        /// Find Roles of an user with user's name
        /// </summary>
        /// <param name="userName"></param>
        /// <returns></returns>
        public async Task<IQueryable<IdentityRole>> FindRoles(String userName)
        {
            try
            {
                var user = base._userManager.FindByName(userName);
                var roles = base._roleManager.Roles.Where(x => x.Users.Where(y => y.UserId.Equals(user.Id)).Count() > 0);
                return roles;
            }
            catch (Exception)
            {

                throw;
            }
           
        }

        /// <summary>
        /// Check if the role exist
        /// </summary>
        /// <param name="name">Role name</param>
        /// <returns>true(Exist)/false(Not exist)</returns>
        public async Task<bool> IsRoleExist(String name)
        {
            var isExist = await base._roleManager.RoleExistsAsync(name);
            return isExist;
        }
}


l   Base Repository

public class BaseRepository : IDisposable
{
        protected AuthContext _ctx = null;

        //private AuthUserManager _userManager = null;
        protected UserManager<EnhancedIdentityUser> _userManager = null;
        protected RoleManager<IdentityRole> _roleManager = null;
        public BaseRepository()
        {
            this._ctx = new AuthContext();
            //Intialize the Identity manager instances
            this._userManager = UserManagerFactory.Create(this._ctx);
            this._roleManager = RoleManagerFactory.Create(this._ctx);
        }
        public virtual void Dispose()
        {
            this._ctx.Dispose();
            this._userManager.Dispose();
            this._roleManager.Dispose();
        }
}


到這邊我們已經完成了所有的介面和操作方法,接下來會開始設定Identity Claims
Authentication Server可以提供授權給特定RoleIdentity (Token)


設定Identity Claims

開啟先前實作好的 BearerAuthorizationServerProvider 這個類別 (繼承自 OAuthAuthorizationServerProvider ) 除了原本加入的Claim Type : 使用者名稱

identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));

現在要加入Claim Type : Role
例如底下這行代碼將加入Admin這個腳色的授權ClaimIdentity (Token)
identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));


當然使用者在要求Token時,並不會夾帶它屬於那些Role,這時候上面剛才實做的 Role Repository就可以派上用場,直接用User Name去資料庫找到所有對應的Role

完整程式碼請參考如下:

l   BearerAuthorizationServerProvider

public class BearerAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override async Task ValidateClientAuthentication(
            OAuthValidateClientAuthenticationContext context)
        {
            context.Validated();
        }

        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {

            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

            IdentityUser user = await AuthRepositorySingleton.GetInstance.FindUser(context.UserName, context.Password);

            if (user == null)
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                return;
            }
            else if (!user.EmailConfirmed)
            {
                context.SetError("invalid_grant", "Denied because the email hadn't been confirmed.");
                return;
            }

            var identity = new ClaimsIdentity(context.Options.AuthenticationType);

            this.setClaimsForIdentity(identity, context);


            context.Validated(identity);

        }

        private void setClaimsForIdentity(ClaimsIdentity identity, OAuthGrantResourceOwnerCredentialsContext context)
        {
            try
            {
                #region Add Claim : User's Name
                identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
                #endregion

                #region Add Claim : User's Roles
                var identityRoles = RoleRepositorySingleton.GetInstance.FindRoles(context.UserName).Result;

                foreach (IdentityRole role in identityRoles)
                {
                    identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
                }
                #endregion
            }
            catch (Exception)
            {
                throw;
            }
        }
}



Securing WebApi with both Registered User and Role

最後的部分,當然是把我們的服務類別或方法設定需要Role的授權!
我們一開始實作的Web Api : RoleController 裡面的新增、刪除、查詢方法,一般來說不會讓一般使用者使用,可設定只具有系統管理者腳色(Admin)的使用者才能使用,這時候我們就可以很方便的在Web ApiHttp method加上 [Authorize(Roles = "Admin")] ,達到保護的效果。

PS. 如果需要設定多個腳色,則以逗號區隔,如 [Authorize(Roles = "Admin, SysOwner")]

範例:

[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<HttpResponseMessage> Create(RoleModel roleModel)
{ …. }




其他:更新註冊使用者的DTO類別

Hold on a minute! 雖然以上已經完成整個Role的管理及授權、驗證, 不過通常會在第一次註冊使用者的時候,就指定其腳色吧!! 這時候請調整註冊使用者的DTO類別~~!

l   UserModel 只列出新增的代碼

public class UserModel
{
        //Skip the original properties ...

        [Display(Name = "Roles")]
        public List<RoleModel> Roles { get; set; }

}


並且在Web Api的註冊方法裡面,加上新增該使用者腳色的代碼:

[AllowAnonymous]
[HttpPost]
public async Task<HttpResponseMessage> Register(UserModel userModel)
{
            try
            {
                #region Model Validation
                var isModelValidationOk = base.ValidateDtoModel();

                if (!isModelValidationOk)
                {
                    return new HttpResponseMessage(HttpStatusCode.BadRequest);

                }
                #endregion


                #region Register Identity User
                var identityUser = await this.registerUser(userModel);
                if (identityUser == null) //Fail
                {
                    return new HttpResponseMessage(HttpStatusCode.InternalServerError);
                }
                #endregion

                #region Set the user's roles
                if (userModel.Roles != null && userModel.Roles.Count > 0)
                {
                    await this.setRoleToUser(userModel.UserName, userModel.Password, userModel.Roles);
                }
                #endregion

                #region Send a confirmation email
                await this.sendConfirmEmail(identityUser);
                #endregion


                LogUtility.Logger.Info(String.Format("Registered an user : {0}", userModel.UserName));
                return new HttpResponseMessage(HttpStatusCode.Created);
            }
            catch (Exception ex)
            {
                LogUtility.LogErrorMsg(ex);
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);

            }
}

private async Task setRoleToUser(string userName, string password, List<RoleModel> roles)
{
IdentityResult result =
                    await AuthRepositorySingleton.GetInstance.SetRoleToUser(
userName, password, roles);

               
                if (!result.Succeeded)
                {
                    if (result.Errors != null)
                        LogUtility.LogErrorMsg(result.Errors);
                }
}

PS. 至於刪除使用者的時候,並不需要加上刪除使用者及對應腳色的關聯 (資料表名稱:AspNetUserRoles) OWIN Identity的封裝程式碼會幫我們完成,這部分可自行試看看!



測試



我們在Web Api GetAllRoles (查詢所有腳色)方法,加上指定的Role :

 


Register users with specific roles

註冊了三個使用者及其所對應的腳色:

Name
Role
Leia
Guest
Lily
Admin
Mother
JB
Admin
Father


參考JSON如下:



關聯表的結果和預期相同。




Test the Web Api

接下來分別以三個使用者 (順序請參考上表,分別為Leia Lily JB ),取得各自的Token,再對我們的GetAllRoles方法送出Request

以下是Http Response結果,果然只有具有FatherMother腳色權限的Identity,才能通過授權並正確取得資料。

 



Reference


沒有留言:

張貼留言