Fix unhelpful generic 401 error on AniList expiry (#3888)
A leftover of what I assume was the v1 API behaviour handling manually multiplied the `expires` values by 1,000, supposedly to make it a millisecond value. However, AL tokens have been saved as milliseconds since at least the v2 API implementation in 2018, so this has made every expiry comparison ludicrously impossible. Example: millisecond timestamp: 1788529931115 (2026-09-04T13:52:11.115Z) *1,000 1788529931115000 (+58646-04-04T21:45:15Z) So any expiry comparisons would say "yes this token is still totally valid", causing Mihon to send expired credentials and AL to respond with a 401. With these changes (which include some loosely associated clean-up), we now correctly identify expired credentials before we attempt any AL requests & actually show a toast with the "Token expired." error message for the first time in 8 years or so? I was able to keep the new ALOAuth shape compatible with already-serialized data, though whenever a user (re-)links AL from now on, both the `token_type` and `expiresIn` fields won't exist any more. Since they had no use in the current code anyway, I don't consider this a problem. I took the liberty of adding basic instructions to the error message but these can't be i18n'd properly so I kept it short and sweet. --- Side effect of expired credentials is Mihon erasing the token in storage, meaning AL disappears from the tracker sheet until re-linked. I would say this is acceptable, but we might see panicked users in support saying "Mihon deleted my AL links" or something.
This commit is contained in:
@@ -20,6 +20,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
|
||||
- Show scores in MangaUpdates search results (and authors for `id:` prefix searches) ([@MajorTanya](https://github.com/MajorTanya)) ([#3795](https://github.com/mihonapp/mihon/pull/3795))
|
||||
- Remove whitespace from MAL and MB `id:` prefix search inputs before searching ([@MajorTanya](https://github.com/MajorTanya)) ([#3793](https://github.com/mihonapp/mihon/pull/3793))
|
||||
- Show a helpful error message for expired AniList credentials ([@MajorTanya](https://github.com/MajorTanya)) ([#3888](https://github.com/mihonapp/mihon/pull/3888))
|
||||
|
||||
### Fixed
|
||||
- Fixed app and extension update check running again on configuration change ([@AntsyLich](https://github.com/AntsyLich)) ([#3708](https://github.com/mihonapp/mihon/pull/3708))
|
||||
|
||||
@@ -218,7 +218,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
||||
|
||||
suspend fun login(token: String) {
|
||||
try {
|
||||
val oauth = api.createOAuth(token)
|
||||
val oauth = ALOAuth(token)
|
||||
interceptor.setAuth(oauth)
|
||||
val currentUser = api.getCurrentUser()
|
||||
scorePreference.set(currentUser.mediaListOptions.scoreFormat)
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserViewerData
|
||||
@@ -283,10 +282,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
return findLibManga(track, userId) ?: throw Exception("Could not find manga")
|
||||
}
|
||||
|
||||
fun createOAuth(token: String): ALOAuth {
|
||||
return ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||
}
|
||||
|
||||
suspend fun getCurrentUser(): ALUserViewerData {
|
||||
return withIOContext {
|
||||
val query = """
|
||||
|
||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.isExpired
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
@@ -11,14 +10,8 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*
|
||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||
* before its original expiration date.
|
||||
*/
|
||||
private var oauth: ALOAuth? = null
|
||||
set(value) {
|
||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
@@ -27,17 +20,11 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
||||
throw Exception("Not authenticated with Anilist")
|
||||
}
|
||||
if (oauth == null) {
|
||||
oauth = anilist.loadOAuth()
|
||||
oauth = anilist.loadOAuth() ?: throw IOException("No authentication token")
|
||||
}
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth!!.isExpired()) {
|
||||
anilist.logout()
|
||||
throw IOException("Token expired")
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw IOException("No authentication token")
|
||||
throw IOException("Token expired. Reconnect AniList in Settings.")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@Serializable
|
||||
data class ALOAuth(
|
||||
@SerialName("access_token")
|
||||
val accessToken: String,
|
||||
@SerialName("token_type")
|
||||
val tokenType: String,
|
||||
val expires: Long,
|
||||
@SerialName("expires_in")
|
||||
val expiresIn: Long,
|
||||
)
|
||||
|
||||
fun ALOAuth.isExpired() = System.currentTimeMillis() > expires
|
||||
@EncodeDefault
|
||||
val expires: Long = Clock.System.now().plus(365.days).toEpochMilliseconds(),
|
||||
) {
|
||||
fun isExpired() = Clock.System.now().plus(1.minutes).toEpochMilliseconds() > expires
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user