简体   繁体   English

Java Web GoogleSignin - GoogleIdTokenVerifier 验证令牌字符串返回 null

[英]Java Web GoogleSignin - GoogleIdTokenVerifier verify token string returns null

I am adding google register/signin to my web app and i have encountered a problem.我正在将谷歌注册/登录添加到我的 web 应用程序中,但遇到了问题。

This is my code:这是我的代码:

private static final HttpTransport transport = new NetHttpTransport();
private static final JsonFactory jsonFactory = new JacksonFactory();
private static final String MY_APP_GOOGLE_CLIENT_ID = "wouldntyouliketoknow";

public UsernamePasswordAuthenticationToken verify(final String idTokenString){

    GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
                                            .setAudience(Collections.singletonList(MY_APP_GOOGLE_CLIENT_ID))
                                            .build();

    try {
        GoogleIdToken idToken = verifier.verify(idTokenString);// <-- verifier.verify returns null !!!
        if (idToken != null) {
            Payload payload = idToken.getPayload();
            String email = payload.getEmail();
            if(Boolean.valueOf(payload.getEmailVerified())){
                UserJPA jpa = userRepository.findByEmail(email);
                if(jpa==null){
                    throw new UsernameNotFoundException("Cannot find user with email = "+email);
                }
                if(!jpa.isRegisterredWithGoogle()){
                    throw new UsernameNotFoundException("This user did not use the 'Register with google' option.");
                }
                bokiAuthenticationProvider.checkUserActiveAndUnlocked(jpa);

                return new UsernamePasswordAuthenticationToken(jpa.getUsername(), jpa.getPasswordHesh(), 
                        bokiAuthenticationProvider.getAuthorities(jpa.getUserHasRoleSecurityList()));
            }
        }else{
            System.out.println("The *idToken* object is null !!!");
        }
    } catch (GeneralSecurityException | IOException e) {
        e.printStackTrace();
    }

    throw new MyCustomException("Google token is invalid or has expired");
}

To create my CLIENT_ID I followed instructions here:要创建我的 CLIENT_ID,我按照此处的说明进行操作:

https://developers.google.com/identity/sign-in/web/devconsole-project https://developers.google.com/identity/sign-in/web/devconsole-project

The problem is that verifier.verify keeps returning null .问题是verifier.verify不断返回null

I have checked:我检查过:

  • my user did register with google and the database fields are properly filled我的用户确实在谷歌注册,并且数据库字段已正确填写

  • i am getting different string tokens from google each time i try google_sign_in每次尝试 google_sign_in 时,我都会从 google 获得不同的字符串令牌

  • my CLIENT_ID is valid and active in the google console.我的CLIENT_ID在 Google 控制台中有效且处于活动状态。

To add to the confusion, this whole thing worked fine just a month ago.更令人困惑的是,整件事在一个月前还运行良好。 I went away on sick leave and when i came back, my boss welcomed me with this issue.我请了病假,回来时,我的老板用这个问题欢迎我。

Anyone have any idea what might have happened?有人知道可能发生了什么吗?

I finally figured it out.我终于弄明白了。

Since no one knew how to help me, i dropped the designated google libraries and went and made my own token verification from scratch.由于没有人知道如何帮助我,我放弃了指定的 google 库,并从头开始进行自己的令牌验证。

I went and used the google-token-verifier-url-tool here :我去这里使用了 google-token-verifier-url-tool :

https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123 https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=XYZ123

At the bottom of the page here在页面底部这里

https://developers.google.com/identity/protocols/OpenIDConnect https://developers.google.com/identity/protocols/OpenIDConnect

, i found how to decipher the json. ,我找到了如何破译json。

