前言

之前写过一篇 Identity – User Login, Forgot Password, Reset Password, Logout, 当时写的比较简陋, 今天有机会就写多一篇实战版.

建议先阅读之前那篇做一个 warm up.

本篇会讲到

1. user login

2. forgot and reset password

3. two factor

4. external login

5. logout

我这个实战场景是一个 CMS 的登入. 由 Admin 提前创建好 User (员工), 所以本篇不会涉及 user registrator 的环节.

而且 external login 也是提前由 Admin 配置好的.

这篇主要是讲和 Identity 相关的代码. 不会涵盖所有的代码, 它不是一个 step by step 的教程哦.

主要参考

Identity – Introduction & Scaffold (做一个 Scaffold 看代码)

Identity source code

Authentication source code

Docs – Identity (所有相关的)

Program.cs Setup

identity configuration

builder.Services
.AddIdentity<User, Role/* 包含简单的 RBAC*/>(options =>
{
options.Lockout.MaxFailedAccessAttempts = 15; // default 是 5 次, 太少了
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); // default 是 15 分钟, 太久了 // strong password required
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 10;
options.Password.RequiredUniqueChars = 1; options.User.AllowedUserNameCharacters = ""; // default 有一些符号是不允许的, 我 by pass all character
options.User.RequireUniqueEmail = true; // default 是 false 不 practical 吧
})
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>();

cookie configuration

builder.Services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/access-denied";
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.ReturnUrlParameter = "returnUrl";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.SlidingExpiration = true;
});

改了一些路径, 和 cookie 有效期规则, 没什么重点.

External Login 的 configuration 我下面会补上.

Create User

// create super admin role
var superAdminRole = new Role
{
Name = "Super Admin",
Description = "The super admin role has full permission to access any page, API or function. Please be cautious when assigning this role.",
Status = RoleStatus.Active,
IsSystemRole = true,
};
await roleManager.CreateAsync(superAdminRole); // create user
var derrick = new User
{
UserName = "Derrick",
Email = "hengkeat87@gmail.com",
ProfileName = "Derrick Yam",
};
await userManager.CreateAsync(derrick);
await userManager.AddPasswordAsync(derrick, "temporary strong password");
await userManager.AddToRoleAsync(derrick, "Super Admin");

每一个新 user 都 set 一个 strong password, 用户第一次登入时可以通过 forgot password, 登入后在自行更换密码.

External Login 和 Two-factor 下面我会补上.

Visit protect page and redirect to login

做一个 private 页面

访问它就会被 redirect 到 /login (我们在 cookie configuration 配置的路径), 同时会带上 query params ?returnUrl=/private

当用户登入后会被 redirect 回来 /private 页面. 这个是常见的用户体验.

Login

Login page 长这样

我们假设用户已经知道他的密码. 那他就输入 username, password, remember me 然后 submit.

题外话:

remember me default 是 tick. 这个我是参考了 Google 和 Microsoft 的体验.

Google 是完全没有问直接就是 remember me, Microsoft 会有一个 popup 询问, 它的引导是 Yes remember me.

所以大家都假设用户是在自己设备上使用居多.

Login.cs

主要代码是

var loginResult = await _signInManager.PasswordSignInAsync(
user, formData.Password,
isPersistent: formData.RememberMe,
lockoutOnFailure: true
);

PasswordSignInAsync 内部会替我们管理暴力破解. 当密码错误太多次后, user account 就会被 lock 一段时间.

loginResult 会显示各种可能出现的错误. 我们逐个处理就可以了.

when every ok, return Page() at the end. 在 /login 页面执行 SignIn 代码, ASP.NET Core 会替我们 redirect user to returnUrl. 我们不需要自己做 redirect.

Forgot Password

在 login button 下方有一个小小的 forgot password link

user 忘记密码时点击它会去到 /forgot-password 页面, 它长这样

通过 email 找回 username 和 password.

ForgotPassword.cs

主要代码是创建 token

var token = await userManager.GeneratePasswordResetTokenAsync(user);

这个 token 在 /reset-password page 就会用上了.

其余的代码都是在做 email 的东西, user 打开 email 按一个 link 会去到 /reset-password 输入新密码.

email 内容长这样

Reset Password

ResetPassword.cs

主要代码是

var resetPasswordResult = await userManager.ResetPasswordAsync(user, token, formData.NewPassword);
if (userManager.SupportsUserLockout && await userManager.IsLockedOutAsync(user))
{
await userManager.ResetAccessFailedCountAsync(user);
await userManager.SetLockoutEndDateAsync(user, null);
}

user reset password 我们也可以直接替他 sign in, 这个看你体验要怎样设计. 因为也要考虑到后续的 two-factor, 带去 login 走一遍正规流程也是一个不错的选择.

注: 如果要替 user 直接 sign in 的话, 需要手动 redirect to returnUrl. ASP.NET Core 只有在 /login 页面 sign in 才会替自动 redirect.

Require Two-factor

首先在 create user 时要 enable two-factor

// create user
var derrick = new User
{
UserName = "Derrick",
Email = "hengkeat87@gmail.com",
ProfileName = "Derrick Yam",
};
await userManager.CreateAsync(derrick);
await userManager.AddPasswordAsync(derrick, "temporary strong password");
await userManager.AddToRoleAsync(derrick, "Super Admin");
// 开启 two-factor
await userManager.SetTwoFactorEnabledAsync(derrick, true);
await userManager.ResetAuthenticatorKeyAsync(derrick);

Login.cs

1. 只有在 SetTwoFactorEnabledAsync 和 ResetAuthenticatorKeyAsync 两个都成立后, PasswordSignInAsync 才会检查 two factor.

如果只是 enable 但是没有 generate authenticator key 是不算开启 two-factor 的哦.

2. for 第一次 two-factor 我们需要让用户 scan qrcode setup authenticator. 这里我利用了 recovery code 做一个判断是否是第一次 (取巧)

3. recovery code 是当 user lost device 的时候可以使用的 two-factor login backup 方案.

Setup Two-Factor

第一次 login two-factor 需要 scan qrcode setup authenticator app.

bek3 caln luvw uplr zmwl 75eq dhuk sm4h 这段 code 就是上面 ResetAuthenticatorKeyAsync 搞出来的.

我只是把它按四个字母空格分隔, 然后 lowercase. 这是为了用户体验. qrcode 也是基于这个 key 做出来的.

SetupTwoFactor.cs

要先检查 user 是否已经通过 password login, 如果有 require two-factor, password login 会保存在 session cookie 而且时间很短而已.

authen key 是在 create user 时就已经 set 进去了的, 现在只是拿出来而已.

generate qrcode 代码

string GenerateQrCodeBase64(string companyName, string username, string authenticatorKey)
{
var queryBuilder = new QueryBuilder();
queryBuilder.Add("secret", authenticatorKey);
queryBuilder.Add("issuer", companyName + " CMS");
queryBuilder.Add("digits", "6");
var uri = $"otpauth://totp/{StgUtil.EncodeURIComponent(username)}" + queryBuilder.ToQueryString().Value; var writer = new BarcodeWriterPixelData
{
Format = BarcodeFormat.QR_CODE,
Options = new QrCodeEncodingOptions
{
Height = 200,
Width = 200,
NoPadding = true
}
};
var pixelData = writer.Write(uri);
var image = Image.LoadPixelData<Rgba32>(pixelData.Pixels, pixelData.Width, pixelData.Height);
return image.ToBase64String(JpegFormat.Instance);
}

用到了 ZXing 和 ImageSharp 插件. qrcode 内容是一个 URI, 开头是 otpauth://totp 然后是一些参数. secret authen key 不可以有空格哦.

user scan 了之后就会拿到 OTP, 输入后就 verify

登入后可以显示 recovery code 给 user 让它记入起来. (这里我偷懒没有做处理)

注: 虽然 database 里有保存 recovery code, 但是 Identity 没有任何 public 接口可以查看. 调用 GenerateNewTwoFactorRecoveryCodesAsync 后会返回 recovery codes 它就只有一次机会.

往后可以在调用, 它会 delete 掉之前的然后创建过新的.

Login with Two-factor

for 2nd times login 的 user 就不需要再 setup authen app 了.

LoginWithTwoFactor.cs

和 setup two-factor 差不多, 检查 OTP 就可以了.

