mattbdean/JRAW

Refreshing token for userless oauth

saket opened this issue · 7 comments

saket commented

I don't know if I understand this correctly, but it seems that when using userless oauth, access tokens do not get refreshed after they expire, resulting in 401 errors. I went through RedditClient#request(), but it only renews token if AuthManager#canRenew() returns true. And that only happens when a refresh token is present, which is not the case with userless credentials.

Am I missing something here?

Update: I just saw that AuthManager#canRenew() is supposed to return true when using userless auth. I will investigate why I'm still seeing this error and report back.

Can you provide code that can reproduce this?

saket commented

Sorry for the late update. I was trying to investigate this more, but I haven't found anything useful. I think this problem exists for both userless and logged-in auth modes. The refresh token becomes null occasionally, resulting in 401s.

When my app starts, my JRAW wrapper is setup this way:

accountHelper.onSwitch { newRedditClient -> clientSubject.onNext(newRedditClient) }

when {
  tokenStore.usernames.isNotEmpty() -> accountHelper.switchToUser(tokenStore.usernames.first())
  else -> accountHelper.switchToUserless()
}

Here's the way I use StatefulAuthHelper:

class UserLoginHelper(private val helper: StatefulAuthHelper) {

  fun authorizationUrl(): String {
    val scopes = arrayOf(…)
    return helper.getAuthorizationUrl(requestRefreshToken = true, useMobileSite = true, scopes = *scopes)
  }

  fun parseOAuthSuccessUrl(successUrl: String) {
    helper.onUserChallenge(successUrl)
  }
}

I also have an OkHttp interceptor that pro-actively refreshes tokens instead of waiting for 60m to complete.

override fun intercept2(chain: Interceptor.Chain): Response {
  if (chain.request().url().toString().startsWith("https://www.reddit.com/api/v1/access_token")) {
    return chain.proceed(chain.request())
  }

  if (client.authManager.canRenew()) {
    val latestOAuthData: OAuthData = client.authManager.current!!
    val expirationDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(latestOAuthData.expiration.time), UTC)
    val recommendedRefreshDate = expirationDate.minusMinutes(10)
    val now = LocalDateTime.now(UTC)

    if (now > recommendedRefreshDate && now < expirationDate) {
      refreshTokenAheadOfTime()
    }
  }

  return chain.proceed(chain.request())
}

Do you see anything strikingly glaring in these gists? I'm trying but finding it difficult to reproduce this bug reliably, especially because I have to wait for 60m to complete.

Are you using a SharedPreferencesTokenStore or another subclass of DeferredPersistentTokenStore? If so you might have forgotten to load() the tokens

saket commented

No, I'm using a SharedPreferencesTokenStore:

val store = SharedPreferencesTokenStore(appContext)
store.load()
store.autoPersist = true
saket commented

I think I've figured out the problem with logged in users. I was getting the first username stored in a token store, which is wrong. It's usually <userless> that is stored in the beginning before the logged in user's name.

For now, I'll store the logged-in username elsewhere and provide it to AccountHelper#switchToUser() instead of finding it inside TokenStore#usernames.

I'll also close this issue until I'm able to reproduce why access tokens aren't getting refreshed for userless apps.

saket commented

Re-opening this issue. I'm still seeing this problem. After some limited research, I think that the refresh token in OAuthData is somehow getting stored as null. If I read the stored json for PersistedAuthData, I see this:

{
  "latest": {
    "accessToken": "XXX",
    "scopes": [
      "account",
      "edit",
      "history",
      "identity",
      "mysubreddits",
      "privatemessages",
      "read",
      "report",
      "save",
      "submit",
      "subscribe",
      "vote",
      "wikiread"
    ],
    "expiration": 1528580965149
  },
  "refreshToken": "YYY"
}

Any ideas what might be happening?

I'm thinking of reading PersistedAuthData#getRefreshToken() in the meanwhile in case OAuthData#getRefreshToken() is null.

DeferredPersistentTokenStore won't write non-significant PersistedAuthData objects. Maybe there's some faulty logic in there?