What i do is, I contact their online tool in code, get the json response and verify it manually.我所做的是,我用代码联系他们的在线工具,获取 json 响应并手动验证。 This is my code :这是我的代码:

    private Map<String,String> getMapFromGoogleTokenString(final String idTokenString){
        BufferedReader in = null;
        try {
            // get information from token by contacting the google_token_verify_tool url :
            in = new BufferedReader(new InputStreamReader(
                                        ((HttpURLConnection) (new URL("https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=" + idTokenString.trim()))
                                        .openConnection()).getInputStream(), Charset.forName("UTF-8")));

            // read information into a string buffer :
            StringBuffer b = new StringBuffer();
            String inputLine;
            while ((inputLine = in.readLine()) != null){
                b.append(inputLine + "\n");
            }

            // transforming json string into Map<String,String> :
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.readValue(b.toString(), objectMapper.getTypeFactory().constructMapType(Map.class, String.class, String.class));

        // exception handling :
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch(Exception e){
            System.out.println("\n\n\tFailed to transform json to string\n");
            e.printStackTrace();
        } finally{
            if(in!=null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    // chack the "email_verified" and "email" values in token payload 
    private boolean verifyEmail(final Map<String,String> tokenPayload){
        if(tokenPayload.get("email_verified")!=null && tokenPayload.get("email")!=null){
            try{
                return Boolean.valueOf(tokenPayload.get("email_verified")) && tokenPayload.get("email").contains("@gmail.");
            }catch(Exception e){
                System.out.println("\n\n\tCheck emailVerified failed - cannot parse "+tokenPayload.get("email_verified")+" to boolean\n");
            }
        }else{
            System.out.println("\n\n\tCheck emailVerified failed - required information missing in the token");
        }
        return false;
    }

    // check token expiration is after now :
    private boolean checkExpirationTime(final Map<String,String> tokenPayload){
        try{
            if(tokenPayload.get("exp")!=null){
                // the "exp" value is in seconds and Date().getTime is in mili seconds
                return Long.parseLong(tokenPayload.get("exp")+"000") > new java.util.Date().getTime();
            }else{
                System.out.println("\n\n\tCheck expiration failed - required information missing in the token\n");
            }
        }catch(Exception e){
            System.out.println("\n\n\tCheck expiration failed - cannot parse "+tokenPayload.get("exp")+" into long\n");
        }
        return false;
    }

    // check that at least one CLIENT_ID matches with token values
    private boolean checkAudience(final Map<String,String> tokenPayload){
        if(tokenPayload.get("aud")!=null && tokenPayload.get("azp")!=null){
            List<String> pom = Arrays.asList("MY_CLIENT_ID_1",
                                             "MY_CLIENT_ID_2",
                                             "MY_CLIENT_ID_3");

            if(pom.contains(tokenPayload.get("aud")) || pom.contains(tokenPayload.get("azp"))){
                return true;
            }else{
                System.out.println("\n\n\tCheck audience failed - audiences differ\n");
                return false;
            }
        }
        System.out.println("\n\n\tCheck audience failed - required information missing in the token\n");
        return false;
    }

    // verify google token payload :
    private boolean doTokenVerification(final Map<String,String> tokenPayload){
        if(tokenPayload!=null){
            return verifyEmail(tokenPayload) // check that email address is verifies 
                && checkExpirationTime(tokenPayload) // check that token is not expired
                && checkAudience(tokenPayload) // check audience
                ; 
        }
        return false;
    }

Once i have had this detailed verification, i was able to see exactly where the mistake was ;一旦我进行了详细的验证,我就能够准确地看到错误在哪里; The front end was sending me invalid CLIENT_ID value.前端向我发送了无效的 CLIENT_ID 值。 [ grumble, grumble ] I have asked them about it about a hundred times and they told me that those values match. [抱怨,抱怨] 我已经问过他们大约一百次了,他们告诉我这些价值观是一致的。 BAH !呸!

So the mistake was not somewhere in my original code for token verification, it was an error in communication in my office.所以这个错误不是在我的原始令牌验证代码中的某个地方,而是我办公室的通信错误。

I will revert back to the original code, with the proper CLIENT_ID this time for the sake of security.为了安全起见,这次我将恢复到原始代码,并使用正确的 CLIENT_ID。 Still, i must make one complaint about the google library - it never told me why token verification was failing.不过,我必须对谷歌图书馆提出一个抱怨——它从来没有告诉我为什么令牌验证失败。 It took me DAYS of pain staking effort to finally figure it out.我花了几天的努力才最终弄明白。 I know they did it out of security, but still, the lack of support is galling.我知道他们这样做是出于安全考虑,但仍然缺乏支持令人痛苦。

Had similar issue.有类似的问题。 It was so stupid, but I just have wrong time on my server.这太愚蠢了,但我只是在我的服务器上有错误的时间。 I think verification always fail because expiration time of token is end in my server time universe.我认为验证总是失败,因为令牌的到期时间在我的服务器时间范围内结束。

For those who still having this issue:对于那些仍然有这个问题的人:

  1. Try python example( https://developers.google.com/identity/sign-in/web/backend-auth#python ) to ensure that you are using right CLIENT_ID, if no, you will see exception message with right CLIENT_ID尝试 python 示例( https://developers.google.com/identity/sign-in/web/backend-auth#python )以确保您使用正确的 CLIENT_ID,如果没有,您将看到带有正确 CLIENT_ID 的异常消息

  2. Use com.google.api-client of version 1.31.3 and com.google.oauth-client of version 1.31.4使用com.google.api-client的版本1.31.3com.google.oauth-client的版本1.31.4

  3. Use com.google.api.client.json.gson.GsonFactory as JsonFactory使用com.google.api.client.json.gson.GsonFactory作为JsonFactory

我通过从客户端发送idToken而不是authToken解决了我的问题

Not sure if anyone is still looking at this, but the code from google GitHub actually solved this for me.不确定是否有人还在看这个,但来自谷歌 GitHub 的代码实际上为我解决了这个问题。 https://github.com/googlearchive/gplus-verifytoken-java/tree/master/src/com/google/plus/samples/verifytoken https://github.com/googlearchive/gplus-verifytoken-java/tree/master/src/com/google/plus/samples/verifytoken

They mention that the project is no longer actively maintained, and remains there as an archive of this work though.他们提到该项目不再得到积极维护,但仍作为这项工作的存档保留在那里。

I found two reasons of why it can happen.我发现了为什么它会发生的两个原因。

  1. There is a CLIENT_ID mismatch between the Spring boot server and the frontend library. Spring 引导服务器和前端库之间存在 CLIENT_ID 不匹配。
  2. The token id which is generated upon Sign in with Google is expired.使用 Google 登录时生成的令牌 ID 已过期。 It generally expires after 1 hour.它通常在 1 小时后到期。

Solution to refresh the auth token:刷新授权令牌的解决方案:

We get a reloadAuthResponse method on the response object upon successful sign in. Also, we get the expires_in property which gives us the number of milliseconds after which the token will expire.成功登录后,我们在响应 object 上获得了 reloadAuthResponse 方法。此外,我们还获得了 expires_in 属性,它为我们提供了令牌过期的毫秒数。 Following is the code snippet to use it.以下是使用它的代码片段。

const handleSuccessfulGoogleResponse = (response) => {
 console.log(response)
 const loginDetails = {
   name: response.profileObj.name,
   email: response.profileObj.email,
   tokenId: response.tokenId
 }
 //Do what is needed with the data
 refreshTokenGenerator(response);
}

const refreshTokenGenerator = res => {
 let refreshTime = (res.tokenObj.expires_in || 3600 - 50 * 60) * 1000;
 const refreshToken = async () => {
    console.log('running at ',new Date(Date.now()).toLocaleTimeString());
    const refreshedResponse = await res.reloadAuthResponse();
    refreshTime = (refreshedResponse.expires_in || 3600 - 50 * 60) * 
   1000;

   const newTokenId = refreshedResponse.id_token;
   //store the newTokenId and use it for the network calls
    setTimeout(refreshToken, refreshTime);
 }
 setTimeout(refreshToken, refreshTime);
}

Here, the handleSuccessfulGoogleResponse() is executed upon a successful sign in. We are calling the refreshTokenGenerator() inside it which uses the expires_in property(if it exists otherwise 3600ms by default).在这里,handleSuccessfulGoogleResponse() 在成功登录后执行。我们在其中调用 refreshTokenGenerator() ,它使用 expires_in 属性(如果存在,则默认为 3600 毫秒)。 We are then using setTimeout() to execute the function to generate the new token before the current one expires.然后我们使用 setTimeout() 执行 function 以在当前令牌到期之前生成新令牌。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM