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 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.