2015年8月24日 星期一

Token Authentication with ASP.NET OWIN Identity (1) – Base Service

 ASP.NET   IDENTITY   Token based authentication

背景

SOASPA的架構下,採用Token-based的認證機制是必然的趨勢。
本篇將以ASP.NET OWIN Identity 實作以Bearer Token* 為主的認證機制。

備註:
Bearer Token IETF 提出在Client side欲存取服務時,必須透過此服務相信的伺服器做Token認證後才能使用該服務。

實作上主要參考 TAISEER (MVP of Microsoft)此篇文章:

環境

l   Visual Studio 2015
l   WEB API 2.2
l   套件版本:
Microsoft.Owin.Host.SystemWeb 3.0.1
Microsoft ASP.NET Identity Owin 2.2.1
Microsoft ASP.NET Identity EntityFramework 2.2.1
Microsoft ASP.NET Web API 2.2 OWIN 5.2.3
Microsoft.Owin.Security.OAuth 3.0.1


實作 : 授權(認證)Authentication Server


Create a WebApi with OWIN startup

WebApi專案中加入以下套件,



加入ASP.NET Identity



順便一併加入Enable Cross-Origin Resource Sharing (CORS) ~




建立IdentityDbContext

public class AuthContext : IdentityDbContext<IdentityUser>
    {
        public AuthContext() : base("AuthContext")
        {
        }
    }

並在Config加入對應的Connection String

<connectionStrings>
    <add name="AuthContext" connectionString="Data Source=XXXX;Initial Catalog=JB;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>


IdentityDbContext 已具有Code-first建立對應資料庫的方法。
因此不用擔心後端資料庫的部分。


建立資料庫的Authentication Repository

l   UserModel

/// <summary>
/// User information for authorization
/// </summary>
public class UserModel
{
        [Required]
        [Display(Name="User Name")]
        public String UserName { get; set; }

        [Required]
        [StringLength(100, MinimumLength=6, ErrorMessage="The {0}'s length must be between {2}~{1} characters long")]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public String Password { get; set; }


        [DataType(DataType.Password)]
        [Display(Name="Confirm password")]
        [Compare("Password", ErrorMessage="The password and confirmation password do not match.")]
        public String ConfirmPassword { get; set; }
}


l   Authentication Repository

Microsoft.AspNet.Identity.UserManager<TUser> 已經實作了很完整的CRUD方法,我們可直接叫用。

public class AuthRepository : IDisposable
    {
        private AuthContext _ctx = null;
        private UserManager<IdentityUser> _userManager = null;

        public AuthRepository()
        {
            this._ctx = new AuthContext();
            this._userManager = new UserManager<IdentityUser>(
                new UserStore<IdentityUser>(this._ctx));

             //設定可允許UserName包含特殊字元及空白
            this._userManager.UserValidator =
                new UserValidator<IdentityUser>(this._userManager)
                {
                    AllowOnlyAlphanumericUserNames = false
                };
        }

        public async Task<IdentityResult> RegisterUser(UserModel userModel)
        {
            var user = new IdentityUser
            {
                UserName=userModel.UserName
            };

            var result = await this._userManager.CreateAsync(user, userModel.Password);
            return result;
        }

        public async Task<IdentityResult> Unregisteruser(String userName)
        {
            try
            {
                var user = await this._userManager.FindByNameAsync(userName);
                var identityResult = await this._userManager.DeleteAsync(user);
                return identityResult;
            }
            catch (Exception)
            {
                throw;
            }
        }

        public async Task<IdentityUser> FindUser(String userName, String password)
        {
            var user = await this._userManager.FindAsync(userName, password);
            return user;
        }

        public async Task<IQueryable<IdentityUser>> GetAllUsers()
        {
            return this._userManager.Users;
        }

        public void Dispose()
        {
            this._ctx.Dispose();
            this._userManager.Dispose();
        }
    }



建立AuthenticationRestful Services,包含取得所有使用者, 註冊及註銷使用者

很直覺的就是拿上一步的Repository來使用即可。

AccountController :

public class AccountController : ApiController
{
        private AuthRepository _authRepo = null;

        public AccountController()
        {
            this._authRepo = new AuthRepository();
        }

        [AllowAnonymous]
        [EnableCors(origins: "*", headers: "*", methods: "*")]
        [HttpGet]
        public async Task<IQueryable<UserModel>> GetAllUsers()
        {
            var identityUsers = await this._authRepo.GetAllUsers();

            var users = identityUsers.Select(x => new UserModel()
            {
                UserName = x.UserName,
                Password = String.Empty,
                ConfirmPassword = String.Empty
            });

            return users;
        }

        [AllowAnonymous]
        [EnableCors(origins: "*", headers: "*", methods: "*")]
        [HttpPost]
        public async Task<HttpResponseMessage> Unregister(IEnumerable<UserModel> users)
        {
            try
            {
                foreach(var user in users)
                {
                    await this._authRepo.Unregisteruser(user.UserName);
                }
                return new HttpResponseMessage(HttpStatusCode.OK);
            }
            catch (Exception ex)
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }


