|
| 1 | +package com.cornellappdev.uplift.data.auth |
| 2 | + |
| 3 | +import com.apollographql.apollo.ApolloClient |
| 4 | +import com.apollographql.apollo.api.ApolloRequest |
| 5 | +import com.apollographql.apollo.api.ApolloResponse |
| 6 | +import com.apollographql.apollo.api.ExecutionContext |
| 7 | +import com.apollographql.apollo.api.Operation |
| 8 | +import com.apollographql.apollo.interceptor.ApolloInterceptor |
| 9 | +import com.apollographql.apollo.interceptor.ApolloInterceptorChain |
| 10 | +import com.cornellappdev.uplift.RefreshAccessTokenMutation |
| 11 | +import kotlinx.coroutines.flow.Flow |
| 12 | +import kotlinx.coroutines.flow.emitAll |
| 13 | +import kotlinx.coroutines.flow.first |
| 14 | +import kotlinx.coroutines.flow.flow |
| 15 | +import javax.inject.Inject |
| 16 | +import javax.inject.Named |
| 17 | +import javax.inject.Singleton |
| 18 | + |
| 19 | +/** |
| 20 | + * Execution context to track retries. |
| 21 | + */ |
| 22 | +internal class RetryContext(val retryCount: Int) : ExecutionContext.Element { |
| 23 | + override val key: ExecutionContext.Key<*> = Key |
| 24 | + |
| 25 | + companion object Key : ExecutionContext.Key<RetryContext> |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * An Apollo Interceptor that handles token expiration errors that return as 200 OK with |
| 30 | + * GraphQL errors (specifically "Signature has expired"). |
| 31 | + */ |
| 32 | +@Singleton |
| 33 | +class ApolloAuthInterceptor @Inject constructor( |
| 34 | + private val tokenManager: TokenManager, |
| 35 | + private val sessionManager: SessionManager, |
| 36 | + @Named("refresh") private val refreshClient: ApolloClient |
| 37 | +) : ApolloInterceptor { |
| 38 | + override fun <D : Operation.Data> intercept( |
| 39 | + request: ApolloRequest<D>, |
| 40 | + chain: ApolloInterceptorChain |
| 41 | + ): Flow<ApolloResponse<D>> = flow { |
| 42 | + val response = chain.proceed(request).first() |
| 43 | + |
| 44 | + val retryCount = request.executionContext[RetryContext]?.retryCount ?: 0 |
| 45 | + // TODO: replace string check with explicit error codes if backend implements |
| 46 | + if (response.errors?.any { it.message.contains("Signature has expired") } == true && retryCount < 1) { |
| 47 | + val refreshToken = tokenManager.getRefreshToken() |
| 48 | + if (refreshToken != null) { |
| 49 | + try { |
| 50 | + val mutationResponse = refreshClient.mutation(RefreshAccessTokenMutation()) |
| 51 | + .addHttpHeader("Authorization", "Bearer $refreshToken") |
| 52 | + .execute() |
| 53 | + |
| 54 | + val newAccessToken = mutationResponse.data?.refreshAccessToken?.newAccessToken |
| 55 | + if (newAccessToken != null) { |
| 56 | + tokenManager.saveTokens(newAccessToken, refreshToken) |
| 57 | + // Retry the request with the new token |
| 58 | + val newRequest = request.newBuilder() |
| 59 | + .addExecutionContext(RetryContext(retryCount + 1)) |
| 60 | + .build() |
| 61 | + emitAll(chain.proceed(newRequest)) |
| 62 | + return@flow |
| 63 | + } else { |
| 64 | + sessionManager.logout() |
| 65 | + } |
| 66 | + } catch (e: Exception) { |
| 67 | + sessionManager.logout() |
| 68 | + } |
| 69 | + } else { |
| 70 | + sessionManager.logout() |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + emit(response) |
| 75 | + } |
| 76 | +} |
0 commit comments