-
Notifications
You must be signed in to change notification settings - Fork 1
Auth - Integrate Backend Token Endpoints #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…icator for token handling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR integrates backend token authentication endpoints into the Android app, implementing secure token storage with encryption, automatic token refresh, and comprehensive session management.
Key Changes
- Implemented encrypted token storage using Android Keystore and DataStore with custom serialization
- Created token refresh and re-authentication flow with OkHttp Interceptor and Authenticator
- Migrated from Moshi to Kotlin Serialization for JSON processing
- Separated authenticated and unauthenticated network clients to avoid circular dependencies
Reviewed Changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| gradle/libs.versions.toml | Removed Moshi dependencies, added DataStore Preferences and Kotlin Serialization converter |
| app/build.gradle.kts | Updated minSdk to 26 for Base64 support, replaced Moshi with Kotlin Serialization, added DataStore dependency |
| app/src/main/AndroidManifest.xml | Enabled cleartext traffic for HTTP API calls (development only) |
| app/src/main/java/com/cornellappdev/hustle/di/NetworkModule.kt | Created separate authenticated/unauthenticated OkHttp clients and Retrofit instances, integrated interceptor and authenticator |
| app/src/main/java/com/cornellappdev/hustle/di/DataStoreModule.kt | Configured encrypted DataStore for user preferences |
| app/src/main/java/com/cornellappdev/hustle/di/AppModule.kt | Updated import path for moved AuthRepository |
| app/src/main/java/com/cornellappdev/hustle/data/security/Crypto.kt | Implemented encryption/decryption using Android Keystore with AES-CBC |
| app/src/main/java/com/cornellappdev/hustle/data/local/auth/UserPreferencesSerializer.kt | Custom serializer for encrypting/decrypting DataStore data |
| app/src/main/java/com/cornellappdev/hustle/data/local/auth/TokenManager.kt | Manages secure storage and retrieval of access and refresh tokens |
| app/src/main/java/com/cornellappdev/hustle/data/repository/auth/SessionManager.kt | Notifies UI components when session expires |
| app/src/main/java/com/cornellappdev/hustle/data/repository/auth/AuthRepository.kt | Added token verification and storage on Google sign-in |
| app/src/main/java/com/cornellappdev/hustle/data/remote/auth/AuthApiService.kt | Defined verify-token and refresh-token API endpoints |
| app/src/main/java/com/cornellappdev/hustle/data/remote/auth/AuthInterceptor.kt | Adds Authorization header with access token to requests |
| app/src/main/java/com/cornellappdev/hustle/data/remote/auth/TokenAuthenticator.kt | Handles 401 responses with token refresh, Firebase re-auth fallback, and logout |
| app/src/main/java/com/cornellappdev/hustle/data/model/user/UserPreferences.kt | Data class for storing encrypted token data |
| app/src/main/java/com/cornellappdev/hustle/data/model/user/User.kt | Added UserResponse model for backend API |
| app/src/main/java/com/cornellappdev/hustle/data/model/user/AuthTokens.kt | Request/response models for token verification and refresh |
| app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/RootViewModel.kt | Integrated SessionManager to handle session expiration |
| app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt | Added navigation to onboarding screen when user is signed out |
| app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/onboarding/SignInScreenViewModel.kt | Updated import path for moved AuthRepository |
| app/src/main/java/com/cornellappdev/hustle/ui/viewmodels/profile/ProfileScreenViewModel.kt | Updated import path for moved AuthRepository |
Comments suppressed due to low confidence (2)
app/src/main/java/com/cornellappdev/hustle/data/repository/auth/AuthRepository.kt:76
- Error handling issue: If
authApiService.verifyToken()fails, the user is signed into Firebase but tokens are not stored, leaving the app in an inconsistent state. Consider rolling back the Firebase sign-in (callingfirebaseAuth.signOut()) when token verification fails to maintain consistency between Firebase auth state and backend token state.
app/src/main/java/com/cornellappdev/hustle/data/repository/auth/AuthRepository.kt:69 - Unclear error message: "Empty response body" doesn't provide enough context about what went wrong. Consider a more specific message like "Token verification succeeded but response body is empty" to help with debugging.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val accessToken = runBlocking { | ||
| tokenManager.getAccessToken() | ||
| } ?: return chain.proceed(originalRequest) |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
runBlocking used in interceptor chain. Using runBlocking in AuthInterceptor.intercept() can block the OkHttp thread pool, potentially causing performance issues or deadlocks under load. Consider using a coroutine-aware HTTP client or restructuring to avoid blocking calls in the interceptor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Little difficult to work around this because token manager is suspend and intercept is a synchronous function we are overriding for okhttp
app/src/main/java/com/cornellappdev/hustle/data/repository/auth/SessionManager.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/cornellappdev/hustle/data/remote/auth/AuthInterceptor.kt
Show resolved
Hide resolved
app/src/main/java/com/cornellappdev/hustle/data/repository/auth/SessionManager.kt
Show resolved
Hide resolved
| return runBlocking { | ||
| mutex.withLock { | ||
| val currentToken = tokenManager.getAccessToken() | ||
| val failedToken = response.request.header("Authorization")?.removePrefix("Bearer ") | ||
|
|
||
| // If the token has been updated since the request was made, use the new token and skip refresh flow | ||
| if (currentToken != null && currentToken != failedToken) { | ||
| return@runBlocking buildAuthRequest(response, currentToken) | ||
| } | ||
|
|
||
| // 1. Try to refresh the access token using the refresh token | ||
| // 2. If that fails, try to re-authenticate with the Firebase token | ||
| // 3. If that also fails, sign the user out and notify session expiration | ||
| tryRefreshToken(response) | ||
| ?: tryFirebaseReAuthentication(response) | ||
| ?: handleAuthFailure() | ||
| } | ||
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
runBlocking used in authenticator. Using runBlocking in TokenAuthenticator.authenticate() can block the OkHttp thread pool during token refresh attempts. While the mutex helps prevent concurrent refreshes, this could still cause performance degradation. Consider the performance implications, especially when multiple requests fail simultaneously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing here for the intercept with synchronous function and suspend functions used inside
app/src/main/java/com/cornellappdev/hustle/ui/navigation/HustleNavigation.kt
Show resolved
Hide resolved
app/src/main/java/com/cornellappdev/hustle/data/remote/auth/TokenAuthenticator.kt
Show resolved
Hide resolved
app/src/main/java/com/cornellappdev/hustle/data/local/auth/TokenManager.kt
Show resolved
Hide resolved
…boarding route before navigation; reworked to have individual retrofit builderinstances
zachseidner1
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Impressive stuff as always. Have some questions about the set up but ultimately this looks great. Also, make sure to post the secret.properties file in the hustle-android Slack channel!
| val iv = cipher.iv | ||
| val encrypted = cipher.doFinal(bytes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is iv?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initialization vector probably
| KeyProperties.PURPOSE_ENCRYPT or | ||
| KeyProperties.PURPOSE_DECRYPT | ||
| ) | ||
| .setBlockModes(BLOCK_MODE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this line here (maybe add comment explaining)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I might add a comment to this file with a link to Phillip Lackner's video for more information since I pretty much took it from there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I know, I think block mode is used since the AES algorithm we are using makes use of block ciphers, and we are telling it to only allow the use of the Cipher Block Chaining strategy for generating the key to solve the problem where the input block will always output the same output block if encrypting each block with the key, so malicious actors can recognize the pattern potentially. (source: Gemini)
| val existingKey = keyStore | ||
| .getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry | ||
| return existingKey?.secretKey ?: createKey() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering how this works, does this use a new key every time we run the app, or is the key somehow saved locally and persisted across sessions? If we are using a new key for every run of the app, how will that work for decrypting stuff we encrypted in the past using a different key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be the same key that is saved locally I believe. I think it checks if there is an existing key in the keystore, and if not, it creates a key and stores it into the keystore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lol following Philipp Lackner tutorial, I approve
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, he is the goat
| replay = 0, | ||
| extraBufferCapacity = 1, | ||
| onBufferOverflow = BufferOverflow.DROP_OLDEST |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
curious why these settings exactly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
replay = 0 is to make sure we don't cache any old events for new subscribers because we only want the session expiration to occur once.
extraBufferCapacity = 1: tryEmit is not suspending, so this adds a buffer for tryEmit to drop the event in in case the collector is busy at the moment.
onBufferOverflow = DROP_OLDEST is to make sure we always get the newest event if for instance 2 network calls fail at the same time.
Overview
Added logic to handle access and refresh tokens from backend
Changes Made
Test Coverage
Next Steps (delete if not applicable)
Related PRs or Issues (delete if not applicable)
Screenshots (delete if not applicable)
Screen.Recording.2025-11-13.at.12.41.31.AM.mov
Misc