        [AllowAnonymous]
        [EnableCors(origins: "*", headers: "*", methods: "*")]
        [HttpPost]
        public async Task<HttpResponseMessage> Register(UserModel userModel)
        {
            if (!ModelState.IsValid) //userModel的驗證失敗
            {
                if (ModelState.Values != null && ModelState.Values.Count>0)
                {
                    foreach (var errorMsg in ModelState.Values)
                    {
                        Console.WriteLine(
                            String.Format("Error : {0}",errorMsg.Errors.FirstOrDefault().ErrorMessage));
                    }
                }

                return new HttpResponseMessage(HttpStatusCode.BadRequest);
            }

            IdentityResult result = await this._authRepo.RegisterUser(userModel);
            if(!result.Succeeded)
            {
                if(result.Errors!=null)
                {
                    foreach(var error in result.Errors)
                    {
                        Console.WriteLine(String.Format("Error : {0}",error));
                    }
                }
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }

            return new HttpResponseMessage(HttpStatusCode.OK);
        }
    }



Enable the Identity protocol

最後一步是建立 Authorization Server Provider及啟用整個認證機制。

l   Authorization Server Provider

繼承OAuthAuthorizationServerProvider 並覆寫

n   ValidateClientAuthentication

MSDN :
Web 應用程式接受 Basic 驗證認證,且呈現在要求標頭中,則可呼叫 context.TryGetBasicCredentials(out clientId, out clientSecret) 以取得這些值。若並未呼叫 context.Validated,不會進一步處理要求。

簡而言之,如果只做Identity的驗證,則可在此方法直接呼叫context.Validated()接受用戶端要求TokenRequest

如果有在Http Header加上Basic Authentication,則可呼叫
TryGetBasicCredentials(
out clientId, out clientSecret)
來取得資訊並做第一層驗證。 (可參考下面註解掉的程式碼)



n   GrantResourceOwnerCredentials

Token 端點的要求與 "password" "grant_type" 抵達時呼叫。 這會在使用者直接提供名稱和密碼認證到用戶端應用程式的使用者介面時發生

亦即我們在這裡放上要檢核ID/PWD的程式碼。



BearerAuthorizationServerProvider 程式碼

public class BearerAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override async Task ValidateClientAuthentication(
            OAuthValidateClientAuthenticationContext context)
        {
            context.Validated();
           
            #region TryGetBasicCredentials
            //可對Http Basic Authentication做第一層認證檢查 (Http Header)
            /*
            string clientId, clientSecret;
            if (context.TryGetBasicCredentials(out clientId, out clientSecret))
            {
                if (clientId.Equals("XXX") && clientSecret.Equals("XXX"))
                    context.Validated();
                else
                    context.Rejected();
            }
            else
            {
                context.Rejected();
            }
            */
            #endregion       
}

        public override async Task GrantResourceOwnerCredentials(
OAuthGrantResourceOwnerCredentialsContext context)
        {

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

            using (AuthRepository _repo = new AuthRepository())
            {
                IdentityUser user = await _repo.FindUser(context.UserName, context.Password);

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

            var identity = new ClaimsIdentity(context.Options.AuthenticationType);
            identity.AddClaim(new Claim("sub", context.UserName));
            identity.AddClaim(new Claim("role", "user"));

            context.Validated(identity);

        }
    }


最後,在OWIN Startup類別,啟用Identity
PS.
因已在Startup.cs註冊WebApiRoute設定,因此可取消(或註解)Global.asax的部分。

/// <summary>
/// OWIN Startup
/// </summary>
public class Startup
{
        /// <summary>
        /// Configuration
        /// </summary>
        /// <param name="appBuilder"></param>
        /// <remarks>注意方法名稱必須為Configuration</remarks>
        public void Configuration(IAppBuilder appBuilder)
        {
            HttpConfiguration config = new HttpConfiguration();

            //Configure OAuth
            this.configureOAuth(appBuilder);

            //Configure WebApi Route
            this.configureWebApiRoute(config);
            appBuilder.UseWebApi(config);

            //Enable CORS
            config.EnableCors();
        }

        private void configureWebApiRoute(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

        private void configureOAuth(IAppBuilder app)
        {
            OAuthAuthorizationServerOptions oAuthServerOpts = new OAuthAuthorizationServerOptions()
            {
                AllowInsecureHttp = true, //是否允許Http傳輸(上線後以Https較安全)

                //
指定取得Token路徑為 : http://localhost:port/token
                TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
               
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(2), //Token使用期限為取得後N
                Provider = new BearerAuthorizationServerProvider()
            };

            //Token Generation
            app.UseOAuthAuthorizationServer(oAuthServerOpts);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
        }
}


測試


1.  Register a user



2.  Get the token

(Request)


PS. Http Body :
grant_type=password&username=Leia Skywalker&password=12345678

(Response)


此時如果沒有意外,我們已經成功取得token
access_token後的值複製下來。

3.  Authorization

還記得在AccountController有一個取得所有使用者的HTTP GET方法,請把它改成需要授權才能使用。

//[AllowAnonymous]
[Authorize]
[EnableCors(origins: "*", headers: "*", methods: "*")]
[
HttpGet]
public async Task<IQueryable<UserModel>> GetAllUsers()
{ … }

不使用Token做認證的結果 ~ HTTP 401 Unauthorized



Header加上
Authorization : bearer [
上面取得的token] 成功過了認證。



當此token的有效期結束後,則必須重新再次取得!

(
至於註銷Unregister使用者,請再自行測試囉~ )





Reference





沒有留言:

張貼留言