来自 WSO2 API 网关的 JWT 不符合规范 (https://www.rfc-editor.org/rfc/rfc7519)。
我看到的所有样本都是这样的:
<Base64lEncodedHeader>.<Base64EncodedPayload>.<OPTIONAL, Base64EncodedSignature>
但应该是:
<Base64UrlEncodedHeader>.<Base64UrlEncodedPayload>.<OPTIONAL, Base64UrlEncodedSignature>
问题是使用 Base64 而不是 Base64Url 编码。由于签名基于<Base64EncodedHeader>.<Base64EncodedPayload>,并且MS JWT 框架正在根据预期的<Base64UrlEncodedHeader>.<Base64UrlEncodedPayload> 验证签名,因此它将始终无法通过验证。我不得不编写自己的自定义签名验证码来解决这个问题。然后我在使用 JwtSecurityTokenHandler 解析和解码之前从令牌中剥离签名。
这是最终代码:
try
{
// Get data and signature from unaltered token
var data = Encoding.UTF8.GetBytes(token.Split('.')[0] + '.' + token.Split('.')[1]);
var signature = Convert.FromBase64String(token.Split('.')[2]);
// Get certificate from file
var x509 = new X509Certificate2(HttpContext.Current.Server.MapPath("~/App_Data/" + ConfigurationManager.AppSettings["CertFileName"]));
// Verify the data with the signature
var csp = (RSACryptoServiceProvider)x509.PublicKey.Key;
if (!csp.VerifyData(data, "SHA256", signature))
{
// Signature verification failed; data was possibly altered
throw new SecurityTokenValidationException("Data signature verification failed. Token cannot be trusted!");
}
// strip off signature from token
token = token.Substring(0, token.LastIndexOf('.') + 1);
// Convert Base64 encoded token to Base64Url encoding
token = token.Replace('+', '-').Replace('/', '_').Replace("=", "");
// Use JwtSecurityTokenHandler to validate the JWT token
var tokenHandler = new JwtSecurityTokenHandler();
// Read the JWT
var parsedJwt = tokenHandler.ReadToken(token);
// Set the expected properties of the JWT token in the TokenValidationParameters
var validationParameters = new TokenValidationParameters()
{
NameClaimType = "http://wso2.org/claims/enduser",
AuthenticationType = ((JwtSecurityToken)parsedJwt).Claims.Where(c => c.Type == "http://wso2.org/claims/usertype").First().Value,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = false,
RequireExpirationTime = true,
RequireSignedTokens = false,
//ValidAudience = ConfigurationManager.AppSettings["AllowedAudience"],
ValidIssuer = ConfigurationManager.AppSettings["Issuer"],
//IssuerSigningToken = new X509SecurityToken(cert),
CertificateValidator = X509CertificateValidator.None
};
// Set both HTTP Context and Thread principals, so they will be in sync
HttpContext.Current.User = tokenHandler.ValidateToken(token, validationParameters, out parsedJwt);
Thread.CurrentPrincipal = HttpContext.Current.User;
// Treat as ClaimsPrincipal, extract JWT expiration and inject it into request headers
var cp = (ClaimsPrincipal)Thread.CurrentPrincipal;
context.Request.Headers.Add("JWT-Expiration", cp.FindFirst("exp").Value);
}
catch (SecurityTokenValidationException stvErr)
{
// Log error
if (context.Trace.IsEnabled)
context.Trace.Write("JwtAuthorization", "Error validating token.", stvErr);
}
catch (System.Exception ex)
{
// Log error
if (context.Trace.IsEnabled)
context.Trace.Write("JwtAuthorization", "Error parsing token.", ex);
}