問題描述

在APIM中配置對傳入的Token進行預驗證,確保傳入後端被保護的API的Authorization資訊正確有效,可以使用validate-jwt策略。validate-jwt 策略強制要求從指定 HTTP 標頭或指定查詢引數提取的 JSON Web 令牌 (JWT) 必須存在且有效。validate-jwt 策略支援 HS256 和 RS256 簽名演算法。

  • 對於 HS256,必須在策略中以 base64 編碼形式提供內聯方式的金鑰。
  • 對於 RS256,金鑰可以通過 Open ID 配置終結點來提供,或者通過提供包含公鑰或公鑰的模數指數對的已上傳證書的 ID 來提供。
在最開始使用HS256簽名演算法的Token時,在validate-jwt策略中配置 issuer-signing-keys就能成功驗證JWT,但是當使用RS256簽名演算法適合,這樣就不行。會丟擲 'System.NotSupportedException: IDX10634: Unable to create the SignatureProvider.\nAlgorithm: 'RS256', SecurityKey: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey... 錯誤。
 

HS256配置

  1. <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unvalid authorization" require-expiration-time="true" require-signed-tokens="true">
  2. <issuer-signing-keys>
  3. <key>在HS256演算法中使用的金鑰,進行base64編碼後的值</key>
  4. </issuer-signing-keys>
  5. <audiences>
  6. <audience>在生成JWT Token時設定的aud值</audience>
  7. </audiences>
  8. </validate-jwt>

RS256配置(附帶錯誤訊息)

  1. <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unvalid authorization" require-expiration-time="true" require-signed-tokens="true">
  2. <issuer-signing-keys>
  3. <key><RS256 公鑰內容通過base64編碼後的值></key>
  4. </issuer-signing-keys>
  5. <audiences>
  6. <audience>在生成JWT Token時設定的aud值</audience>
  7. </audiences>
  8. </validate-jwt>

在進行驗證時錯誤訊息(文末附錄中包含如何在APIM門戶中通過Test功能檢測策略的執行結果及錯誤

  1. validate-jwt (-0.132 ms)
  2. {
  3. "message": "JWT Validation Failed: IDX10503: Signature validation failed.
    Keys tried: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '', InternalId: 'HuvkEY3HBhujvk2qeDfgPFD2iYc-GYrnlDX6Yd1LsYQ'. ,
    KeyId: \r\n'.\nExceptions caught:\n 'System.NotSupportedException: IDX10634: Unable to create the SignatureProvider.\nAlgorithm: 'RS256',
    SecurityKey: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '',
    InternalId: 'HuvkEY3HBhujvk2qeDfgPFD2iYc-GYrnlDX6Yd1LsYQ'.'\n is not supported.
    The list of supported algorithms is available here: https://aka.ms/IdentityModel/supported-algorithms\r\n
    at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateSignatureProvider(SecurityKey key, String algorithm, Boolean willCreateSignatures, Boolean cacheProvider)\r\n
    at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateForVerifying(SecurityKey key, String algorithm, Boolean cacheProvider)\r\n
    at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(Byte[] encodedBytes, Byte[] signature, SecurityKey key, String algorithm, SecurityToken securityToken, TokenValidationParameters validationParameters)\r\n
    at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token,
    TokenValidationParameters validationParameters)\r\n'.
    \ntoken: '{\"typ\":\"jwt\",\"alg\":\"RS256\"}.{\"aud\":\"xxxxx.azure-api.cn\",\"user_id\":123456,\"username\":\"lutpython\",\"exp\":1631786725}'.."
  4. }

那麼, 如何來解決RS256 JWT的驗證問題呢?

解決方案

正如APIM官網中特別提醒的一句話“對於 RS256,金鑰可以通過 Open ID 配置終結點來提供,或者通過提供包含公鑰或公鑰的模數指數對的已上傳證書的 ID 來提供 (英文原文:For RS256 the key may be provided either via an Open ID configuration endpoint, or by providing the ID of an uploaded certificate that contains the public key or modulus-exponent pair of the public key.)” , 所以APIM 中的validate-jwt是不支援使用直接配置公鑰(Public Key)。 目前的方案有兩種:

1) 使用openid configuration, OpenID Configuration中包含了公鑰的內容,是有提供Token的許可權伺服器管理提供(如 Azure AD中就自動包含OpenID Configuration Endpoint).  注:這部分的詳細介紹可以參考官網:https://docs.azure.cn/zh-cn/api-management/api-management-access-restriction-policies#azure-active-directory-token-validation

2) 把有證書機構(CA)頒發的包含公鑰(Publick Key)的 .pfx證書上傳到APIM中,然後配置 key certificate-id="<上傳在APIM證書中的ID值>"

  1. <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unvalid authorization" require-expiration-time="true" require-signed-tokens="true">
  2. <issuer-signing-keys>
  3. <key certificate-id="<上傳在APIM證書中的ID值>" />
  4. </issuer-signing-keys>
  5. <audiences>
  6. <audience>在生成JWT Token時設定的aud值</audience>
  7. </audiences>
  8. </validate-jwt>

方案步驟

可以使用以下的步驟來驗證RS256 JWT:

1) 使用 openssl 指令建立 證書 (Local.pfx)

  1. openssl.exe req -x509 -nodes -sha256 -days 3650 -subj "/CN=Local" -newkey rsa:2048 -keyout Local.key -out Local.crt

  2. openssl.exe pkcs12 -export -in Local.crt -inkey Local.key -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider" -out Local.pfx

