ASP.NET IDENTITY Token based authentication
▌背景
本篇重點在於加入Email認證, 只有通過Email認證的使用者才可以正常取得Identity的token。
▌相關文章
▌目標
l 在註冊(Register)時,寄發一封有認證URL (含UserId及Email token) 的Email到使用者信箱。
l 當User點選Email上的連結時,Server side會認證,並告知是否認證成功。
l 調整登入(login)的原則,只有當帳號/密碼正確,且做過認證Email通過的使用者才可以正常登入並取得token。
▌環境
l Visual Studio 2015
l WEB API 2.2
l Entity Framework 6
l SNMP Relay Server *
l 套件版本:
Microsoft.Owin.Host.SystemWeb 3.0.1
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
PS.在Email 發送的服務上,本篇是採用SNMP Relay來做轉送的動作,實際上可依您的開發環境做調整。
▌實作
▋新增EMAIL 發送服務類別
l IdentityEmailService
:
必須實作IIdentityMessageService 這個介面, 至於SendAsync方法則必須依照您使用的Email服務再自行實作。
必須實作IIdentityMessageService 這個介面, 至於SendAsync方法則必須依照您使用的Email服務再自行實作。
public class IdentityEmailService : IIdentityMessageService
{
/// <summary>
/// Send Email (Async)
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public async Task SendAsync(IdentityMessage msg)
{
/* Modify this fuction to meet your
development environment */
}
}
|
▋建立產生Microsoft.AspNet.Identity.UserManager<TUser>的工廠類別
我們在處理認證的Repository : AuthRepository ,大量使用了 UserManager 的方法;
因為加入Email認證需要UserManager需協助產生Email Token,因此需設定其屬性。
我們另外產生一支UserManagerFactory 來建立並設定它。
public static class UserManagerFactory
{
private static IDataProtectionProvider provider =
new
Microsoft.Owin.Security.DataProtection.DpapiDataProtectionProvider("ASP.NET
Identity");
public static UserManager<EnhancedIdentityUser> Create(AuthContext context)
{
var userManager =
new UserManager<EnhancedIdentityUser>(new UserStore<EnhancedIdentityUser>(context));
//指定UserManager發送Email的方法為我們自行定義的IdentityEmailService
userManager.EmailService
= new
JB.OWIN.Identity.Service.IdentityEmailService();
userManager.UserTokenProvider = new DataProtectorTokenProvider<EnhancedIdentityUser>(provider.Create("ASP.NET
Identity"))
{
TokenLifespan = TimeSpan.FromHours(24) //Pwd's
valid time
};
return userManager;
}
}
|
有了工廠,當然接下來是調整Repository囉!
▋AuthRepository : 更新UserManager的建立方式,並加入SendConfirmEmail (寄送認證Email)及ConfirmEmailAsync(驗證Email
Token)方法
這邊只列出新增及更新的部分 ~
l AuthRepository : Constructor
利用工廠模式建立UserManager物件。
利用工廠模式建立UserManager物件。
public AuthRepository()
{
this._ctx = new AuthContext();
this._userManager = UserManagerFactory.Create(this._ctx);
//設定可允許欄位UserName, Email包含特殊字元及空白 this._userManager.UserValidator =
new UserValidator<EnhancedIdentityUser>(this._userManager)
{
AllowOnlyAlphanumericUserNames = false
};
}
|
l AuthRepository : SendConfirmEmail
幾個重點:
幾個重點:
n 以IdentityUser.Id作為參數產生Email token。
n 記得token要做Url編碼 (UrlEncode),因為驗證時會透過URL傳送。
n callbackUrl 在User打開Email開啟的驗證連結,我們還沒實作這一支Web Api,所以Url可以等下再加上; 但可以肯定的,驗證Email token一定會夾帶User id和Email token!
所以在驗證Url後面加上這兩個Url參數吧~
ex. http://localhost/api/Email/Confirm?userId=XXX&token=XXXX
所以在驗證Url後面加上這兩個Url參數吧~
ex. http://localhost/api/Email/Confirm?userId=XXX&token=XXXX
public async Task SendConfirmEmail(IdentityUser user)
{
try
{
//Get the email token
string emailToken =
await this._userManager.GenerateEmailConfirmationTokenAsync(user.Id);
emailToken = System.Web.HttpUtility.UrlEncode(emailToken);
//Get the confirmation uri
var callbackUrl = new Uri(
String.Format("{0}?userId={1}&token={2}",
AppConfigHelper.GetInstance.ConfirmEmailRoute,
user.Id, emailToken));
await this._userManager.SendEmailAsync(user.Id, "Confirm your
account", "請驗證您的Email信箱: <a href=\"" + callbackUrl + "\">=>>>
Click Here</a>");
}
catch (Exception ex)
{
throw;
}
}
|
l AuthRepository : SendConfirmEmail
這一支方法是給驗證Email的Web Api呼叫的,先建起來放著。
這一支方法是給驗證Email的Web Api呼叫的,先建起來放著。
public Task<IdentityResult>
ConfirmEmailAsync(string
userId, string emailToken)
{
return this._userManager.ConfirmEmailAsync(userId, emailToken);
}
|
▋Web Api
最後,該把我們的Email認證服務加入到Authentication
的Web Api了。
首先在註冊使用者的方法上, 除了驗證註冊Model、註冊, 現在多了一個寄送認證Email的動作。
l AccountController :
Register
[HttpPost]
public async Task<HttpResponseMessage> Register(UserModel userModel)
{
#region Model Validation
var isModelValidationOk = this.validateUserModel();
if (!isModelValidationOk){
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
#endregion
#region Start Register Identity User
var identityUser = await this.registerUser(userModel);
if (identityUser == null) //Fail
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
#endregion
#region Send a confirmation email
await this.sendConfirmEmail(identityUser);
#endregion
return new HttpResponseMessage(HttpStatusCode.Created);
}
private async Task sendConfirmEmail(IdentityUser user)
{
await this._authRepo.SendConfirmEmail(user);
}
|
加入新的ApiController (或使用AccountController), 加上驗證Email token的Http Method.
l EmailController : Confirm
[AllowAnonymous]
[HttpGet]
public async Task<HttpResponseMessage> Confirm()
{
NameValueCollection nvc =
HttpUtility.ParseQueryString(Request.RequestUri.Query);
var userId = nvc["userId"] ?? String.Empty;
var token = nvc["token"] ?? String.Empty;
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(token))
{
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
IdentityResult result = null;
try
{
result = await this._authRepo.ConfirmEmailAsync(userId,
token);
}
catch (Exception)
{
//Continue the following check
}
var response = new HttpResponseMessage(HttpStatusCode.Moved) {
Content = new StringContent("") };
response.Content.Headers.ContentType
= new
System.Net.Http.Headers.MediaTypeHeaderValue("text/html");
if (result!=null && result.Succeeded)
{
response.Headers.Location = new
Uri(AppConfigHelper.GetInstance.ConfirmEmailOkPage);
}
else
{
response.Headers.Location = new
Uri(AppConfigHelper.GetInstance.ConfirmEmailNgPage);
}
logger.Info(response.Headers.Location.ToString());
return response;
}
|
上面這一支驗證Email token的重點:
n 以User Id和 Email token做驗證; 驗證時(呼叫AuthRepository.ConfirmEmailAsync),如果是不正確的資訊會導致Exception,所以catch & continue, 因為不論驗證成功與否,我們都要將Url導到對應的網頁來顯示訊息。
n 因為要驗證後做轉址,所以將response的Content-type設定為”text/html”, 並指定Header的location。
最後! 以上的程式碼有指定了一些Url (例如驗證Email toekn、驗證成功redirect、 驗證失敗redirect), 請將這些Url加入到Config檔~~~ (我這邊是用AppConfigHelper另外處理)
當然也別忘了分別建立驗證成功(失敗)的Redirect 網頁~~
For example …
For example …
▌測試
PS. 這時候,我們也可以複製Email上的認證連結,然後隨意修改email token;這時候使用此網址便會認證失敗,轉入到認證失敗的Html。
|
2.
測試Login
使用先前建立的登入介面,可以使用註冊且驗證Email的使用者登入了~~
疑!! 你有發現了嗎,即時我們不驗證Email,也是可以順利登入的唷!!
使用先前建立的登入介面,可以使用註冊且驗證Email的使用者登入了~~
疑!! 你有發現了嗎,即時我們不驗證Email,也是可以順利登入的唷!!
因為登入(取得Token)的過程中,我們並沒有加上判斷AspNetUsers.EmailConfirmed 的條件~~
如果不加上這個條件,也失去了Email驗證的意義了~~
所以請繼續往下面看下去唄!
如果不加上這個條件,也失去了Email驗證的意義了~~
所以請繼續往下面看下去唄!
▌彩蛋
▋Verify UserName/Password and Email-confirmed flag
before sending a token
更新BearerAuthorizationServerProvider使其在驗證User時,一併判斷是否已做過Email認證。
l BearerAuthorizationServerProvider : GrantResourceOwnerCredentials
public class BearerAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
//Other
codes …
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;
}
else if(!user.EmailConfirmed)
{
context.SetError("invalid_grant", "Denied
because the email hadn't been confirmed.");
return;
}
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub",
context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
}
|
加上黃色那一段程式碼之後,就只有UserName/Pwd正確且做過Email驗證的使用者,才可以登入 J
完成~~!
▌Reference
沒有留言:
張貼留言