1. TwoFactorAuthenicatorSignInAsync 的 isPersistent 指的是 user cookie 和 password sign in 的 isPersistent 等价 , 而 rememberClient 指的是 two-factor cookie.

如果 isPersistent: false, rememberClient: true 就表示 close browser 后 user 必须重新 password sign in, 但不需要再一次 two-factor OTP login.

2. 登入后记得要 manual redirect to returnUrl 哦. ASP.NET Core 只有在 /login page 会替我们自动 redirect

Login with Recovery Code

LoginWithRecoveryCode.cs

1. 处理 two-factor 前, 记得要先检查 user 是否已经通过 pasword login

2. recovery code 是可以一直 repeat use 的.

3. TwoFactoryRecoveryCodeSignInAsync 的 isPersistent 和 rememberClient 一定是 false. 全部走 session cookie. 这个是 Identity 封装的用户体验 (为了安全)

External Login

介绍

虽然 external login 经常和 Identity 一起出现, 但其实它们是可以完全无关系的.

你可以 external login without Identity. (external login package under /security/authentication 而 /identity 是独立另一个 package)

external login 通常有两个最要目的.

1. 拿来替代 password login

2. 拿来 call thrid party API service.

这两个目的是完全不同而且没有关联的, 你可以只用作第一个或只用作第二个, 这篇我主要是作为第一个用途.

Register Client App

参考:

Docs – Google external login setup in ASP.NET Core

Docs – Microsoft Account external login setup with ASP.NET Core

Docs – Facebook external login setup in ASP.NET Core

Program.cs

builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = "client id";
options.ClientSecret = "client secret";
options.AccessDeniedPath = "/external-login"; // if user suddenly cancel login will redirect to this
options.ReturnUrlParameter = "returnUrl";
options.SaveTokens = true; // true if we need direct use access token / refresh token
options.AccessType = "offline"; // if we need refresh token
// add whatever scope you need
// scope list: https://developers.google.com/identity/protocols/oauth2/scopes
options.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
options.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
options.Scope.Add("https://www.googleapis.com/auth/plus.business.manage");
// prompt = every time re-popup ask for permission
// include_granted_scopes = scope will 累加
options.AuthorizationEndpoint += "?prompt=consent&include_granted_scopes=true";
})
.AddMicrosoftAccount(options =>
{
options.ClientId = "client id";
options.ClientSecret = "client secret";
options.AccessDeniedPath = "/external-login";
options.ReturnUrlParameter = "returnUrl";
options.SaveTokens = true;
options.Scope.Add("offline_access"); // if we need refresh token
options.Scope.Add("https://graph.microsoft.com/User.ReadBasic.All");
options.Scope.Add("https://graph.microsoft.com/Calendars.ReadWrite");
options.Scope.Add("https://graph.microsoft.com/Mail.ReadWrite");
options.AuthorizationEndpoint += "?prompt=consent";
})
.AddFacebook(options =>
{
options.AppId = "client id";
options.AppSecret = "client secret";
options.AccessDeniedPath = "/external-login";
options.ReturnUrlParameter = "returnUrl";
options.SaveTokens = true;
options.AuthorizationEndpoint += "?prompt=consent";
});

看注释理解细节.

1. Google 和 Microsoft 的接口不完全一致.

2. client client 和 secret 最好存放进 User Secrets 里.

3. facebook 的 version 通常比较旧,比如目前最新版本是 v18,但是 ASP.NET Core 用的是最旧的 v14 版。

如果我们上面 3 个 API 不同版本的接口都是一直的话,我们可以 override 掉它。

通过修改 FacebookOptions 就可以了

4. facebook 没有 refresh token 的概念,它只有 Long-lived User Access Token

Create User

注:下面所有例子只会涉及 Google 和 Microsoft Login,Facebook Login 被我省略掉了,但它没有什么特别,大家都大同小异而已。

在创建 user 的时候添加 external login.

有些系统的体验是让用户 password login 后, 自己去添加. 而我则是让 admin 其添加, user 第一次登入就不需要用 password, two-factor 了.

