One of the dark corners of the OAuth experience is refresh tokens. Every time I play with OAuth based authentication, refresh bites me. Assume some level of hand waving and simplification in what follows as I’m not trying to be perfectly detailed about OAuth.
OAuth and Refresh Tokens
The way OAuth2 generally works is that after a successful authentication two tokens are generated, an access token and a refresh token. The access token is used to access a service. For example, to make a REST call the developer would include the access token in the header. Access tokens have an intentionally short lifetime, usually an hour or so. That means that after an hour if you present that token to a service it will reject the request. That way if the access token is stolen there is a reasonable limit on the amount of damage that can be done.
Refresh tokens have a much longer lifetime and allow the holder to get a new access token when needed. As long as the refresh token is still valid the user doesn’t have to reauthenticate, from their point of view they get uninterrupted access to the service they want. Unfortunately, the user point of view is not the same as the developer point of view, and developers do have to think about this lifecycle, sometimes.
It’s Automatic!
Don’t worry, almost all of this is handled for you transparently and automatically if you’re using a decent OAuth client SDK. I’ve been using the google_sign_in Flutter plugin and it does a good job on all of that. You call the GoogleSignInAccount.authHeaders
method and it returns the headers you need with an up to date access token and all is good in the world. If the current access token has expired and needs to be updated then it will happen behind the scenes and you’ll get a fresh one that you can use.
GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
],
);
GoogleSignInAccount _currentUser;
// In a real application signInSilently should be tried first
// so that users that have already authenticated don't have to
// reauthenticate each time.
_currentUser = await _googleSignIn.signIn();
if ( _currentUser == null ) {
return;
}
// This line is the key. The headers returned contain
// a valid token even if a refresh was required when
// called.
var headers = await _currentUser.authHeaders;
Until It Isn’t
Like all free things there is a catch with the transparent and automatic goodness. If you get the access token using the GoogleSignInAccount.authHeaders
method and then save it somewhere until later, it may have expired by the time that you use it. Given that Flutter apps tend to be pretty reactive by nature you can’t always predict when an object you’ve created will actually be used. For example, if you’re depending on user interaction to trigger a next step.
I ran into this problem recently by creating a NetworkImage
and then using it fairly durably in the UI. The network image took the request headers as a parameter to its constructor. Depending on whether the image was displayed immediately or later, after the access token had expired, there would be a 403 exception trying to retrieve the image. To further complicate diagnosing the issue, usually the image was served from cache after the first load, so it would work for a long time.
In this particular case the fix was to create a new ImageProvider
implementation that dynamically retrieves the auth headers when it is trying to load the image from the URL. That way it always has a valid access token. More generally don’t store the returned auth headers, always get them dynamically from the source to ensure they’re not expired.
The moral is to always be at least somewhat aware of how the underlying protocols work.