Search code examples
javagoogle-apigoogle-oauthgoogle-appsgoogle-api-java-client

Spring : Google authentication redirect_uri_mismatch and URL wont open on browser


I am working on a Spring-MVC application running on tomcat in which I would like to use Google drive functionality. I tried with a service account on my local machine and I had no problems. But when I uploaded the code on server, the browser URL wont be opened. Then I thought, I should not use a service account, I should use a normal web-application account. Now when I do that, I get a redirect_uri_mismatch.

I don't understand one thing, I am setting the redirect URL in flow, in the JSON, why on earth is it getting the redirect_url with random port numbers. If I change the port number in the browser URL, it works fine. But still on server it wont open the browser url, I can see it in tomcat logs, but the damn thing does not open the URL.

Here are my redirect URL from Google app :

http://localhost/authorizeuser
http://localhost:8080/
http://localhost:8080
http://localhost
http://localhost:8080/Callback
https://testserver.net/Callback
http://testserver.net/Callback
http://127.0.0.1

Here is my client_secret.json :

{"web": {
    "client_id": "clientid",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://accounts.google.com/o/oauth2/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_email": "clientemailstuff",
    "client_x509_cert_url": "certurlstuff",
    "client_secret": "itsasecret",
    "redirect_uris": ["http://localhost:8080/","http://localhost:8080"],
    "javascript_origins": ["https://testserver.net", "http://testserver.net","http://localhost:8080"]
}}

And here is the code where I am trying to authenticate :

 @Override
    public Credential authorize() throws IOException {
        InputStream in =
                DriveQuickstartImpl.class.getResourceAsStream("/client_secret.json");
        GoogleClientSecrets clientSecrets =
                GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));

        GoogleAuthorizationCodeFlow flow =
                new GoogleAuthorizationCodeFlow.Builder(
                        HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
                        .setDataStoreFactory(DATA_STORE_FACTORY)
                        .setAccessType("offline")
                        .build();
        flow.newAuthorizationUrl().setState("xyz").setRedirectUri("http://localhost:8080/Callback");
        Credential credential = new AuthorizationCodeInstalledApp(
                flow, new LocalServerReceiver()).authorize("user");

        if(credential!=null && credential.getRefreshToken() != null){
            storeCredentials(credential);
        }
        return credential;
    }

This is majorly pissing me off as I am setting the redirect url, and it is just being ignored and why on earth a browser tab wont be opened when application is deployed on server.

Update Spring problem also fixed, the below code can be used for GoogleDrive authorization on a server with tomcat or others.

@Service
@Transactional
public class GoogleAuthorization{


    @Autowired
    private DriveQuickstart driveQuickstart;

    private static final String APPLICATION_NAME ="APPNAME";

    private static final java.io.File DATA_STORE_DIR = new java.io.File(
            "/home/deploy/store");

    private static FileDataStoreFactory DATA_STORE_FACTORY;

    private static final JsonFactory JSON_FACTORY =
            JacksonFactory.getDefaultInstance();

    private static HttpTransport HTTP_TRANSPORT;

    private static final List<String> SCOPES =
            Arrays.asList(DriveScopes.DRIVE);

    private static final String clientid = "clientid";
    private static final String clientsecret = "clientsecret";

    private static final String CALLBACK_URI = "http://localhost:8080/getgooglelogin";

    private String stateToken;

    private final GoogleAuthorizationCodeFlow flow;

    public GoogleAuthorization(){
        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
            DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR);

        } catch (GeneralSecurityException | IOException e) {
            e.printStackTrace();
        }

        flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT,
                JSON_FACTORY, clientid, clientsecret, SCOPES).setAccessType("offline").setApprovalPrompt("force").build();
        generateStateToken();

    }



    /**
     * Builds a login URL based on client ID, secret, callback URI, and scope
     */
    public String buildLoginUrl() {

        final GoogleAuthorizationCodeRequestUrl url = flow.newAuthorizationUrl();

        return url.setRedirectUri(CALLBACK_URI).setState(stateToken).build();
    }

    /**
     * Generates a secure state token
     */
    private void generateStateToken(){
        SecureRandom sr1 = new SecureRandom();
        stateToken = "google;"+sr1.nextInt();
    }

    /**s
     * Accessor for state token
     */
    public String getStateToken(){
        return stateToken;
    }

    /**
     * Expects an Authentication Code, and makes an authenticated request for the user's profile information
     * * @param authCode authentication code provided by google
     */
    public void saveCredentials(final String authCode) throws IOException {

        GoogleTokenResponse response = flow.newTokenRequest(authCode).setRedirectUri(CALLBACK_URI).execute();
        Credential credential = flow.createAndStoreCredential(response, null);
        System.out.println(" Credential access token is "+credential.getAccessToken());
        System.out.println("Credential refresh token is "+credential.getRefreshToken());
// The line below gives me a NPE.
        this.driveQuickstart.storeCredentials(credential);
    }
}

Controller method :

  @RequestMapping(value = "/getgooglelogin")
    public String getGoogleLogin(HttpServletRequest request, HttpServletResponse response, HttpSession session,Model model) {
// Below guy should be autowired if you want to use Spring. 
        GoogleAuthorization helper = new GoogleAuthorization();

        if (request.getParameter("code") == null
                || request.getParameter("state") == null) {

            model.addAttribute("URL", helper.buildLoginUrl());
            session.setAttribute("state", helper.getStateToken());

        } else if (request.getParameter("code") != null && request.getParameter("state") != null && request.getParameter("state").equals(session.getAttribute("state"))) {
            session.removeAttribute("state");

            try {
                helper.saveCredentials(request.getParameter("code"));
                return "redirect:/dashboard";
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return "newjsp";
    }

newjsp just has a button to click on the URL.


Solution

  • Specifically, you're getting random ports because you are using LocalServerReceiver, which starts up a jetty instance on a free port in order to receive an auth code.

    At a higher level, it looks like you are developing a web server application, but you are trying to use Google OAuth as if it were an installed application. If you are indeed making a web server application, you should be using your server's host name instead of localhost in your callback URL, providing a link for the end user to authenticate using flow.newAuthorizationUrl(), and have your callback fetch the token using flow.newTokenRequest(String). Also make sure that the Client ID you created in your console is of type Web application, or you'll get redirect_uri_mismatch errors. A full working example of how to do this can be found here.