Contents

.Net Core Identity 令牌驗證與帳號安全

本篇筆記紀錄使用 Net Core Identity 產生 Token 所做令牌做一些驗證信處理,帳號登入錯誤太多次,我們可以設定安全規則,在多次登入失敗可以封鎖幾分鐘帳號。我們可以產生令牌,令牌是有時效的,超過時間就會過期,這邊令牌非一般 JWT ,他是有 Data Protection API 做一層加密,這邊不會探討這部分。

阻止帳號未驗證信箱登入

AspNetUsers有一個 EmailConfirmed 可以確認是否有驗證信箱。使用前需要在 Startup.csConfigureServices()中的 RequireConfirmedEmail 屬性設定為true

1
2
3
4
            services.Configure<IdentityOptions>(options =>
            {
                options.SignIn.RequireConfirmedEmail = true;
            });
1
2
3
4
5
6
7
                // 沒有這一個判斷會走到「登入失敗,請重試」,注意這邊要做這個判斷,使用者才知道是什麼錯誤。
                if (user != null & !user.EmailConfirmed &&
                    await _userManager.CheckPasswordAsync(user, model.Password))
                {
                    ModelState.AddModelError(string.Empty, "你的信箱尚為進行驗證");
                    return View(model);
                }

GIT: 使用者登入做信箱驗證動作 · malagege/NetCoreAuthSample@92872cc

電子信箱確認令牌

電子令牌需要使用_userManager.GenerateEmailConfirmationTokenAsync(user)產生令牌(Token)。

這邊可以用Url.Action產生Email驗證頁面連結。透過 _userManager.ConfirmEmailAsync(user,token)進行驗證。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
        public async Task<IActionResult> RegisterAsync(RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser
                {
                    UserName = model.Email,
                    Email = model.Email,
                    City = model.City,
                };

                var result = await _userManager.CreateAsync(user, model.Password);  // 第二個參數密碼要放,沒放不會檢核密碼規則

                if (result.Succeeded)
                {
                    string confirmationLink = await GenerateConfirmactionLinkAsync(user);

                    //當前 admin 新增帳號,導回使用者清單
                    if(_signInManager.IsSignedIn(User) && User.IsInRole("Admin"))
                    {
                        return RedirectToAction("userlist", "admin");
                    }

                    ViewBag.ErrorTitle = "註冊成功";
                    ViewBag.ErrorMessage = "已經發送一組驗證信,請進行驗證。";
                    return View("Error");
                }

                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            return View(model);
        }

        private async Task<string> GenerateConfirmactionLinkAsync(ApplicationUser user)
        {
            string token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
            string confirmationLink = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, token = token }, Request.Scheme);
            return confirmationLink;
        }

這邊特別提一下,Url.Action("ConfirmEmail", "Account", new { userId = user.Id, token = token }, Request.Scheme);裡面的Request.Scheme是什麼,正常這個參數是填寫httphttps,但使用這個HttpRequest物件可以對應Server 設定。

產生 NotSupportedException: No IUserTwoFactorTokenProvider<TUser> named ‘Default’ is registered. 錯誤

https://i.imgur.com/CF5QGAb.png

需要在ConfigureServices()方法添加AddDefaultTokenProviders(),該方法可以用來信箱令牌驗證、密碼重製、雙因子驗證。

接下來就把 View 頁面用上去就完成

1
<h1>你的電子信箱已經驗證成功</h1>

你可能會忽略程式錯誤但使用者新增上去

理論上我猜Identity 相關 function 裡面有做 SaveChange(),所以我嘗試外面加 TransactionScope 就能解決這個問題。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
        public async Task<IActionResult> RegisterAsync(RegisterViewModel model)
        {
            using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser
                {
                    UserName = model.Email,
                    Email = model.Email,
                    City = model.City,
                };

                var result = await _userManager.CreateAsync(user, model.Password);  // 第二個參數密碼要放,沒放不會檢核密碼規則

                if (result.Succeeded)
                {
                    scope.Complete();
                    string confirmationLink = await GenerateConfirmactionLinkAsync(user);
                    _logger.LogInformation($"發送驗證信連結:{confirmationLink}");

                    //當前 admin 新增帳號,導回使用者清單
                    if(_signInManager.IsSignedIn(User) && User.IsInRole("Admin"))
                    {
                        return RedirectToAction("userlist", "admin");
                    }

                    ViewBag.ErrorTitle = "註冊成功";
                    ViewBag.ErrorMessage = "已經發送一組驗證信,請進行驗證。";
                    return View("Error");
                }

                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            return View(model);
        }

GIT: 申請帳號發送驗證信動作與檢查 · malagege/NetCoreAuthSample@3fbb828

第三方令牌做信箱驗證

跟上面邏輯一樣,由於這邊範例是信箱為key,通常第三方登入綁定原帳號不一定是用信箱,所以這邊就不實作了,不一定要做信箱驗證。

啟用帳號

過了超過驗證信箱時間,我們需要重新發送驗證信功能。更上面差不多事情就不細講,參照GITHUB設定。

