-
Notifications
You must be signed in to change notification settings - Fork 656
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🔒 deprecate BearerTokenInterceptor and provide tests and docs instead (…
…#4068) * deprecate BearerTokenInterceptor and provide tests and docs instead * add to the left pane * Update docs/source/advanced/authentication.mdx Co-authored-by: Benoit Lubek <[email protected]> * Update docs/source/advanced/authentication.mdx Co-authored-by: Benoit Lubek <[email protected]> * Update docs/source/advanced/authentication.mdx Co-authored-by: Benoit Lubek <[email protected]> Co-authored-by: Benoit Lubek <[email protected]>
- Loading branch information
1 parent
8d05ac2
commit 6f0b798
Showing
9 changed files
with
481 additions
and
361 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
...me/src/commonMain/kotlin/com/apollographql/apollo3/network/http/BearerTokenInterceptor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/TokenProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
164 changes: 164 additions & 0 deletions
164
...ntime/src/commonTest/kotlin/com/apollographql/apollo3/network/AuthorizationInterceptor.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package com.apollographql.apollo3.network | ||
|
||
import com.apollographql.apollo3.api.http.HttpRequest | ||
import com.apollographql.apollo3.api.http.HttpResponse | ||
import com.apollographql.apollo3.mpp.currentTimeMillis | ||
import com.apollographql.apollo3.network.AuthorizationInterceptor.TokenProvider | ||
import com.apollographql.apollo3.network.http.HttpInterceptor | ||
import com.apollographql.apollo3.network.http.HttpInterceptorChain | ||
import kotlinx.coroutines.sync.Mutex | ||
import kotlinx.coroutines.sync.withLock | ||
|
||
/** | ||
* An [HttpInterceptor] that handles authentication | ||
* | ||
* This is provided as is and will most likely need to be adapted to different backend requirements. | ||
* | ||
* There is surprising amount of details that can differ between different implementation. | ||
* | ||
* This [AuthorizationInterceptor] assumes the tokens form a chain where each new token | ||
* is computed based on the previous token value and invalidates all previous ones. | ||
* | ||
* @param tokenProvider a [TokenProvider] that gets tokens from the preferences or from the network | ||
* @param maxSize The maximum number of links to keep. This is theoretically needed in case some requests are | ||
* **very** slow and the token has been refreshed (by other concurrent requests) multiple times | ||
* when the initial request receives the 401. | ||
* | ||
* In practice, this is very unlikely to happen and a max size of 1 should be enough for most | ||
* scenarios | ||
*/ | ||
class AuthorizationInterceptor(private val tokenProvider: TokenProvider, private val maxSize: Int = 1) : HttpInterceptor { | ||
private val mutex = Mutex() | ||
|
||
class Token(val value: String, val expiresEpochSecond: Long) | ||
|
||
interface TokenProvider { | ||
/** | ||
* Load the token from preferences the first time a token is required. | ||
* | ||
* This function is never called concurrently. Implementation do not need to lock. | ||
* | ||
* @return the token from the preferences or null if no token was ever generated | ||
*/ | ||
suspend fun loadToken(): Token? | ||
|
||
/** | ||
* Refreshes an existing token. This is called when a token is expired or a 401 | ||
* error is received. | ||
* | ||
* This function is never called concurrently. Implementation do not need to lock. | ||
* | ||
* Any exception thrown will bubble up to the caller | ||
* | ||
* @param oldToken the previous token or null if there was none | ||
* @return a new token from the oldToken | ||
*/ | ||
suspend fun refreshToken(oldToken: String?): Token | ||
} | ||
|
||
/** | ||
* A token link in the chain | ||
*/ | ||
class TokenLink( | ||
val oldValue: String?, | ||
val newValue: String, | ||
val expiresEpochSecond: Long, | ||
var next: TokenLink?, | ||
) | ||
|
||
private var head: TokenLink? = null | ||
private var tail: TokenLink? = null | ||
private var listSize: Int = 0 | ||
|
||
override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse { | ||
val tokenValue = mutex.withLock { | ||
if (tail == null) { | ||
var token = tokenProvider.loadToken() | ||
if (token == null) { | ||
token = tokenProvider.refreshToken(null) | ||
} | ||
tail = TokenLink( | ||
oldValue = null, | ||
newValue = token.value, | ||
expiresEpochSecond = token.expiresEpochSecond, | ||
next = null | ||
) | ||
head = tail | ||
listSize++ | ||
} | ||
|
||
val link = tail!! | ||
|
||
// Start refreshing tokens 2 seconds before they actually expire to account for | ||
// network time | ||
val margin = 2 | ||
if (currentTimeMillis() / 1000 + margin - link.expiresEpochSecond >= 0) { | ||
// This token will soon expire, get a new one | ||
val token = tokenProvider.refreshToken(link.newValue) | ||
|
||
insert( | ||
TokenLink( | ||
oldValue = link.newValue, | ||
newValue = token.value, | ||
expiresEpochSecond = token.expiresEpochSecond, | ||
next = null | ||
) | ||
) | ||
} | ||
|
||
tail!!.newValue | ||
} | ||
|
||
val response = chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $tokenValue").build()) | ||
|
||
return if (response.statusCode == 401) { | ||
val newTokenValue: String = mutex.withLock { | ||
var cur = head | ||
while (cur != null) { | ||
if (cur.oldValue == tokenValue) { | ||
// follow the chain up to the new token | ||
while (cur!!.next != null) { | ||
cur = cur.next | ||
} | ||
// we have found a valid new token for this old token | ||
return@withLock cur.newValue | ||
} | ||
cur = cur.next | ||
} | ||
|
||
// we haven't found a link for this old value, get a new token | ||
val token = tokenProvider.refreshToken(tokenValue) | ||
insert( | ||
TokenLink( | ||
oldValue = tokenValue, | ||
newValue = token.value, | ||
expiresEpochSecond = token.expiresEpochSecond, | ||
next = null | ||
) | ||
) | ||
|
||
token.value | ||
} | ||
chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $newTokenValue").build()) | ||
} else { | ||
response | ||
} | ||
} | ||
|
||
/** | ||
* Insert a new link. | ||
* | ||
* Assumes the list is not empty | ||
*/ | ||
private fun insert(tokenLink: TokenLink) { | ||
tail!!.next = tokenLink | ||
tail = tokenLink | ||
listSize++ | ||
|
||
// Trim the list if needed | ||
while (listSize > maxSize) { | ||
head = head!!.next | ||
listSize-- | ||
} | ||
} | ||
} |
Oops, something went wrong.