如何从 .net core 客户端生成 JWT Bearer Flow OAuth 访问令牌?

4
我在使用.NET Core客户端生成“JWT Bearer Flow”类型的OAuth访问令牌时遇到了问题,这是用于Salesforce端点所要求的。看起来有限的.NET Framework示例显示了.NET客户端执行此操作,但没有显示.NET Core客户端执行此操作,例如: https://salesforce.stackexchange.com/questions/53662/oauth-jwt-token-bearer-flow-returns-invalid-client-credentials
因此,在我的.NET Core 3.1应用程序中,我已生成了自签名证书,并在加载证书时将私钥添加到上述示例代码中,但是在以下行上发生了System.InvalidCastException异常:
var rsa = certificate.GetRSAPrivateKey() as RSACryptoServiceProvider;

异常:

System.InvalidCastException: 'Unable to cast object of type 'System.Security.Cryptography.RSACng' to type 'System.Security.Cryptography.RSACryptoServiceProvider'.'

看起来这个私钥被用作JWT Bearer Flow的签名部分,或许在.NET Core中没有像在.NET Framework中使用RSACryptoServiceProvider。

我的问题是这样的 - 在.NET Core中是否有一种方法生成OAuth JWT Bearer Flow的访问令牌?

我正在使用的完整代码:

static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
        var token = GetAccessToken();
    }

    static dynamic GetAccessToken()
    {
        // get the certificate
        var certificate = new X509Certificate2(@"C:\temp\cert.pfx");

        // create a header
        var header = new { alg = "RS256" };

        // create a claimset
        var expiryDate = GetExpiryDate();
        var claimset = new
        {
            iss = "xxxxxx",
            prn = "xxxxxx",
            aud = "https://test.salesforce.com",
            exp = expiryDate
        };

        // encoded header
        var headerSerialized = JsonConvert.SerializeObject(header);
        var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
        var headerEncoded = ToBase64UrlString(headerBytes);

        // encoded claimset
        var claimsetSerialized = JsonConvert.SerializeObject(claimset);
        var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
        var claimsetEncoded = ToBase64UrlString(claimsetBytes);

        // input
        var input = headerEncoded + "." + claimsetEncoded;
        var inputBytes = Encoding.UTF8.GetBytes(input);

        // signature
        var rsa = (RSACryptoServiceProvider) certificate.GetRSAPrivateKey();

        var cspParam = new CspParameters
        {
            KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
            KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
        };
        var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
        var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
        var signatureEncoded = ToBase64UrlString(signatureBytes);

        // jwt
        var jwt = headerEncoded + "." + claimsetEncoded + "." + signatureEncoded;

        var client = new WebClient();
        client.Encoding = Encoding.UTF8;
        var uri = "https://login.salesforce.com/services/oauth2/token";
        var content = new NameValueCollection();

        content["assertion"] = jwt;
        content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";

        string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));

        var result = JsonConvert.DeserializeObject<dynamic>(response);

        return result;
    }

    static int GetExpiryDate()
    {
        var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        var currentUtcTime = DateTime.UtcNow;

        var exp = (int)currentUtcTime.AddMinutes(4).Subtract(utc0).TotalSeconds;

        return exp;
    }

    static string ToBase64UrlString(byte[] input)
    {
        return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
    }
2个回答

4

好吧 - 结果是在stackoverflow上发布后,大脑开始运转。

最终答案是深入研究这里的类似问题,并使用x509certificate2 sign for jwt in .net core 2.1的解决方案。

最终,我替换了以下代码:

var cspParam = new CspParameters
{
      KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
      KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
var signatureEncoded = ToBase64UrlString(signatureBytes);

使用这段代码需要利用System.IdentityModel.Tokens.Jwt nuget包:

var signingCredentials = new X509SigningCredentials(certificate, "RS256");
var signature = JwtTokenUtilities.CreateEncodedSignature(input, signingCredentials);

解决方案后的完整代码如下:
static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
        var token = GetAccessToken();
    }

    static dynamic GetAccessToken()
    {
        // get the certificate
        var certificate = new X509Certificate2(@"C:\temp\cert.pfx");

        // create a header
        var header = new { alg = "RS256" };

        // create a claimset
        var expiryDate = GetExpiryDate();
        var claimset = new
        {
            iss = "xxxxx",
            prn = "xxxxx",
            aud = "https://test.salesforce.com",
            exp = expiryDate
        };

        // encoded header
        var headerSerialized = JsonConvert.SerializeObject(header);
        var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
        var headerEncoded = ToBase64UrlString(headerBytes);

        // encoded claimset
        var claimsetSerialized = JsonConvert.SerializeObject(claimset);
        var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
        var claimsetEncoded = ToBase64UrlString(claimsetBytes);

        // input
        var input = headerEncoded + "." + claimsetEncoded;
        var inputBytes = Encoding.UTF8.GetBytes(input);

        var signingCredentials = new X509SigningCredentials(certificate, "RS256");
        var signature = JwtTokenUtilities.CreateEncodedSignature(input, signingCredentials);

        // jwt
        var jwt = headerEncoded + "." + claimsetEncoded + "." + signature;

        var client = new WebClient();
        client.Encoding = Encoding.UTF8;
        var uri = "https://test.salesforce.com/services/oauth2/token";
        var content = new NameValueCollection();

        content["assertion"] = jwt;
        content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";

        string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));

        var result = JsonConvert.DeserializeObject<dynamic>(response);

        return result;
    }

    static int GetExpiryDate()
    {
        var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        var currentUtcTime = DateTime.UtcNow;

        var exp = (int)currentUtcTime.AddMinutes(4).Subtract(utc0).TotalSeconds;

        return exp;
    }

    static string ToBase64UrlString(byte[] input)
    {
        return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
    }

这是一个很好的例子。在我们的沙盒环境中尝试时,我遇到了400错误。有没有关于追踪问题的建议?证书和签名看起来都没问题。 - LeeFranke
我会仔细查看400响应文本,它应该能给你一个错误的提示。可能是你的证书、发行者/用户名信息、在Salesforce中为连接的应用程序设置OAuth的方式等问题。以上内容对我们在所有Salesforce环境中进行JWT OAuth身份验证都有效。 - Jack S
你好,我按照你的步骤获取了签名凭证。但是在获取签名时遇到了问题。如果您能提供帮助,将不胜感激。在获取签名时,我遇到了以下异常:"System.InvalidOperationException: 'IDX10638: 无法创建 SignatureProvider,'key.HasPrivateKey' 为 false,无法创建签名。密钥:[PII 已隐藏。有关详细信息,请参见 " - shehanpathi

0

我回复这个问题只是因为一个类似的答案在我第一次访问此页面时会帮助我很多。

首先,你不必从C#客户端生成JWT。

要生成JWT令牌,您可以使用此网站:https://jwt.io/

有一个非常好的视频展示如何生成JWT令牌: https://www.youtube.com/watch?v=cViU2-xVscA&t=1680s

生成后,从您的C#客户端使用它调用获取access_token的终端点 https://developer.salesforce.com/docs/atlas.en-us.api_iot.meta/api_iot/qs_auth_access_token.htm (观看YouTube上的视频)

如果一切正常,你将得到access_token

要运行API调用,你只需要access_token而不是JWT。
一旦你拥有它,将其添加到HTTP调用中,就像这样。
public static void AddBearerToken(this HttpRequestMessage request, string accessToken)
    {
        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
    }

有时候 access_token 会过期。为了检查其有效性,您可以调用令牌 introspect api https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oidc_token_introspection_endpoint.htm&type=5

您需要传递两个额外的参数:client_id 和 client_secret。

  • client_id 是 Consumer Key。您可以从 Salesforce 中的 Connected App 获取它。
  • client_server 是 Consumer Secret。您可以从 Salesforce 中的 Connected App 获取它。

如果 introspect token API 返回响应

{ active: false, ...  }

这意味着 access_token 已过期,您需要发出新的令牌。 要发出新的 access_token,只需再次使用相同的 JWT 调用 "/services/oauth2/token"。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接