var providerKey = "Temp_" + Guid.NewGuid().ToString();
await userManager.AddLoginAsync(newDerrick, new UserLoginInfo(
loginProvider: GoogleDefaults.AuthenticationScheme,
providerKey: providerKey,
displayName: GoogleDefaults.DisplayName)
); await userManager.AddLoginAsync(newDerrick, new UserLoginInfo(
loginProvider: MicrosoftAccountDefaults.AuthenticationScheme,
providerKey: providerKey,
displayName: MicrosoftAccountDefaults.DisplayName)
); var googleLoginInfo = newDerrick.Logins.Single(e => e.LoginProvider == GoogleDefaults.AuthenticationScheme);
googleLoginInfo.Email = "hengkeat87@gmail.com";
var microsoftLoginInfo = newDerrick.Logins.Single(e => e.LoginProvider == MicrosoftAccountDefaults.AuthenticationScheme);
microsoftLoginInfo.Email = "keatkeat87@live.com";
await userManager.UpdateAsync(newDerrick);

providerKey by right 应该是要放 user external account 的 ID 的. 但是这个阶段 user 还没有 external login 所以还不知道 ID. 那就先放一个 Temp_Guid 吧.

user external login 后, 会通过 email address 做 matching 找到对应的 account. 这个是我的规则.

Login.cshtml

ExternalLogin.cs

主要就是做 callback link 然后 Challenge

这时 user 就会被 redirect to third party login page (比如 Google / Microsoft login page)

after 登入后 user 会先 redirect 到 /signin-google, 这个是封装好的 request handle 处理 code, state 那些. 全部完成后才会 redirect to 我们的 callback link.

callback link 是 GET /external-login?handler=callback

而当前我们处在 POST /external-login.

如果 user cancel login 会被带到 GET /external-login

全部同一个 page, 但是不同 handler.

OnGetCallbackAsync

1. info.LoginProvider 相等于 info.Principal.Claims.Single(e => e.Type == ClaimTypes.NameIdentifier).Value.

2. 如果想保存 refresh token 可以考虑存放在 UserToken entity

await userManager.SetAuthenticationTokenAsync(user, info.LoginProvider, "RefreshToken", refreshToken);

这个 entity 是 identity 用来存放 two-factor 资料的

提醒:

SetAuthenticationTokenAsync 不可以用来更新 token value,它只能用来创建,如果已有,那将被无视。

所以若想 update token value,我们需要先删除,再创建一个新的。

await _userManager.RemoveAuthenticationTokenAsync(user, info.LoginProvider, "RefreshToken");
await _userManager.SetAuthenticationTokenAsync(user, info.LoginProvider, "RefreshToken", refreshToken);

Logout

/private.cshtml

在任何你想 logout 的 page 做一个 form post to /logout

Logout.cs

public async Task OnPostAsync(
[FromServices] SignInManager<User> signInManager
)
{
await signInManager.SignOutAsync();
// 如果也想把 two factor remember device 也 drop 掉
await signInManager.ForgetTwoFactorClientAsync();
}

1. SignOutAsync 会 sign out 三样东西

如果我们不希望全部 sign out 就自己调用 Context.SignOutAsync 来控制.

2. ForgetTwoFactorClientAsync 是清除 two-factor rememberClient 的 cookie, SignOutAsync 没有负责这个.

3. Logout page 也会自动 redirect to returnUrl 哦.

总结

这篇主的场景是 admin create user account then user login.

没有 cover 到的地方是 user 自己的 account management, 比如 change password 等等.

也没有 cover user 自行 register account 的场景.

以后有机会在补上呗.

