GoogleOAuthService.java

package com.soen390.backend.service;

import com.soen390.backend.object.GoogleTokenSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;

import java.time.Instant;
import java.util.Map;
import java.util.UUID;

@Service
public class GoogleOAuthService {

  private final RestTemplate restTemplate;
  private final GoogleSessionService sessionService;

  @Value("${google.oauth.client-id}")
  private String clientId;

  @Value("${google.oauth.client-secret}")
  private String clientSecret;

  @Value("${google.oauth.redirect-uri}")
  private String redirectUri;

  public GoogleOAuthService(RestTemplate restTemplate, GoogleSessionService sessionService) {
    this.restTemplate = restTemplate;
    this.sessionService = sessionService;
  }

  public String exchangeServerAuthCode(String serverAuthCode) {
    String tokenUrl = "https://oauth2.googleapis.com/token";

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
    form.add("code", serverAuthCode);
    form.add("client_id", clientId);
    form.add("client_secret", clientSecret);

    // Keep this present for authorization_code exchanges
    form.add("redirect_uri", redirectUri);

    form.add("grant_type", "authorization_code");

    HttpEntity<MultiValueMap<String, String>> req = new HttpEntity<>(form, headers);

    try {
      ResponseEntity<Map<String, Object>> res = restTemplate.exchange(tokenUrl, HttpMethod.POST, req, new ParameterizedTypeReference<Map<String, Object>>() {});

      if (!res.getStatusCode().is2xxSuccessful() || res.getBody() == null) {
        throw new IllegalStateException("Google token exchange failed (empty response).");
      }

      Map<String, Object> body = res.getBody();
      String accessToken = (String) body.get("access_token");
      String refreshToken = (String) body.get("refresh_token"); // may be null
      Number expiresIn = (Number) body.get("expires_in");       // seconds

      if (accessToken == null || accessToken.isBlank()) {
        throw new IllegalStateException("Google token exchange failed: missing access_token.");
      }

      Instant expiresAt = Instant.now().plusSeconds(expiresIn != null ? expiresIn.longValue() : 3600);

      String sessionId = UUID.randomUUID().toString();
      sessionService.put(sessionId, new GoogleTokenSession(accessToken, refreshToken, expiresAt));
      return sessionId;

    } catch (HttpStatusCodeException e) {
      String googleBody = e.getResponseBodyAsString();
      throw new IllegalStateException("Google token exchange failed: " + googleBody, e);
    }
  }
}