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
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
Added both javascript origins and authorized redirect uris
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.
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.