如何为云函数的HTTP触发器生成JWT?

huangapple go评论75阅读模式
英文:

How to generate a JWT for a Cloud Function HTTP trigger?

问题

以下是翻译好的内容:

我正在尝试生成一个JWT,用于调用我部署的GCP Cloud Function上的HTTP触发器。
我已经使用'allUsers'部署了我的函数并验证了它的工作,但我希望它更安全,所以我需要将JWT附加到我的HTTP请求中。
我正在遵循这个代码,以下代码片段大部分来自于此。

首先,我创建了一个名为testharness-sa的服务帐号,并下载了其密钥文件。当我运行我的测试时,我有一个名为GOOGLE_APPLICATION_CREDENTIALS的环境变量,指向该文件。然后我运行了以下命令:

gcloud functions add-iam-policy-binding gopubsub \
  --member='serviceAccount:testharness-sa@PROJECT_NAME.iam.gserviceaccount.com' \
  --role='roles/cloudfunctions.invoker'

这通过列出云函数上的所有当前绑定,包括testharness-sa,来为我提供了确认。

我的核心代码如下:

private String getSignedJwt() {
    GoogleCredentials credentials = GoogleCredentials
            .getApplicationDefault()
            .createScoped(Collections.singleton(IAM_SCOPE));
    long now = System.currentTimeMillis();
    RSAPrivateKey privateKey = (RSAPrivateKey) credentials.getPrivateKey();
    Algorithm algorithm = Algorithm.RSA256(null, privateKey);
    return JWT.create()
            .withKeyId(credentials.getPrivateKeyId())
            .withIssuer(credentials.getClientEmail())
            .withSubject(credentials.getClientEmail())
            .withAudience(OAUTH_TOKEN_AUDIENCE)
            .withIssuedAt(new Date(now))
            .withExpiresAt(new Date(now + EXPIRATION_TIME_IN_MILLIS))
            .withClaim("target_audience", clientId)
            .sign(algorithm);
}

这为我生成了一个已签名的JWT。据我理解,这用于调用GCP以获取我可以用来调用我的云函数的最终JWT。

一旦我生成了已签名的JWT,我会这样使用它:

String jwt = getSignedJwt();
final GenericData tokenRequest = new GenericData()
        .set("grant_type", JWT_BEARER_TOKEN_GRANT_TYPE)
        .set("assertion", jwt);
final UrlEncodedContent content = new UrlEncodedContent(tokenRequest);

final HttpRequestFactory requestFactory = httpTransport.createRequestFactory();

final HttpRequest request = requestFactory
        .buildPostRequest(new GenericUrl(OAUTH_TOKEN_URI), content)
        .setParser(new JsonObjectParser(JacksonFactory.getDefaultInstance()));

HttpResponse response = request.execute();
...

因此,它获取了已签名的JWT,并创建了一个请求以获取最终的JWT,然后... 失败了。错误消息是“Invalid JWT: Failed audience check”。
看起来我有一个错误的参数,让我们来看看我的参数(它们实际上是常量,而不是参数):

private static final String IAM_SCOPE = "https://www.googleapis.com/auth/iam";
//"https://www.googleapis.com/auth/cloud-platform";
private static final String OAUTH_TOKEN_URI = "https://oauth2.googleapis.com/token";
//"https://www.googleapis.com/oauth2/v4/token";
private static final String OAUTH_TOKEN_AUDIENCE = "https://us-central1-PROJECT_NAME.cloudfunctions.net/gopubsub";
//"https://www.googleapis.com/token";
private static final String JWT_BEARER_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
private static final long EXPIRATION_TIME_IN_MILLIS = 3600 * 1000L;

我已经添加了其他变体作为常量的注释。

根据错误消息,看起来我正在使用错误的受众值。
此页面建议受众应为服务帐号电子邮件,我认为我在其他地方看到过应该是云函数的URL,但这两者都对我不起作用。

我确实知道这可以工作,因为我可以发出这个命令:

gcloud auth print-identity-token

这会给我最终的JWT(只要我的GOOGLE_APPLICATION_CREDENTIALS指向我的JSON文件)。
我可以将该JWT粘贴到一个curl命令中,并成功调用HTTP触发器,如果我不带JWT,它就会失败,所以我知道JWT正在被检查。
但到目前为止,我还不知道如何从我的Java代码中执行类似于gcloud auth print-identity-token的操作。
有人知道吗?

英文:

What I'm trying to do is generate a JWT for calling the HTTP trigger on a GCP Cloud Function I've deployed.
I've already deployed my function with 'allUsers' and verified it works, but I want it to be more secure, so I need to attach a JWT to my HTTP request.
I'm following this code and the following snippets are mostly from that.
I think I am close, but not quite there yet. In all of the samples etc below I've changed my project name to PROJECT_NAME.

First I created a service account named testharness-sa and downloaded its key file. I have an env var GOOGLE_APPLICATION_CREDENTIALS pointing to that file when I run my tests. Then I ran the following command:

gcloud functions add-iam-policy-binding gopubsub \
  --member='serviceAccount:testharness-sa@PROJECT_NAME.iam.gserviceaccount.com' \
  --role='roles/cloudfunctions.invoker'

This gave me a confirmation by listing all the current bindings on my cloud function, including testharness-sa.

The core of my code is this:

    private String getSignedJwt() {
        GoogleCredentials credentials = GoogleCredentials
                .getApplicationDefault()
                .createScoped(Collections.singleton(IAM_SCOPE));
        long now = System.currentTimeMillis();
        RSAPrivateKey privateKey = (RSAPrivateKey) credentials.getPrivateKey();
        Algorithm algorithm = Algorithm.RSA256(null, privateKey);
        return JWT.create()
                .withKeyId(credentials.getPrivateKeyId())
                .withIssuer(credentials.getClientEmail())
                .withSubject(credentials.getClientEmail())
                .withAudience(OAUTH_TOKEN_AUDIENCE)
                .withIssuedAt(new Date(now))
                .withExpiresAt(new Date(now + EXPIRATION_TIME_IN_MILLIS))
                .withClaim("target_audience", clientId)
                .sign(algorithm);
    }