openssl.exe 下載地址:https://slproweb.com/products/Win32OpenSSL.html, 使用時需要 cd到 openssl.exe所在的bin目錄中

2)上傳 Local.pfx 檔案到APIM,並自定義證書ID, 如:apim-rs256-01

3)在APIM的策略中(API級,單個操作級別,或者一組API[產品], 或者全部的APIs)。validate-jwt 內容如下:

  1. <policies>
  2. <inbound>
  3. <base />
  4. <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="401 unauthorized">
  5. <issuer-signing-keys>
  6. <key certificate-id="apim-rs256-01" />
  7. </issuer-signing-keys>
  8. </validate-jwt>
  9. </inbound>
  10. <backend>
  11. <base />
  12. </backend>
  13. <outbound>
  14. <base />
  15. </outbound>
  16. <on-error>
  17. <base />
  18. </on-error>
  19. </policies>

4) 使用以下的C#程式碼生成 Token

  1. using Microsoft.IdentityModel.Tokens;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IdentityModel.Tokens.Jwt;
  5. using System.Linq;
  6. using System.Security.Claims;
  7. using System.Security.Cryptography;
  8. using System.Security.Cryptography.X509Certificates;
  9. using System.Text;
  10. using System.Threading.Tasks;
  11.  
  12. namespace ConsoleAppjwt
  13. {
  14. class Program
  15. {
  16. static void Main(string[] args)
  17. { // Token Generation
  18. var CLIENT_ID = "Local";
  19. var ISSUER_GUID = "b0123cec-86bb-4eb2-8704-dcf7cb2cc279";
  20.  
  21. var filePath = @"C:\Users\xxxx\source\repos\ConsoleAppjwt\ConsoleAppjwt\Cert\Local.pfx";
  22. var x509Certificate2 = new X509Certificate2(filePath, "123456789");
  23.  
  24. var signingCredentials = new X509SigningCredentials(x509Certificate2, SecurityAlgorithms.RsaSha256Signature); //, SecurityAlgorithms.Sha256Digest
  25. var tokenHandler = new JwtSecurityTokenHandler();
  26.  
  27. var originalIssuer = $"{CLIENT_ID}";
  28. var issuer = originalIssuer;
  29.  
  30. DateTime utcNow = DateTime.UtcNow;
  31. DateTime expired = utcNow + TimeSpan.FromHours(1);
  32.  
  33. var claims = new List<Claim> {
  34. new Claim("aud", "https://login.microsoftonline.com/{YOUR_TENENT_ID}/oauth2/token", ClaimValueTypes.String, issuer, originalIssuer),
  35. new Claim("exp", "1460534173", ClaimValueTypes.DateTime, issuer, originalIssuer),
  36. new Claim("jti", $"{ISSUER_GUID}", ClaimValueTypes.String, issuer, originalIssuer),
  37. new Claim("nbf", "1460533573", ClaimValueTypes.String, issuer, originalIssuer),
  38. new Claim("sub", $"{CLIENT_ID}", ClaimValueTypes.String, issuer, originalIssuer)
  39. };
  40.  
  41. ClaimsIdentity subject = new ClaimsIdentity(claims: claims);
  42.  
  43. var tokenDescriptor = new SecurityTokenDescriptor
  44. {
  45. Subject = subject,
  46. Issuer = issuer,
  47. Expires = expired,
  48. SigningCredentials = signingCredentials,
  49. };
  50.  
  51. JwtSecurityToken jwtToken = tokenHandler.CreateToken(tokenDescriptor) as JwtSecurityToken;
  52. jwtToken.Header.Remove("typ");
  53. var token = tokenHandler.WriteToken(jwtToken);
  54.  
  55. Console.WriteLine(token);
  56. //Start to Verify Token
  57. Console.WriteLine("Start to Verify Token");
  58. ValidationToken(token);
  59.  
  60. Console.ReadLine();
  61. }
  62.  
  63. static void ValidationToken(string token)
  64. {
  65. try
  66. {
  67. JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
  68. var filePath = @"C:\Users\xxxx\source\repos\ConsoleAppjwt\ConsoleAppjwt\Cert\Local.pfx";
  69.  
  70. TokenValidationParameters tvParameter = new TokenValidationParameters
  71. {
  72. ValidateIssuerSigningKey = true,
  73. IssuerSigningKey = new X509SecurityKey(new X509Certificate2(filePath, "123456789")),
  74. ValidateIssuer = false,
  75. ValidateAudience = false,
  76. // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
  77. ClockSkew = TimeSpan.Zero
  78. };
  79. SecurityToken stoken;
  80. jwtHandler.ValidateToken(token, tvParameter, out stoken);
  81. Console.WriteLine(stoken);
  82. Console.WriteLine("Validate Token Success");
  83. }
  84. catch (Exception ex)
  85. {
  86. Console.WriteLine(ex);
  87. }
  88. }
  89. }
  90. }

5) 呼叫APIM介面對Authorization驗證

當然,也可以直接在呼叫APIM的客戶端中進行驗證。

附錄一:在APIM的介面配置頁中,Test頁面點選Send按鈕,傳送API的請求,通過頁面中的Trace部分檢視每一部分的耗時,處理結果,或錯誤詳情。

參考資料

驗證 JWT:https://docs.azure.cn/zh-cn/api-management/api-management-access-restriction-policies#validate-jwt

How to validate JWT signed with RS256 Algorithm with validate-jwt policy in Azure API management :https://stackoverflow.com/questions/37050233/how-to-validate-jwt-signed-with-rs256-algorithm-with-validate-jwt-policy-in-azur