I built a Spring Boot service that writes data to Google Sheets using their Java API. Everything works fine initially, but I keep running into an issue where the OAuth token stops working after a few hours and I get 401 unauthorized errors.
The problem is that I have to manually authenticate through the browser again every time this happens. Is there a way to make the token refresh automatically so users don’t have to keep logging in?
Here’s my implementation:
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.SheetsScopes;
@Service
public class SheetsIntegrationService {
private static final Logger log = LoggerFactory.getLogger(SheetsIntegrationService.class);
@Value("${sheets.app-name}")
private String appName;
private static final java.io.File CREDENTIAL_DIR = new java.io.File(System.getProperty("user.home"),
".tokens/google-sheets-api");
private FileDataStoreFactory storeFactory;
private JsonFactory factory;
private HttpTransport transport;
private List<String> permissions;
private Sheets apiService;
public SheetsIntegrationService() throws Exception {
try {
this.factory = JacksonFactory.getDefaultInstance();
this.permissions = Arrays.asList(SheetsScopes.SPREADSHEETS);
this.transport = GoogleNetHttpTransport.newTrustedTransport();
this.storeFactory = new FileDataStoreFactory(CREDENTIAL_DIR);
} catch (Exception e) {
log.error("Failed to initialize Sheets service: " + e.getMessage());
throw e;
}
Credential auth = this.getCredentials();
this.apiService = new Sheets.Builder(this.transport, this.factory, auth)
.setApplicationName(this.appName).build();
}
public void writeDataToSheet(String sheetId, String cellRange, ValueRange data) throws IOException {
Sheets.Spreadsheets.Values.Append writeRequest = apiService.spreadsheets().values()
.append(sheetId, cellRange, data).setValueInputOption("USER_ENTERED");
AppendValuesResponse result = writeRequest.execute();
}
private Credential getCredentials() throws IOException {
InputStream secretStream = SheetsIntegrationService.class.getResourceAsStream("/config/client_credentials.json");
GoogleClientSecrets secrets = GoogleClientSecrets.load(this.factory, new InputStreamReader(secretStream));
GoogleAuthorizationCodeFlow authFlow = new GoogleAuthorizationCodeFlow.Builder(
this.transport, this.factory, secrets, this.permissions)
.setDataStoreFactory(this.storeFactory)
.setAccessType("online")
.build();
LocalServerReceiver receiver = new LocalServerReceiver.Builder()
.setHost("localhost")
.setPort(8888)
.build();
Credential auth = new AuthorizationCodeInstalledApp(authFlow, receiver).authorize("default-user");
log.info("Auth tokens stored in " + CREDENTIAL_DIR.getAbsolutePath());
return auth;
}
}
Update: Fixed it by changing the auth flow configuration:
GoogleAuthorizationCodeFlow authFlow = new GoogleAuthorizationCodeFlow.Builder(
this.transport, this.factory, secrets, this.permissions)
.setDataStoreFactory(this.storeFactory)
.setAccessType("offline")
.setApprovalPrompt("force")
.addRefreshListener(new DataStoreCredentialRefreshListener(userId, this.storeFactory))
.build();