mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 11:17:16 -04:00
fix(stores): drop expired Epic and Amazon tokens when a refresh fails (#804)
A failed token refresh returned the stored access token even after it had expired, so the next request failed with a generic error instead of asking to sign in again. The stored token is now returned only while it is still valid. The Epic docs now say that achievements are sent with Epic's launcher user agent, which the achievements service requires.
This commit is contained in:
1 parent
4b17bc9a23
commit
d99d728ba5
4 files changed
+17
-9
No files matched your search
@@ -70,6 +70,9 @@ object AmazonCredentialStore {
|
|||||||
private fun fresh(a: Account): Boolean =
|
private fun fresh(a: Account): Boolean =
|
||||||
a.expiresAt - System.currentTimeMillis() > REFRESH_MARGIN_MS || a.refreshToken.isEmpty()
|
a.expiresAt - System.currentTimeMillis() > REFRESH_MARGIN_MS || a.refreshToken.isEmpty()
|
||||||
|
|
||||||
|
private fun unexpired(a: Account): String? =
|
||||||
|
a.accessToken.takeIf { a.expiresAt <= 0 || a.expiresAt > System.currentTimeMillis() }
|
||||||
|
|
||||||
private fun refreshLocked(context: Context, a: Account): String? {
|
private fun refreshLocked(context: Context, a: Account): String? {
|
||||||
if (a.refreshToken.isEmpty()) return null
|
if (a.refreshToken.isEmpty()) return null
|
||||||
val answer = AmazonNative.call("auth.refresh", JSONObject().put("refreshToken", a.refreshToken))
|
val answer = AmazonNative.call("auth.refresh", JSONObject().put("refreshToken", a.refreshToken))
|
||||||
@@ -90,7 +93,7 @@ object AmazonCredentialStore {
|
|||||||
synchronized(refreshLock) {
|
synchronized(refreshLock) {
|
||||||
val again = load(context) ?: return null
|
val again = load(context) ?: return null
|
||||||
if (fresh(again)) return again.accessToken
|
if (fresh(again)) return again.accessToken
|
||||||
return refreshLocked(context, again) ?: again.accessToken
|
return refreshLocked(context, again) ?: unexpired(again)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ object EpicCredentialStore {
|
|||||||
private fun fresh(c: Credentials): Boolean =
|
private fun fresh(c: Credentials): Boolean =
|
||||||
c.expiresAt - System.currentTimeMillis() > REFRESH_MARGIN_MS || c.refreshToken.isNullOrEmpty()
|
c.expiresAt - System.currentTimeMillis() > REFRESH_MARGIN_MS || c.refreshToken.isNullOrEmpty()
|
||||||
|
|
||||||
|
private fun unexpired(c: Credentials): String? =
|
||||||
|
c.accessToken.takeIf { c.expiresAt <= 0 || c.expiresAt > System.currentTimeMillis() }
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getValidAccessToken(context: Context): String? {
|
fun getValidAccessToken(context: Context): String? {
|
||||||
val current = load(context) ?: return null
|
val current = load(context) ?: return null
|
||||||
@@ -96,7 +99,7 @@ object EpicCredentialStore {
|
|||||||
val result = answer?.let(TokenResult::from)
|
val result = answer?.let(TokenResult::from)
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
Log.w(TAG, "token refresh failed: ${answer?.optString("error").orEmpty().ifEmpty { "no answer" }}")
|
Log.w(TAG, "token refresh failed: ${answer?.optString("error").orEmpty().ifEmpty { "no answer" }}")
|
||||||
return again.accessToken
|
return unexpired(again)
|
||||||
}
|
}
|
||||||
save(context, result)
|
save(context, result)
|
||||||
return result.accessToken
|
return result.accessToken
|
||||||
|
|||||||
@@ -118,9 +118,10 @@ On-disk protocol (what a resumed run, by either engine, relies on): a file is as
|
|||||||
Everything Epic runs in the Rust `epic` module: the web-service calls (sign-in, refresh, exchange
|
Everything Epic runs in the Rust `epic` module: the web-service calls (sign-in, refresh, exchange
|
||||||
codes, library, catalog, build information, shop shelves, achievements, cloud saves), manifest
|
codes, library, catalog, build information, shop shelves, achievements, cloud saves), manifest
|
||||||
parsing and the install engine. Tokens are passed in per call and never stored or logged by the
|
parsing and the install engine. Tokens are passed in per call and never stored or logged by the
|
||||||
library; the Kotlin facade keeps them in the shared Keystore-sealed account store. All requests send
|
library; the Kotlin facade keeps them in the shared Keystore-sealed account store. Requests send
|
||||||
one User-Agent, `DroidDeck/<versionName>` (`epic/net.rs::USER_AGENT_PRODUCT`; the version comes from
|
`DroidDeck/<versionName>` (`epic/net.rs::USER_AGENT_PRODUCT`; the version comes from
|
||||||
`nativeConfigure`).
|
`nativeConfigure`), except achievements, which Epic only answers for its launcher agent
|
||||||
|
(`epic/net.rs::EPIC_LAUNCHER_USER_AGENT`).
|
||||||
|
|
||||||
| Method | JNI signature | Symbol suffix |
|
| Method | JNI signature | Symbol suffix |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
@@ -66,9 +66,10 @@ Numbers in brackets refer to the spec's list of unknowns.
|
|||||||
**Chosen where the spec left it open**
|
**Chosen where the spec left it open**
|
||||||
|
|
||||||
3. **User-Agent** [1]: every HTTP request (account, library, catalog, build info, CDNs, store
|
3. **User-Agent** [1]: every HTTP request (account, library, catalog, build info, CDNs, store
|
||||||
GraphQL, promotions, achievements, cloud saves) sends `DroidDeck/<versionName>`, defined once in
|
GraphQL, promotions, cloud saves) sends `DroidDeck/<versionName>`, defined once in
|
||||||
`epic/net.rs` (`USER_AGENT_PRODUCT`). Whether every endpoint accepts it is untested; changing that
|
`epic/net.rs` (`USER_AGENT_PRODUCT`); changing that constant is the one place to adjust.
|
||||||
constant is the one place to adjust. The sign-in **WebView** keeps the launcher-style UA it used
|
Achievements are the exception: Epic's achievements service rejects other agents, so
|
||||||
|
`epic/achievements.rs` sends Epic's launcher agent (`net.rs::EPIC_LAUNCHER_USER_AGENT`). The sign-in **WebView** keeps the launcher-style UA it used
|
||||||
before, because a sign-in page that rejects the UA would lock every user out; that string lives
|
before, because a sign-in page that rejects the UA would lock every user out; that string lives
|
||||||
in `epic/account.rs` (`WEBVIEW_USER_AGENT`).
|
in `epic/account.rs` (`WEBVIEW_USER_AGENT`).
|
||||||
4. **Shared fetch core** [14]: chunk downloads reuse the crate's store-agnostic `fetch_core` (also
|
4. **Shared fetch core** [14]: chunk downloads reuse the crate's store-agnostic `fetch_core` (also
|
||||||
@@ -138,6 +139,6 @@ Numbers in brackets refer to the spec's list of unknowns.
|
|||||||
- Sign-in through the WebView, and that an **existing** saved account (sealed `credentials.json`)
|
- Sign-in through the WebView, and that an **existing** saved account (sealed `credentials.json`)
|
||||||
stays signed in after the update.
|
stays signed in after the update.
|
||||||
- Every endpoint with the DroidDeck User-Agent: library, catalog, build info, CDN downloads, store
|
- Every endpoint with the DroidDeck User-Agent: library, catalog, build info, CDN downloads, store
|
||||||
GraphQL and promotions, achievements, cloud saves.
|
GraphQL and promotions, cloud saves (achievements use the launcher agent, see above).
|
||||||
- Library sync and shelves; install, pause/resume, cancel, update and repair; a card install with
|
- Library sync and shelves; install, pause/resume, cancel, update and repair; a card install with
|
||||||
the internal scratch cache; launch with an exchange code; cloud-save up/down.
|
the internal scratch cache; launch with an exchange code; cloud-save up/down.
|
||||||
Reference in new issue
Block a user