1+ package com.cornellappdev.uplift.data.auth
2+
3+ import android.util.Log
4+ import com.apollographql.apollo.ApolloClient
5+ import com.cornellappdev.uplift.RefreshAccessTokenMutation
6+ import kotlinx.coroutines.runBlocking
7+ import kotlinx.coroutines.withTimeout
8+ import okhttp3.Authenticator
9+ import okhttp3.Request
10+ import okhttp3.Response
11+ import okhttp3.Route
12+ import javax.inject.Inject
13+ import javax.inject.Named
14+ import javax.inject.Singleton
15+
16+ @Singleton
17+ class TokenAuthenticator @Inject constructor(
18+ private val tokenManager : TokenManager ,
19+ private val sessionManager : SessionManager ,
20+ @Named(" refresh" ) private val apolloClient : ApolloClient
21+ ) : Authenticator {
22+
23+ override fun authenticate (route : Route ? , response : Response ): Request ? {
24+ if (responseCount(response) >= 2 ) {
25+ return null
26+ }
27+
28+ val refreshToken = tokenManager.getRefreshToken() ? : return null
29+
30+ synchronized(this ) {
31+ // Check if the token was already refreshed by another thread
32+ // while this request was waiting for the lock.
33+ val currentToken = tokenManager.getAccessToken()
34+ val requestToken = response.request.header(" Authorization" )?.substringAfter(" Bearer " )
35+
36+ if (currentToken != null && currentToken != requestToken ) {
37+ return response.request.newBuilder()
38+ .header(" Authorization" , " Bearer $currentToken " )
39+ .build()
40+ }
41+
42+ // 3. Since OkHttp's Authenticator is synchronous but Apollo is suspend-based,
43+ // we use runBlocking to wait for the refresh mutation result.
44+ return runBlocking {
45+ try {
46+ val mutationResponse = withTimeout(10000L ) {
47+ apolloClient.mutation(RefreshAccessTokenMutation ())
48+ // We manually add the Refresh Token to this specific call
49+ // because the "refresh" ApolloClient has no interceptor.
50+ .addHttpHeader(" Authorization" , " Bearer $refreshToken " )
51+ .execute()
52+ }
53+
54+ val newAccessToken = mutationResponse.data?.refreshAccessToken?.newAccessToken
55+
56+ if (newAccessToken != null && newAccessToken != requestToken) {
57+ tokenManager.saveTokens(newAccessToken, refreshToken)
58+
59+ // Retry the original request with the new Access Token
60+ response.request.newBuilder()
61+ .header(" Authorization" , " Bearer $newAccessToken " )
62+ .build()
63+ } else {
64+ // Refresh failed (e.g., refresh token expired on backend)
65+ sessionManager.logout()
66+ null
67+ }
68+ } catch (e: Exception ) {
69+ // Network error or server down during refresh
70+ Log .e(" TokenAuthenticator" , " Refresh timed out or failed" , e)
71+ sessionManager.logout()
72+ null
73+ }
74+ }
75+ }
76+ }
77+
78+ private fun responseCount (response : Response ? ): Int {
79+ var result = 1
80+ var current = response
81+ // Traverse the chain of prior responses
82+ while (current?.priorResponse != null ) {
83+ result++
84+ current = current.priorResponse
85+ }
86+ return result
87+ }
88+ }
0 commit comments