Skip to content

Commit

Permalink
Support Token Expiry properly (#878)
Browse files Browse the repository at this point in the history
* Support token expiry properly

* Small fix

* Lint

* Use newer fixes for expiry

* Lint
  • Loading branch information
Syer10 authored Feb 19, 2024
1 parent 6803ac0 commit 07e0110
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ class TrackerScoresDataLoader : KotlinDataLoader<Int, List<String>> {
}
}

class TrackerTokenExpiredDataLoader : KotlinDataLoader<Int, Boolean> {
override val dataLoaderName = "TrackerTokenExpiredDataLoader"

override fun getDataLoader(): DataLoader<Int, Boolean> =
DataLoaderFactory.newDataLoader { ids ->
future {
ids.map { id ->
TrackerManager.getTracker(id)?.getIfAuthExpired()
}
}
}
}

class TrackRecordsForMangaIdDataLoader : KotlinDataLoader<Int, TrackRecordNodeList> {
override val dataLoaderName = "TrackRecordsForMangaIdDataLoader"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.TrackRecordsForTrackerIdDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.TrackerDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.TrackerScoresDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.TrackerStatusesDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.TrackerTokenExpiredDataLoader
import suwayomi.tachidesk.graphql.dataLoaders.UnreadChapterCountForMangaDataLoader

class TachideskDataLoaderRegistryFactory {
Expand Down Expand Up @@ -71,6 +72,7 @@ class TachideskDataLoaderRegistryFactory {
TrackerDataLoader(),
TrackerStatusesDataLoader(),
TrackerScoresDataLoader(),
TrackerTokenExpiredDataLoader(),
TrackRecordsForMangaIdDataLoader(),
DisplayScoreForTrackRecordDataLoader(),
TrackRecordsForTrackerIdDataLoader(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class TrackerType(
fun trackRecords(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<TrackRecordNodeList> {
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordNodeList>("TrackRecordsForTrackerIdDataLoader", id)
}

fun isTokenExpired(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<Boolean> {
return dataFetchingEnvironment.getValueFromDataLoader<Int, Boolean>("TrackerTokenExpiredDataLoader", id)
}
}

class TrackStatusType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import okhttp3.OkHttpClient
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
import uy.kohesive.injekt.injectLazy
import java.io.IOException

abstract class Tracker(val id: Int, val name: String) {
val trackPreferences = TrackerPreferences
Expand Down Expand Up @@ -81,6 +82,14 @@ abstract class Tracker(val id: Int, val name: String) {
) {
trackPreferences.setTrackCredentials(this, username, password)
}

fun getIfAuthExpired(): Boolean {
return trackPreferences.trackAuthExpired(this)
}

fun setAuthExpired() {
trackPreferences.setTrackTokenExpired(this)
}
}

fun String.extractToken(key: String): String? {
Expand All @@ -93,3 +102,7 @@ fun String.extractToken(key: String): String? {
}
return null
}

class TokenExpired : IOException("Token is expired, re-logging required")

class TokenRefreshFailed : IOException("Token refresh failed")
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ object TrackerPreferences {

fun getTrackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")

fun trackAuthExpired(tracker: Tracker) =
preferenceStore.getBoolean(
trackTokenExpired(tracker.id),
false,
)

fun setTrackCredentials(
sync: Tracker,
username: String,
Expand All @@ -25,6 +31,7 @@ object TrackerPreferences {
preferenceStore.edit()
.putString(trackUsername(sync.id), username)
.putString(trackPassword(sync.id), password)
.putBoolean(trackTokenExpired(sync.id), false)
.apply()
}

Expand All @@ -38,14 +45,22 @@ object TrackerPreferences {
if (token == null) {
preferenceStore.edit()
.remove(trackToken(sync.id))
.putBoolean(trackTokenExpired(sync.id), false)
.apply()
} else {
preferenceStore.edit()
.putString(trackToken(sync.id), token)
.putBoolean(trackTokenExpired(sync.id), false)
.apply()
}
}

fun setTrackTokenExpired(sync: Tracker) {
preferenceStore.edit()
.putBoolean(trackTokenExpired(sync.id), true)
.apply()
}

fun getScoreType(sync: Tracker) = preferenceStore.getString(scoreType(sync.id), Anilist.POINT_10)

fun setScoreType(
Expand All @@ -63,5 +78,7 @@ object TrackerPreferences {

private fun trackToken(trackerId: Int) = "track_token_$trackerId"

private fun trackTokenExpired(trackerId: Int) = "track_token_expired_$trackerId"

private fun scoreType(trackerId: Int) = "score_type_$trackerId"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package suwayomi.tachidesk.manga.impl.track.tracker.anilist

import okhttp3.Interceptor
import okhttp3.Response
import suwayomi.tachidesk.manga.impl.track.tracker.TokenExpired
import java.io.IOException

class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
Expand All @@ -17,6 +18,9 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
}

override fun intercept(chain: Interceptor.Chain): Response {
if (anilist.getIfAuthExpired()) {
throw TokenExpired()
}
val originalRequest = chain.request()

if (token.isNullOrEmpty()) {
Expand All @@ -26,9 +30,9 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
oauth = anilist.loadOAuth()
}
// Refresh access token if null or expired.
if (oauth!!.isExpired()) {
anilist.logout()
throw IOException("Token expired")
if (oauth?.isExpired() == true) {
anilist.setAuthExpired()
throw TokenExpired()
}

// Throw on null auth.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist

import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Response
import suwayomi.tachidesk.manga.impl.track.tracker.TokenExpired
import suwayomi.tachidesk.manga.impl.track.tracker.TokenRefreshFailed
import uy.kohesive.injekt.injectLazy
import java.io.IOException

Expand All @@ -13,51 +16,27 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
private var oauth: OAuth? = null

override fun intercept(chain: Interceptor.Chain): Response {
if (myanimelist.getIfAuthExpired()) {
throw TokenExpired()
}
val originalRequest = chain.request()

if (token.isNullOrEmpty()) {
throw IOException("Not authenticated with MyAnimeList")
}
if (oauth == null) {
oauth = myanimelist.loadOAuth()
}
// Refresh access token if expired
if (oauth != null && oauth!!.isExpired()) {
setAuth(refreshToken(chain))
if (oauth?.isExpired() == true) {
refreshToken(chain)
}

if (oauth == null) {
throw IOException("No authentication token")
throw IOException("MAL: User is not authenticated")
}

// Add the authorization header to the original request
val authRequest =
originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "Suwayomi v${AppInfo.getVersionName()}")
.build()

val response = chain.proceed(authRequest)
val tokenIsExpired =
response.headers["www-authenticate"]
?.contains("The access token expired") ?: false

// Retry the request once with a new token in case it was not already refreshed
// by the is expired check before.
if (response.code == 401 && tokenIsExpired) {
response.close()

val newToken = refreshToken(chain)
setAuth(newToken)

val newRequest =
originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${newToken.access_token}")
.build()

return chain.proceed(newRequest)
}

return response
return chain.proceed(authRequest)
}

/**
Expand All @@ -70,23 +49,36 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
myanimelist.saveOAuth(oauth)
}

private fun refreshToken(chain: Interceptor.Chain): OAuth {
val newOauth =
runCatching {
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
private fun refreshToken(chain: Interceptor.Chain): OAuth =
synchronized(this) {
if (myanimelist.getIfAuthExpired()) throw TokenExpired()
oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it }

val response =
try {
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
} catch (_: Throwable) {
throw TokenRefreshFailed()
}

if (response.code == 401) {
myanimelist.setAuthExpired()
throw TokenExpired()
}

if (oauthResponse.isSuccessful) {
with(json) { oauthResponse.parseAs<OAuth>() }
return runCatching {
if (response.isSuccessful) {
with(json) { response.parseAs<OAuth>() }
} else {
oauthResponse.close()
response.close()
null
}
}

if (newOauth.getOrNull() == null) {
throw IOException("Failed to refresh the access token")
.getOrNull()
?.also {
this.oauth = it
myanimelist.saveOAuth(it)
}
?: throw TokenRefreshFailed()
}

return newOauth.getOrNull()!!
}
}

0 comments on commit 07e0110

Please sign in to comment.