This gives me a signed JWT. As I understand things, this is used to call GCP to get me a final JWT I can use to call my cloud function.

Once I generate the signed JWT I use it like this:

        String jwt = getSignedJwt();
        final GenericData tokenRequest = new GenericData()
                .set("grant_type", JWT_BEARER_TOKEN_GRANT_TYPE)
                .set("assertion", jwt);
        final UrlEncodedContent content = new UrlEncodedContent(tokenRequest);

        final HttpRequestFactory requestFactory = httpTransport.createRequestFactory();

        final HttpRequest request = requestFactory
                .buildPostRequest(new GenericUrl(OAUTH_TOKEN_URI), content)
                .setParser(new JsonObjectParser(JacksonFactory.getDefaultInstance()));

        HttpResponse response = request.execute();
...

So it gets the signed JWT and makes up a request to get the final JWT and then... fails. The error is "Invalid JWT: Failed audience check"
It looks like I have a bad parameter, so let's look at my parameters (they are actually constants, not parameters):

    private static final String IAM_SCOPE = "https://www.googleapis.com/auth/iam";
	//"https://www.googleapis.com/auth/cloud-platform";
    private static final String OAUTH_TOKEN_URI = "https://oauth2.googleapis.com/token";
	//"https://www.googleapis.com/oauth2/v4/token";
    private static final String OAUTH_TOKEN_AUDIENCE = "https://us-central1-PROJECT_NAME.cloudfunctions.net/gopubsub";
	//"https://www.googleapis.com/token";
    private static final String JWT_BEARER_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
    private static final long EXPIRATION_TIME_IN_MILLIS = 3600 * 1000L;

I've added other variants as comments against the constants.

Based on the error message it looks like the audience value I'm using is wrong.
This page suggests the audience should be the service account email, and I think I've seen elsewhere that it ought to be the URL of the cloud function, but neither of those work for me.

I do know this can work because I can issue this command:

gcloud auth print-identity-token

which gives me the final JWT (as long as I have GOOGLE_APPLICATION_CREDENTIALS pointing at my json file).
I can paste that JWT into a curl command and invoke the HTTP trigger successfully, and if I leave the JWT out it fails, so I know the JWT is being checked.
But so far I don't know how to do the equivalent of gcloud auth print-identity-token from my Java code.
Anyone know?

答案1

得分: 0

我发现答案与范围无关,尽管错误消息中提到了它。实际上与我传递的 clientId 值有关(尽管在我的问题中没有真正提到,但代码中有)。我使用了在服务账户 JSON 文件中找到的值,一个非常长的数字字符串。那就是问题所在。它需要是我的 HTTP 触发器的 URL。所以这些是我最终得出的常量:

    private static final String IAM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
    private static final String OAUTH_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
    private static final String OAUTH_TOKEN_AUDIENCE = "https://www.googleapis.com/oauth2/v4/token";
    private static final String JWT_BEARER_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
    private static final long EXPIRATION_TIME_IN_MILLIS = 3600 * 1000L;

实际上,如果我删除所有对 IAM_SCOPE 的引用,它仍然有效,所以我得出结论它并不重要。将其组合到一起的代码片段现在如下所示:

        return JWT.create()
                .withKeyId(credentials.getPrivateKeyId())
                .withIssuer(credentials.getClientEmail())
                .withSubject(credentials.getClientEmail())
                .withAudience(OAUTH_TOKEN_AUDIENCE)
                .withIssuedAt(new Date(now))
                .withExpiresAt(new Date(now + EXPIRATION_TIME_IN_MILLIS))
                .withClaim("target_audience", targetURL)
                .sign(algorithm);

具体的更改是 withClaim() 调用,其中现在包含我想要调用的 URL。有了这个设置,代码将返回我所期望的 JWT,而且实际上,当我调用受保护的 HTTP 触发器 URL 时,它确实有效。希望这能帮助其他人。

英文:

I found the answer was not related to scope, despite the error message. It is actually related to the clientId value I was passing (and didn't really mention in my question, though it is there in the code). I was using a value I found in the service account json file, a really long string of digits. That was the problem. It needed to be the URL of my HTTP trigger. So these are the constants I ended up with:

    private static final String IAM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
    private static final String OAUTH_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
    private static final String OAUTH_TOKEN_AUDIENCE = "https://www.googleapis.com/oauth2/v4/token";
    private static final String JWT_BEARER_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
    private static final long EXPIRATION_TIME_IN_MILLIS = 3600 * 1000L;

In fact it still works if I remove all references to IAM_SCOPE, so I conclude that doesn't matter. The piece of code that pulls it together now looks like this:

        return JWT.create()
                .withKeyId(credentials.getPrivateKeyId())
                .withIssuer(credentials.getClientEmail())
                .withSubject(credentials.getClientEmail())
                .withAudience(OAUTH_TOKEN_AUDIENCE)
                .withIssuedAt(new Date(now))
                .withExpiresAt(new Date(now + EXPIRATION_TIME_IN_MILLIS))
                .withClaim("target_audience", targetURL)
                .sign(algorithm);

Specifically the change is the withClaim() call which now contains the URL I want to call. With that in place the code returns the JWT I was hoping for and which does, in fact, work when I call the secured HTTP trigger URL. Hope that helps someone else.

huangapple
  • 本文由 发表于 2020年4月5日 10:16:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/61037206.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定