When it comes to Gmail integration, you can make your users' lives easier by auto-refreshing their OAuth2 tokens. But to do that, you'll need to set up a DataStore implementation.

And that's exactly what I'll show you how to do in this part of the tutorial on integrating Gmail with Angular and Spring Boot.

If you want to read other articles in the series, feel free to visit the gmail integration tag. 

And if you want to just browse through some code, take a look at what I've got on GitHub.

Otherwise, hang around here for a bit.

The Story So Far

Up to this point, you've done the following:

  • Created an OAuth2 client ID and secret on Google Cloud Console
  • Enabled the Gmail API via Google Cloud Console
  • Set the necessary properties in your Spring Boot application.properties file 

If you haven't done any of that, then I suggest you go to Part 1 of this series and take care of those action items. Otherwise, you won't make much progress going forward.

Goals

So what are you going to learn in this guide? A few of things:

  • How to set up a DataStore implementation
  • Why you need to set up a DataStore implementation
  • How to set up a refresh listener
  • Why you need to set up a refresh listener

And then you can get a beer.

Shopping at the Data Store

And now we need to talk about the DataStore. But first: let me explain its purpose.

When your user grabs an access token via OAuth2, that token won't last forever.

It's only good for about an hour. After that, the user will get an error.

Well, that sucks.

But it doesn't have to suck. You see, the API can handle the task of refreshing the token.

You'll be shocked to learn that it handles the process of refreshing the token with the aid of something called a refresh token.

BUT... if you want your application to use that auto-refresh feature, you'll need to persist the credentials. 

And guess what you use to do that? The DataStore implementation.

So let's take a look at that DataStore implementation right now. It's in a class called GoogleApiDataStore.

public class GoogleApiDataStore<V extends Serializable> extends AbstractDataStore<V> {

	private UserRepository userRepository;
	
	private AuthorizationUtil authorizationUtil;
	
	private final Lock lock = new ReentrantLock();

	
	protected GoogleApiDataStore(DataStoreFactory dataStoreFactory, String id, UserRepository userRepository, AuthorizationUtil authorizationUtil) {
		super(dataStoreFactory, id);
		this.userRepository = userRepository;
		this.authorizationUtil = authorizationUtil;
	}

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

	
	@Override
	public Collection<V> values() throws IOException {
		return null;
	}

	
	@Override
	public V get(String key) throws IOException {
		if (key != null) {
			lock.lock();
			
			try {
				User user = authorizationUtil.getCurrentUser();
				
				if (user.getId().equals(key)) {
					GoogleApi googleApi = user.getGoogleApi();
					
					if (googleApi != null && googleApi.getStoredCredential() != null) {
						return IOUtils.deserialize(googleApi.getStoredCredential());
					}
				}			
			} finally {
				lock.unlock();
			}
		}
		
		return null;
	}

	
	@Override
	public DataStore<V> set(String key, V value) throws IOException {		
		if (key != null) {
			lock.lock();
			
			try {
				User user = authorizationUtil.getCurrentUser();
				
				if (user.getId().equals(key)) {				
					GoogleApi googleApi = user.getGoogleApi();
					if (googleApi == null ) googleApi = new GoogleApi();
					
					googleApi.setStoredCredential(IOUtils.serialize(value));
					
					user.setGoogleApi(googleApi);
					
					userRepository.save(user);
				} 		
			} finally {
				lock.unlock();
			}
		}
		
		return this;
	}

	
	@Override
	public DataStore<V> clear() throws IOException {
		return null;
	}

	
	@Override
	public DataStore<V> delete(String key) throws IOException {
		return null;
	}
}

First of all, look at what's not going on in that class. It's not Spring componentized.

Instead, it passes in already Spring-managed objects like UserRepository and AuthorizationUtil

Why isn't it Spring-managed? Because the Spring framework isn't the beast that instantiates this class.

Nope. That's gotta be handled with a DataStoreFactory object that I'll show you in a moment. That one, by the way, is Spring-managed.

Now if you look at the code above from a high-level perspective (try standing on a chair) you'll see that it basically acts as a Map.

As in: it manages key-value pairs. Take a look at the get() and set() methods and you'll see what I'm talking about.

The big difference between this class and a Map is that when you set() something, you're persisting that value.

In this case, the serialized credentials get stored in the MongoDB user document as the googleApi property.

That GoogleApi class you see referenced above is ridiculously simple. It include only one property: a byte array called storedCredential. Unsurprisingly, that's the credential that gets persisted in the user document.

And the whole point of persisting that credential, once more, is so that the application can just grab the persisted credential and use it to access Gmail.

That credential, by the way, includes both the access token and the refresh token. Take a look at the StoredCredential class for more info about that.

Now let's take a look at the DataStoreFactory instance that creates the GoogleApiDataStore object.

@Component
public class GoogleApiDataStoreFactory extends AbstractDataStoreFactory {

	@Autowired
	private UserRepository userRepository;
	
	@Autowired
	private AuthorizationUtil authorizationUtil;
	

	@Override
	protected <V extends Serializable> DataStore<V> createDataStore(String id) throws IOException {
		return new GoogleApiDataStore<V>(this, id, userRepository, authorizationUtil);
	}
}

Not much going on there. 

The class extends AbstractDataStoreFactory. That's the base class in the Google OAuth2 API for creating a DataStore instance.

But other than that, all it does is autowire a couple of Spring-managed objects and overrides createDataStore() so it creates the DataStore instance that you just looked at.

But where does that method get called from?

A utility class.

Your OAuth2 Utility Belt

Now take a look at GoogleOauthUtil. I'll cover it in chunks.

	@Value("${google.api.oauth2.token.url}") String tokenUrl;
	@Value("${google.api.oauth2.auth.url}") String authUrl;
	@Value("${google.api.oauth2.client.id}") String clientId;
	@Value("${google.api.oauth2.client.secret}") String clientSecret;

Those properties each get retrieved from the application.properties file. You already set the values in that file (in Part 1) so you should be good to go.

	private DataStore<StoredCredential> dataStore;
	
	@Autowired
	public GoogleOauthUtil(GoogleApiDataStoreFactory dataStoreFactory) {
		setupDataStore(dataStoreFactory);	
	}

	
	private void setupDataStore(DataStoreFactory dataStoreFactory) {
		try {
			dataStore = dataStoreFactory.getDataStore(Constants.CREDENTIAL_STORE_ID);
		} catch (Exception e) {
			LOG.error("Problem creating data store for credentials!", e);
		}
	}

That chunk deals with getting the DataStore instance up and running. The class gets instantiated by Spring (note the @Autowired annotation above the constructor) and sets up the DataStore right away.

Now this part my catch you off guard:

	private List<CredentialRefreshListener> getListeners(String userId) {
		DataStoreCredentialRefreshListener listener = new DataStoreCredentialRefreshListener(userId, dataStore);
		List<CredentialRefreshListener> listeners = new ArrayList<CredentialRefreshListener>();
		listeners.add(listener);
		
		return listeners;
	}

I mentioned above that you'll need to persist the credentials if you want the Google API to handle the process of refreshing the token. And that's true.

But that's not all you need to do.

You also need to set up a listener that stores the refresh token response in the DataStore instance.

That's what the code above does. It creates a List of a single listener: DataStoreCredentialsRefreshListener.

Armed with that single listener, you're ready to let Google do all the hard work when it comes to refreshing tokens.

Wrapping It Up

There you have it. Part 2 is in the books.

And, I'm sorry to say, there's still a long way to go.

Hang with me. You'll be glad you did.

Oh, and have fun!

Photo by Ivan Samkov from Pexels