Google Authentication with JPA

For our Factsys.be project we have been playing with Google Drive integration. First of all I was a little surprised of how confusing the Google documentation is. A simple example to authenticate agains a Google from a web application is hard to find (actually I didn’t and found out myself). But Google authentication is one thing, what about JPA?

Well, authenticating against Google with the Java API uses OAuth2 under the surface. With OAuth2 you authenticate against a service and receive an authentication code, which is only valid for a short amount of time (in case of Google only for 1 hour at the moment of writing). For a one-time action that is just fine, but in our case we want our users to be able to connect our webapplication Factsys to the user’s Google Drive in order to upload documents to their Drive whenever they need it. So maybe right now, within the one-hour timeframe, but maybe in a week, when that access-token has long expired… That why the OAuth2 specs forsee a refresh-token. The refresh token should be stored somewhere where the application can access it so that if the access-token has expired that we can request a new one, based on the refresh-token. But doing all these checks yourself, and all these calls… That’s an option, or you could use the Google Auth Java library that can do this for you.

@Bean
public GoogleAuthorizationCodeFlow googleAuth() throws IOException {
final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
Resource resource = new ClassPathResource("client_secret.json");
InputStream in = resource.getInputStream();
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(
JSON_FACTORY,
new InputStreamReader(in)
);
Collection<String> scopes = Collections.singleton(
DriveScopes.DRIVE
);

File file = new File("/Users/dirkvranckaert/googleauth");
DataStoreFactory dataStore = new FileDataStoreFactory(file);

return new GoogleAuthorizationCodeFlow.Builder(
new NetHttpTransport(),
JSON_FACTORY,
clientSecrets,
scopes
)
.setAccessType("offline")
.setDataStoreFactory(dataStore)
.build();
}

The example above is the one you will build easily yourself based on the Google documentation that is available. Just on a side-note: for the example here I referenced to a hard-coded local directory, in real life you would not do this, instead you would reference some shared disk somewhere. But you’ll get the idea. Basically this works. After the code above I could easily start the authentication process as follows:

@GetMapping("/{user}/googledrive/setup")
public String getGoogleDriveSetup(@PathVariable String user) throws User user = ...; //Whatever you to do here to get a reference to your local-app user
return "redirect:" + googleAuth.newAuthorizationUrl().setRedirectUri(http://localhost:8080/googledrive/auth).setState(user.getEmail()).build();
}

The code above wil then redirect the user to the Google authentication screen (with profile selection if necessary or the login screen + the approval for your app). On another side note: you need to have setup a Google project in the console first, that is where you will have to register the allowed redirect URLs and where you must have found the client_secret.json file.

After redirecting the user to Google, he will be redirected back to us and we’ll do something like this:

TokenResponse tokenResponse = googleAuth.newTokenRequest(code)
.setRedirectUri(endpointService.getGoogleAuthRedirectUrl())
.execute();
googleAuth.createAndStoreCredential(tokenResponse, email);

Easy Peasy this is… And if you check your filesystem then a file will be created in your specified directory. But one thing is bothering me. The file!

I don’t want to mess around with files. The project setup is something like this (simplified):

  • Spring Boot (with Web, Security and Thymeleaf)
  • JPA 2.0
  • Hibernate
  • PostgreSQL

So I do have a database layer in which all of my complex domain entities are persisted. Why not persisting Google Auth data in there instead of using a file?! Well that’s an option… But not right out of the box… Let’s dive into some more coding…

According to the docs you need to do two things: implement your own DataStore and implement your own DataStoreFactory for the generic type StoredCredential. But as we want to persist to our database we would also need a JPA Repository/DAO and the entity that will persisted with the JPA repository. And last but not least off course the database table.
Let’s start with the database prepping. We are using PostgreSQL so you might want to tweak the SQL a little for your SQL implementation that you are using, but the creation of our google_credentials table looks like this:

create table google_credentials (
 key varchar(500) not null constraint google_jpa_data_store_credential_pkey
 primary key,
 access_token varchar(500) null,
 expiration_time_milliseconds bigint null,
 refresh_token varchar(500),
 created_at timestamp default now() not null,
 updated_at timestamp default now() not null
);

The domain model that belongs to this table:

@Entity(name = "google_credentials")
@EntityListeners(AuditingEntityListener.class)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GoogleCredential {
@Id
private String key;
private String accessToken;
private Long expirationTimeMilliseconds;
private String refreshToken;

@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;

public GoogleCredential(String key, StoredCredential credential) {
this.key = key;
this.accessToken = credential.getAccessToken();
this.expirationTimeMilliseconds = credential.getExpirationTimeMilliseconds();
this.refreshToken = credential.getRefreshToken();
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
}

public void apply(StoredCredential credential) {
this.accessToken = credential.getAccessToken();
this.expirationTimeMilliseconds = credential.getExpirationTimeMilliseconds();
this.refreshToken = credential.getRefreshToken();
this.updatedAt = Instant.now();
}
}

The repository is quite simple and only has a few extra lookup methods:

public interface GoogleCredentialRepository extends JpaRepository<GoogleCredential, String> {
Optional<GoogleCredential> findByKey(String key);
Optional<GoogleCredential> findByAccessToken(String key);
@Query(value = "select key from google_jpa_data_store_credential", nativeQuery = true)
Set<String> findAllKeys();
}

The real work will be done in the DataStore implementation, the DataStoreFactory (JPADataStoreFactory) will take in the JPA repository in order to pass on it on to the DataStore whenever Google will need the DataStore.

private GoogleCredentialRepository repository;

public JPADataStoreFactory(GoogleCredentialRepository repository) {
this.repository = repository;
}

@Override
protected JPADataStore createDataStore(String id) throws IOException {
return new JPADataStore(this, id, repository);
}

Our JPADataStore will have to handle the StoredCredentials and translate these to and from GoogleCredentials. This class is really easy, it contains a count, an empty, a clear/delete-all, single delete, a getter and setter method. The setter method is most special one in here, because the api’s never know if the credential is already know (thus an update on DB level) or new (meaning an insert on database level) we’ll do the check ourself based on the key.

public class JPADataStore extends AbstractDataStore<StoredCredential> {
private GoogleCredentialRepository repository;
private JPADataStoreFactory jpaDataStoreFactory;

/**
*
@param dataStoreFactory data store factory
*
@param id data store ID
*/
protected JPADataStore(JPADataStoreFactory dataStoreFactory, String id, GoogleCredentialRepository repository) {
super(dataStoreFactory, id);
this.repository = repository;
}

@Override
public JPADataStoreFactory getDataStoreFactory() {
return jpaDataStoreFactory;
}

@Override
public int size() throws IOException {
return (int) repository.count();
}

@Override
public boolean isEmpty() throws IOException {
return size() == 0;
}

@Override
public boolean containsKey(String key) throws IOException {
return repository.findByKey(key).isPresent();
}

@Override
public boolean containsValue(StoredCredential value) throws IOException {
return repository.findByAccessToken(value.getAccessToken()).isPresent();
}

@Override
public Set<String> keySet() throws IOException {
return repository.findAllKeys();
}

@Override
public Collection<StoredCredential> values() throws IOException {
return repository.findAll().stream().map(c -> {
StoredCredential credential = new StoredCredential();
credential.setAccessToken(c.getAccessToken());
credential.setRefreshToken(c.getRefreshToken());
credential.setExpirationTimeMilliseconds(c.getExpirationTimeMilliseconds());
return credential;
}).collect(Collectors.toList());
}

@Override
public StoredCredential get(String key) throws IOException {
Optional<GoogleCredential> jpaStoredCredentialOptional = repository.findByKey(key);
if (!jpaStoredCredentialOptional.isPresent()) {
return null;
}
GoogleCredential googleCredential = jpaStoredCredentialOptional.get();
StoredCredential credential = new StoredCredential();
credential.setAccessToken(googleCredential.getAccessToken());
credential.setRefreshToken(googleCredential.getRefreshToken());
credential.setExpirationTimeMilliseconds(googleCredential.getExpirationTimeMilliseconds());
return credential;
}

@Override
public DataStore<StoredCredential> set(String key, StoredCredential value) throws IOException {
GoogleCredential googleCredential = repository.findByKey(key).orElse(new GoogleCredential(key, value));
googleCredential.apply(value);
repository.save(googleCredential);
return this;
}

@Override
public DataStore<StoredCredential> clear() throws IOException {
repository.deleteAll();
return this;
}

@Override
public DataStore<StoredCredential> delete(String key) throws IOException {
repository.delete(key);
return this;
}
}

Now that you have all this code in place there only one more thing to do: initialise the JPADataStoreFactory. That’s only a one-liner when constructing the GoogleAuthorizationCodeFlow:

DataStoreFactory dataStore = new JPADataStoreFactory(repository);

Build it, deploy it, try it! It works like a charm. Make sure authentication succeeds, check the database for a record for your user and start doing some Google Drive (or other Google service) calls. Wait for at least one hour and the again, you’ll see an update in the database with an updated access-token that the library has generated based on the refresh-token.
Just for the record, these are the libraries I used at the time of writing:

compile 'com.google.api-client:google-api-client:1.23.0'
compile 'com.google.apis:google-api-services-drive:v3-rev103-1.23.0'

Happy Coder!