GIT: 忘記密碼功能 · malagege/NetCoreAuthSample@f06b336

主要重點_userManager.GeneratePasswordResetTokenAsync產生 token。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
        [HttpPost]
        public async Task<IActionResult> ForgetPasswordAsync(EmailAddressViewModel model)
        {
            if (ModelState.IsValid)
            {
                // 用信箱查詢帳號
                ApplicationUser user = await _userManager.FindByNameAsync(model.Email);
                // 通過驗證才能用此功能
                if( user != null && await _userManager.IsEmailConfirmedAsync(user))
                {
                    string token = await _userManager.GeneratePasswordResetTokenAsync(user);
                    var passwordResetLink = Url.Action("ResetPassword", "Account", new { email = model.Email, token }, Request.Scheme);

                    _logger.LogInformation($"發送驗證信連結:{passwordResetLink}");

                    return View("ForgetPasswordConfirmation");
                }
                // 為了防暴力攻擊,不回應任何訊息
                return View("ForgetPasswordConfirmation");
            }
            return View(model);
        }

密碼重置功能

ViewModel 原本 DataAnntation 比較密碼第一個參數改成nameof(Password),可以讓程式可維護性。最近看到 EF 5 設定 Index也是這樣用。

1
        [Compare("Password",ErrorMessage = "密碼不一置,請重新確認")]

改成

1
        [Compare(nameof(Password),ErrorMessage = "密碼不一置,請重新確認")]

相關設定也請看GIT。

GIT: 重置密碼功能 · malagege/NetCoreAuthSample@7507387

修改令牌時效限制

令牌時效為1天。修改時間方法參照下面程式,這一段我就不放到GIT。

1
2
3
4
            services.Configure<DataProtectionTokenProviderOptions>(options =>
            {
                options.TokenLifespan = TimeSpan.FromSeconds(10);
            });

特定權杖時效限制

參照這篇設定變更電子郵件權杖生命週期,這邊簡單紀錄,但不記到 GIT。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class CustomEmailConfirmationTokenProvider<TUser>
                              :  DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
                                       : base(dataProtectionProvider, options, logger)
    {

    }
}
public class EmailConfirmationTokenProviderOptions : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
builder.Services.AddDefaultIdentity<IdentityUser>(config =>
{
    config.SignIn.RequireConfirmedEmail = true;
    config.Tokens.ProviderMap.Add("CustomEmailConfirmation",
        new TokenProviderDescriptor(
            typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
    config.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
}).AddEntityFrameworkStores<ApplicationDbContext>();

builder.Services.AddTransient<CustomEmailConfirmationTokenProvider<IdentityUser>>();

簡單跟 Claim 和 Handle 處理有點類似。

其他 userManager 小功能紀錄

  • 現有帳號更改密碼_userManager.ChangePasswordAsync(user, 舊密碼,新密碼)
  • 第三方沒設置密碼,新增密碼。_userManager.AddPasswordAsync(user, 新密碼),理論上可能用不到這個功能。後續登入中使用者要用_userManager.RefreshSignInAsync(user)更新使用者登入。

設定密碼太多次錯誤停用帳號

  1. services.Configure<IdentityOptions>加入設定
  2. _signInManager.PasswordSignInAsync第四個參數記得要修改成true

參考如下:

1
2
3
4
5
6
7
8
            services.Configure<IdentityOptions>(options =>
            {
                ...
                
                // 預設帳號密碼輸入五次會封鎖15分鐘(封鎖時間預設是5分鐘)
                options.Lockout.MaxFailedAccessAttempts = 5;
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
            });

_signInManager.PasswordSignInAsync第四個參數記得要修改成true

1
Microsoft.AspNetCore.Identity.SignInResult result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, true);

我們嘗試輸入多組錯誤密碼明顯看到資料庫有修改。

https://i.imgur.com/vUprCfF.png

登入中調整登入成功顯示帳號被鎖。重置密碼功能也要設定解除鎖定。

LoginAsync 方法

1
2
3
4
5
                // 使用者封鎖不讓使用者登入,但我建議可以不提示讓使用者知道,可發信通知使用者知道
                //if (result.IsLockedOut)
                //{
                //    return View("AccountLocked");
                //}

ResetPasswordAsync方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
                    if(result.Succeeded)
                    {
                        // 讓當前已封鎖使用者解除封鎖動作
                        if ( await _userManager.IsLockedOutAsync(user))
                        {
                            await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow);
                        }

                        return View("ResetPasswordConfirmation");
                    }

GIT: 設定密碼太多次錯誤停用帳號 · malagege/NetCoreAuthSample@e52ee78

心得

其實還有很多沒有記,但目前先整理到這邊。
像是

  1. Jwtbearer
  2. AddIdentityServer
  3. Two-Factor

很多寶藏還沒挖,有時間再整理研究。

彩蛋

How to confirm a phone number in ASP.Net Core 1.1MVC - Stack Overflow

ASP.NET Core 自动刷新JWT Token_My IO的技术博客_51CTO博客

ASP.NET Core Web Api之JWT刷新Token(三) - Jeffcky - 博客园