oauth-2.0google-oauthgrails-4

request to token url returning 400 bad request for google oauth2?


I am trying to implement a simple google login in our web application. Here is the OAuth controller.

import grails.plugin.springsecurity.SpringSecurityService
import grails.plugin.springsecurity.annotation.Secured
import groovy.json.JsonSlurper
import org.springframework.web.client.RestTemplate
import org.springframework.http.*

@Secured('permitAll')
class OAuthLoginController {

    SpringSecurityService springSecurityService

    // Step 1: Redirect to Google OAuth2 authorization endpoint
    def index() {
        def googleAuthUrl = "${grailsApplication.config.google.auth.url}?" +
                "client_id=${grailsApplication.config.google.client.id}&" +
                "redirect_uri=${grailsApplication.config.google.client.redirecturi}&" +
                "response_type=code&" +
                "scope=openid%20profile%20email"

        redirect url: googleAuthUrl
    }

    // Step 2: Handle Google callback and exchange code for access token
    def callback(String code) {
        if (!code) {
            flash.message = "Authorization failed."
            redirect uri: "/login/auth"
            return
        }

        def accessToken = exchangeCodeForAccessToken(code)
        if (!accessToken) {
            flash.message = "Failed to retrieve access token."
            redirect uri: "/login/auth"
            return
        }

        def userInfo = fetchUserInfo(accessToken)
        if (!userInfo?.email) {
            flash.message = "Failed to retrieve user info."
            redirect uri: "/login/auth"
            return
        }

        // Check if user exists or create new user
        def user = User.findByUsername(userInfo.email) ?: createUser(userInfo)
        springSecurityService.reauthenticate(user.username)
        redirect uri: "/home"
    }

    private String exchangeCodeForAccessToken(String code) {
        def restTemplate = new RestTemplate()
        def tokenRequestBody = [
                code         : code,
                client_id    : grailsApplication.config.google.client.id,
                client_secret: grailsApplication.config.google.client.secret,
                redirect_uri : grailsApplication.config.google.client.redirecturi,
                grant_type   : "authorization_code"
        ]

        def requestEntity = new HttpEntity<>(tokenRequestBody, new HttpHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED))
        def response = restTemplate.postForEntity(grailsApplication.config.google.auth.tokenurl, requestEntity, String)
        def responseBody = new JsonSlurper().parseText(response.body)
        return responseBody.access_token
    }

    private Map fetchUserInfo(String accessToken) {
        def headers = new HttpHeaders()
        headers.set("Authorization", "Bearer $accessToken")
        def requestEntity = new HttpEntity<>(headers)
        def restTemplate = new RestTemplate()
        def response = restTemplate.exchange(grailsApplication.config.google.auth.userinfourl, HttpMethod.GET, requestEntity, String)
        return new JsonSlurper().parseText(response.body)
    }

    private User createUser(Map userInfo) {
        def user = new User(username: userInfo.email, password: "", enabled: true).save(flush: true)
        def role = Role.findByAuthority("ROLE_USER") ?: new Role(authority: "ROLE_USER").save(flush: true)
        UserRole.create(user, role, true)
        return user
    }
}

and this is the configuration

google:
    client:
        id: 'valid client id'
        secret: 'valid secret'
        redirecturi: 'https://localhost:8443/roadrace/oauth2/callback/google'
    auth:
        url: 'https://accounts.google.com/o/oauth2/v2/auth'
        tokenurl: 'https://oauth2.googleapis.com/token'
        userinfourl: 'https://www.googleapis.com/oauth2/v3/userinfo'

here is the config in google console

enter image description here

When i run the app it asks for google login.

After i login using google and click continue

in the callback i get error at this line

def response = restTemplate.postForEntity(grailsApplication.config.google.auth.tokenurl, requestEntity, String)

and this is the error i get

