I built a Spring Boot service that connects to Google Sheets using their Java API. Everything works fine at first, but after a few hours the connection stops working and I get a 401 error.
The issue is that my OAuth token expires and I have to go through the browser login process again. Is there a way to make the token refresh automatically so users don’t have to keep logging in?
Here’s my service code:
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 SheetsConnectorService implements SheetsConnector {
private static final Log LOG = LogFactory.getLog(SheetsConnectorService.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/sheets-api-data");
private FileDataStoreFactory storeFactory;
private JsonFactory jsonHandler;
private HttpTransport transport;
private List<String> permissions;
private Sheets sheetClient;
public SheetsConnectorService() throws Exception {
try {
this.jsonHandler = 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 connector: " + e.getMessage());
throw e;
}
Credential creds = this.getCredentials();
this.sheetClient = new Sheets.Builder(this.transport, this.jsonHandler, creds)
.setApplicationName(this.appName).build();
}
public void writeDataToSheet(String docId, String cellRange, ValueRange data) throws IOException {
Sheets.Spreadsheets.Values.Append writeRequest = sheetClient.spreadsheets().values()
.append(docId, cellRange, data).setValueInputOption("USER_ENTERED");
AppendValuesResponse result = writeRequest.execute();
}
private Credential getCredentials() throws IOException {
InputStream secretStream = SheetsConnectorService.class.getResourceAsStream("/config/client_credentials.json");
GoogleClientSecrets secrets = GoogleClientSecrets.load(this.jsonHandler, new InputStreamReader(secretStream));
GoogleAuthorizationCodeFlow authFlow = new GoogleAuthorizationCodeFlow.Builder(this.transport, this.jsonHandler,
secrets, this.permissions).setDataStoreFactory(this.storeFactory).setAccessType("online").build();
LocalServerReceiver.Builder receiverBuilder = new LocalServerReceiver.Builder();
receiverBuilder.setHost("localhost");
receiverBuilder.setPort(8888);
Credential userCred = new AuthorizationCodeInstalledApp(authFlow, receiverBuilder.build())
.authorize("current_user");
LOG.info("Credentials stored at " + CREDENTIAL_DIR.getAbsolutePath());
return userCred;
}
}
UPDATE: I fixed it by changing the authorization flow setup like this:
GoogleAuthorizationCodeFlow authFlow = new GoogleAuthorizationCodeFlow.Builder(this.transport, this.jsonHandler,
secrets, this.permissions).setDataStoreFactory(this.storeFactory).setAccessType("offline")
.setApprovalPrompt("force")
.addRefreshListener(
new DataStoreCredentialRefreshListener(userId, this.storeFactory))
.build();