05 Resource Owner Password Credentials 授权.mp4

回顾 Client Credentials

image.png

  • 客户端应用不代表用户,客户端应用本身就相当于资源所有者
  • 通常用于机器对机器的通信
  • 客户端也需要身份认证

Token 请求:

  1. POST http://xxx/connect/token HTTP/1.1
  2. Accept: application/json
  3. Content-Type: application/x-www-form-urlencoded
  4. Content-Length: 116
  5. Host: localhost:5000
  6. grant_type=client_credentials
  7. &scope=api1
  8. &client_id=console+client
  9. &client_secret=xxx

Token 响应:

  1. HTTP/1.1 200 OK
  2. Date: Thu, 02 May 2019 03:52:13 GMT
  3. Content-Type: application/json; charset=UTF-8
  4. Server: Kestrel
  5. Cache-Control: no-store, no-cache, max-age=0
  6. Pragma: no-cache
  7. Transfer-Encoding: chunked
  8. {"access_token":"xxxxxx","expires_in":3600,"token_type":"Bearer"}

可以复制 access_token 后在 jwt.io 解码查看:

image.png

Resource Owner Password Credentials

  • 资源所有者的密码凭证(例如用户名和密码)直接被用来请求 Access Token
  • 通常用于遗留的应用
  • 资源所有者和客户端应用间必须高度信任
  • 其它授权方式不可用的时候才使用,尽量不用

在 IdentityServer 中配置客户端

配置 OpenID 相关资源,并添加 WPF Client:

  1. public static IEnumerable<IdentityResource> GetIdentityResources()
  2. {
  3. return new IdentityResource[]
  4. {
  5. // 要请求下面几个 OpenID 相关的资源,必须先添加它
  6. new IdentityResources.OpenId(),
  7. new IdentityResources.Profile(),
  8. new IdentityResources.Address(),
  9. new IdentityResources.Phone(),
  10. new IdentityResources.Email()
  11. };
  12. }
  13. ...
  14. public static IEnumerable<Client> GetClients()
  15. {
  16. return new[]
  17. {
  18. // client credentials flow client
  19. ...
  20. // WPF client, password grant
  21. new Client
  22. {
  23. ClientId = "wpf client",
  24. AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
  25. ClientSecrets = {new Secret("wpf secret".Sha256())},
  26. AllowedScopes = {
  27. "api1",
  28. IdentityServerConstants.StandardScopes.OpenId,
  29. IdentityServerConstants.StandardScopes.Profile,
  30. IdentityServerConstants.StandardScopes.Email,
  31. IdentityServerConstants.StandardScopes.Address,
  32. IdentityServerConstants.StandardScopes.Phone}
  33. }
  34. };
  35. }

测试用的 User 已定义在 TestUsers 里:

  1. new TestUser{SubjectId = "818727", Username = "alice", Password = "alice",
  2. Claims =
  3. {
  4. new Claim(JwtClaimTypes.Name, "Alice Smith"),
  5. new Claim(JwtClaimTypes.GivenName, "Alice"),
  6. new Claim(JwtClaimTypes.FamilyName, "Smith"),
  7. new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"),
  8. new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
  9. ...
  10. }
  11. },

配置 WPF 客户端

整体界面:

image.png

记得安装 IdentityModel。

MainWindow 代码

  1. public partial class MainWindow : Window
  2. {
  3. private string _accessToken;
  4. private DiscoveryResponse _disco;
  5. public MainWindow()
  6. {
  7. InitializeComponent();
  8. }
  9. private async void RequestAccessToken_ButtonClick(object sender, RoutedEventArgs e)
  10. {
  11. var userName = UserNameInput.Text;
  12. var password = PasswordInput.Password;
  13. var client = new HttpClient();
  14. var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
  15. _disco = disco;
  16. if (disco.IsError)
  17. {
  18. Console.WriteLine(disco.Error);
  19. return;
  20. }
  21. // request access token
  22. var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
  23. {
  24. Address = disco.TokenEndpoint,
  25. ClientId = "wpf client",
  26. ClientSecret = "wpf secret",
  27. Scope = "api1 openid profile address phone email",
  28. UserName = userName,
  29. Password = password
  30. });
  31. if (tokenResponse.IsError)
  32. {
  33. MessageBox.Show(tokenResponse.Error);
  34. return;
  35. }
  36. _accessToken = tokenResponse.AccessToken;
  37. AccessTokenTextBlock.Text = tokenResponse.Json.ToString();
  38. }
  39. private async void RequestApi1Resource_ButtonClick(object sender, RoutedEventArgs e)
  40. {
  41. // call API1 Resource
  42. var apiClient = new HttpClient();
  43. apiClient.SetBearerToken(_accessToken);
  44. var response = await apiClient.GetAsync("http://localhost:5001/identity");
  45. if (!response.IsSuccessStatusCode)
  46. {
  47. MessageBox.Show(response.StatusCode.ToString());
  48. }
  49. else
  50. {
  51. var content = await response.Content.ReadAsStringAsync();
  52. Api1ResponseTextBlock.Text = content;
  53. }
  54. }
  55. private async void RequestIdentityResource_ButtonClick(object sender, RoutedEventArgs e)
  56. {
  57. // call Identity Resource from Identity Server
  58. var apiClient = new HttpClient();
  59. apiClient.SetBearerToken(_accessToken);
  60. var response = await apiClient.GetAsync(_disco.UserInfoEndpoint);
  61. if (!response.IsSuccessStatusCode)
  62. {
  63. MessageBox.Show(response.StatusCode.ToString());
  64. }
  65. else
  66. {
  67. var content = await response.Content.ReadAsStringAsync();
  68. IdentityResponseTextBlock.Text = content;
  69. }
  70. }
  71. }

OpenID 预设的 4 个 Scope

5.4. Requesting Claims using Scope Values

  • profile
    • OPTIONAL. This scope value requests access to the End-User’s default profile Claims, which are: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
  • email
    • OPTIONAL. This scope value requests access to the email and email_verified Claims.
  • address
    • OPTIONAL. This scope value requests access to the address Claim.
  • phone
    • OPTIONAL. This scope value requests access to the phone_number and phone_number_verified Claims.

Token 请求与响应

Token 请求:

  1. POST /oauth/token HTTP/1.1
  2. Host:authorization-server.com
  3. grant_type=password
  4. &username=user@example.com
  5. &password=xxx
  6. &client_id=xxx
  7. &client_secret=xxx

响应:

  1. {
  2. "access_token'; "MTQONjOkZmQ5OTM5NDE9ZTZjNGZmZjI3",
  3. "token_type":"bearer",
  4. "expires_in":3600,
  5. }

示意图

和 Client Credentials 相比:

  1. 多了用户角色
  2. 可以访问身份认证信息

image.png