Identity – user login, forgot & reset password, 2fa, external login, logout 实战篇的更多相关文章

  1. Reset Password Functionality FAQ

    In this Document   Purpose   Questions and Answers   How can users request a password reset?   How d ...

  2. (phpmyadmin error)Login without a password is forbidden by configuration (see AllowNoPassword) in ubuntu

    1.Go to /etc/phpmyadmin/config.inc.php and open it your favorite editor. 2.Search for below line of ...

  3. Mysql re-set password, mysql set encode utf8 mysql重置密码,mysql设置存储编码格式

    There is a link about how to re-set password. http://database.51cto.com/art/201010/229528.htm words ...

  4. GET: https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login? loginicon=true &uuid=odcptUu2JA==&tip=0

    GET: https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login? loginicon=true &uuid=odcptUu2JA==&am ...

  5. db2 identity列重置,reset/restart

    db2中可以对表中的某一个列创建identity列,用于自动填充值,某些情况下(比如删除数据后,需要从最小值开始,并不重复,那可以对标识列进行reset操作) 语法: ALTER TABLE < ...

  6. How to reset password for unknow root

    1. Click "e" when entering the grub 2. Add option " init=/bin/sh" to linux line. ...

  7. reset password for local admin on Windows2016 by Powershell

    上脚本吧,找半天 $password = "yourpassword" $pwd = $password | ConvertTo-SecureString -asPlainText ...

  8. VUE 出现Access to XMLHttpRequest at 'http://192.168.88.228/login/Login?phone=19939306484&password=111' from origin 'http://localhost:8080' has been blocked by CORS policy: The value of the 'Access-Contr

    报错如上图!!!!    解决办法首先打开 config -> index.js ,粘贴 如下图代码,'https://www.baidu.com'换成要访问的的api域名,注意只要域名就够了, ...

  9. SQLite reset password

    https://www.codeproject.com/tips/993395/sqliter-change-set-remove-passwords-on-sqlite-d https://sour ...

  10. mysql reset password重置密码

    安全模式启动 chown -R mysql.mysql /var/run/mysqld/ mysqld_safe --skip-grant-tables & 无密码root帐号登陆 mysql ...

随机推荐

  1. Java-Cookie客户端会话技术

    会话技术 会话:一次对话中包含多次请求和响应 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止 功能:在一次会话的范围内的多次请求间,共享数据 方式: 客户端会话技术:Cook ...

  2. 全网最适合入门的面向对象编程教程:09 类和对象的Python实现-类之间的关系,你知道多少?

    全网最适合入门的面向对象编程教程:09 类和对象的 Python 实现-类之间的关系,你知道多少? 摘要: 本文主要对类之间的关系进行了基本介绍,包括继承.组合.依赖关系,并辅以现实中的例子加以讲解, ...

  3. Mysql密码安全策略修改

    Mysql5.7默认有密码安全策略,密码安全级别要求比较高,在测试环境中使用起来不方便,本经验将介绍如何修改Mysql的密码安全策略,解决ERROR 1819错误. 1:首先使用root用户连接mys ...

  4. Vue 使用Use、prototype自定义全局插件

    Vue 使用Use.prototype自定义全局插件   by:授客 QQ:1033553122   开发环境   Win 10   node-v10.15.3-x64.msi 下载地址: https ...

  5. CF301B Yaroslav and Time 题解

    CF301B 这不最短路的板子题吗? 思路 用 \(ak\) 代表走到第 \(k\) 点时的可恢复单位时间的值. \(i\) 到 \(j\) 的距离是 \(\left ( \left | xi-xj ...

  6. 关于UE5打包DLC

    首先打开Project Lanucher,参考下图:,其次编辑配置两个edit Profile,参考下图: 第一个用来打包项目,第二个生成DLC,dlc填写的名字和插件一样,Main的配置如下: DL ...

  7. C# 12 新增功能实操!

    前言 今天咱们一起来探索并实践 C# 12 引入的全新功能! C#/.NET该如何自学入门? 注意:使用这些功能需要使用最新的 Visual Studio 2022 版本或安装 .NET 8 SDK ...

  8. pytest-req插件:更简单的做接口测试

    pytest-req插件:更简单的做接口测试 背景 我们经常会用到 pytest 和 requests 进行接口自动化测试. pytest 提供了非常方便的插件开发能力,在pytest中使用reque ...

  9. linux学习(7):Linux最常用150个命令汇总

    Linux最常用150个命令汇总 线上查询及帮助命令(2个) man 查看命令帮助,命令的词典,更复杂的还有info,但不常用. help 查看Linux内置命令的帮助,比如cd命令. 文件和目录操作 ...

  10. 【H5】09 音频和视频

    现在我们可以轻松的为一张 web 网页添加简单的图像,下一步是开始为 HTML 文档添加音频和视频的播放器. 在这篇文章当中,我们会学习到 <video> 和 <audio>  ...