2024-11-14 22:23:03.346 ERROR --- [io-8443-exec-10] o.g.web.errors.GrailsExceptionResolver   : BadRequest occurred when processing request: [GET] /roadrace/oauth2/callback/google
400 Bad Request. Stacktrace follows:

org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request
    at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:79)
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:122)
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:102)
    at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:774)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:732)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:666)
    at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:441)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at com.runnercard.OAuthLoginController.exchangeCodeForAccessToken(OAuthLoginController.groovy:64)
    at com.runnercard.OAuthLoginController.callback(OAuthLoginController.groovy:33)
    at org.grails.core.DefaultGrailsControllerClass$MethodHandleInvoker.invoke(DefaultGrailsControllerClass.java:223)
    at org.grails.core.DefaultGrailsControllerClass.invoke(DefaultGrailsControllerClass.java:188)
    at org.grails.web.mapping.mvc.UrlMappingsInfoHandlerAdapter.handle(UrlMappingsInfoHandlerAda

I appreciate any guide to why is sending post request to https://oauth2.googleapis.com/token returning 400 bad request. Isnt this the right way to make the post call?

Thanks for the help.

UPDATE:

On the google console side i have added these things as well

Added test user email

enter image description here

Added scopes

enter image description here

Added both javascript origins and authorized redirect uris

enter image description here

But i still get 400 bad request.

On the app side i dont see anyother thing to add than the code above. I have also waited half hour for configuration to take effect and also changed the redirect urls from localhost to external domains but still no effect.


Solution

  • after doing some test in POSTMAN It turns out the problem was with the way i was doing POST request.

    I changed the implementation from

      private String exchangeCodeForAccessToken(String code) {
            def restTemplate = new RestTemplate()
            def tokenRequestBody = [
                    code         : code,
                    client_id    : grailsApplication.config.google.client.id,
                    client_secret: grailsApplication.config.google.client.secret,
                    redirect_uri : grailsApplication.config.google.client.redirecturi,
                    grant_type   : "authorization_code"
            ]
    
            def requestEntity = new HttpEntity<>(tokenRequestBody, new HttpHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED))
            def response = restTemplate.postForEntity(grailsApplication.config.google.auth.tokenurl, requestEntity, String)
            def responseBody = new JsonSlurper().parseText(response.body)
            return responseBody.access_token
        }
    

    to

    private String exchangeCodeForAccessToken(String code) {
        try {
            // URL of the token endpoint
            URL url = new URL(grailsApplication.config.google.auth.tokenurl)
    
            // Build the request body
            String tokenRequestBody = [
                    "code"         : code,
                    "client_id"    : grailsApplication.config.google.client.id,
                    "client_secret": grailsApplication.config.google.client.secret,
                    "redirect_uri" : grailsApplication.config.google.client.redirecturi,
                    "grant_type"   : "authorization_code"
            ].collect { k, v -> "${URLEncoder.encode(k, 'UTF-8')}=${URLEncoder.encode(v, 'UTF-8')}" }
                    .join('&')
    
            // Open connection
            HttpURLConnection connection = (HttpURLConnection) url.openConnection()
            connection.requestMethod = "POST"
            connection.setDoOutput(true)
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
            connection.setRequestProperty("Accept", "application/json")
    
            // Write the request body
            connection.outputStream.withWriter("UTF-8") { it.write(tokenRequestBody) }
    
            // Read the response
            int responseCode = connection.responseCode
            if (responseCode == HttpURLConnection.HTTP_OK) {
                def responseText = connection.inputStream.text
                def responseBody = new JsonSlurper().parseText(responseText)
                return responseBody.access_token
            } else {
                log.error("Failed to retrieve access token. HTTP response code: ${responseCode}")
                def errorResponse = connection.errorStream?.text
                log.error("Error response: ${errorResponse}")
                return null
            }
        } catch (Exception e) {
            log.error("Error during token exchange: ${e.message}", e)
            return null
        }
    }
    

    and it started working.