Skip to content

♻️ [KMP] Refactor and simplify refresh token login in AuthProviderImpl#47

Merged
JuliaJakubcova merged 1 commit into
mainfrom
fix/kmp/auth-token-refresh
Mar 5, 2026
Merged

♻️ [KMP] Refactor and simplify refresh token login in AuthProviderImpl#47
JuliaJakubcova merged 1 commit into
mainfrom
fix/kmp/auth-token-refresh

Conversation

@JuliaJakubcova

@JuliaJakubcova JuliaJakubcova commented Feb 16, 2026

Copy link
Copy Markdown
Collaborator

📝 Description

  • Logic for refreshing token was over-complicated and dependent on coroutine scope of the first caller

💡 What’s new?

  • With @filwiesner we refactored and simplified the AuthProviderImpl and fixed the problem with the scope that is refreshing being cancelled in which case subsequent calls would not get the refreshed token

😶 What’s missing?

  • Nothing

Summary by CodeRabbit

  • Refactor
    • Simplified and hardened token refresh logic to ensure refreshes run sequentially and avoid overlapping attempts; no public API changes.
  • Bug Fixes
    • Reduces rare authentication errors and unexpected sign-outs during concurrent requests, improving session stability.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @JuliaJakubcova, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the token refresh mechanism within the authentication provider. The primary goal was to simplify the existing logic, which was overly complex and prone to issues related to coroutine scope cancellation. The changes introduce a more robust and streamlined approach to ensure reliable token refreshing across the application.

Highlights

  • Simplified token refresh logic: The refreshToken method in AuthProviderImpl has been refactored to remove complex Deferred and async based state management, leading to a more straightforward and robust implementation.
  • Fixed coroutine scope dependency: The previous logic's dependency on the initial caller's coroutine scope for token refreshing has been eliminated. The new approach ensures that token refreshes are not cancelled prematurely, improving reliability by using NonCancellable context.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt
    • Removed Deferred, async, and coroutineScope imports.
    • Added NonCancellable and withContext imports.
    • Eliminated the inFlight property used for managing concurrent refresh requests.
    • Rewrote the refreshToken method to use a simpler mutex-based approach with NonCancellable context for the actual refresh operation, addressing issues with scope cancellation.
Activity
  • No specific activity (comments, reviews, or progress updates) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown

Walkthrough

Replaces Deferred-based in-flight token refresh with a mutex-guarded sequential refresh in AuthProviderImpl; refreshToken captures the current token, acquires a Mutex, conditionally refreshes inside withContext(NonCancellable), updates the token, and returns the (possibly refreshed) token.

Changes

Cohort / File(s) Summary
Token Refresh Synchronization
shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt
Removed Deferred/async/coroutineScope-based in-flight refresh tracking. Implemented Mutex-guarded sequential refresh: refreshToken() captures oldToken, locks the mutex, performs refresh within withContext(NonCancellable) only if token unchanged, updates token, and returns current token. Updated imports accordingly.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I paused at the gate, one hop, one key,

A mutex hug keeps chaos free.
I swap the token with quiet cheer,
One-at-a-time — no races here.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: refactoring and simplifying the refresh token logic in AuthProviderImpl, which matches the core purpose of the pull request.
Description check ✅ Passed The description covers all required template sections with substantive content about the problem, solution, and states nothing is missing, though detail about the implementation approach could be more specific.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/kmp/auth-token-refresh

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly improves the token refresh logic by replacing a complex, Deferred-based implementation with a more robust and simpler approach using a Mutex and NonCancellable context. This correctly addresses the issue of caller scope cancellation affecting the refresh operation. I have one suggestion to further refine the new implementation by removing a redundant lock and adopting a more idiomatic return pattern, which will enhance both performance and code clarity.

Comment on lines 24 to 33
val oldToken = mutex.withLock { token }

val task = async {
val fresh = tokenRefresher.refresh()
token = fresh
fresh
}
inFlight = task
task
}.let { task ->
try {
task.await()
} finally {
mutex.withLock {
if (inFlight === task) {
inFlight = null
}
mutex.withLock {
if (oldToken == token) {
withContext(NonCancellable) {
token = tokenRefresher.refresh()
}
}
return token
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This implementation of the double-checked locking pattern can be slightly improved for efficiency and readability.

Currently, you are locking the mutex twice: once to read the oldToken and a second time for the check-and-set operation. The first lock is redundant. Reading the token property is not the operation that needs to be synchronized; the critical section is the part that checks the token and potentially refreshes it.

You can simplify this by reading oldToken without a lock, and then using a single withLock block that returns the final token value. This avoids the extra lock/unlock overhead and also makes the code more idiomatic by avoiding a non-local return.

Suggested change
val oldToken = mutex.withLock { token }
val task = async {
val fresh = tokenRefresher.refresh()
token = fresh
fresh
}
inFlight = task
task
}.let { task ->
try {
task.await()
} finally {
mutex.withLock {
if (inFlight === task) {
inFlight = null
}
mutex.withLock {
if (oldToken == token) {
withContext(NonCancellable) {
token = tokenRefresher.refresh()
}
}
return token
}
val oldToken = token
return mutex.withLock {
if (oldToken == token) {
withContext(NonCancellable) {
token = tokenRefresher.refresh()
}
}
token
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt`:
- Around line 23-34: The first mutex.withLock in refreshToken() is redundant;
collapse the two critical sections into a single mutex.withLock in the
refreshToken function: inside the single withLock read a local oldToken = token,
if oldToken == token then call tokenRefresher.refresh() (using NonCancellable
context) and assign token, then return token; remove the separate initial
mutex.withLock and ensure you still use mutex, token, tokenRefresher and
NonCancellable as in the original.

@JuliaJakubcova JuliaJakubcova force-pushed the fix/kmp/auth-token-refresh branch from 431cb00 to 428cb7d Compare February 16, 2026 12:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt`:
- Around line 27-29: The refresh result from tokenRefresher.refresh() may be
null, causing token to be set to null and every subsequent caller to repeatedly
attempt refresh; change the logic inside AuthProviderImpl where token is
assigned (the withContext(NonCancellable) block around tokenRefresher.refresh())
to handle a null return: if refresh() returns null, do not assign null to token
— instead clear stored credentials / set a "needsLogin" flag or throw a specific
AuthenticationRequiredException so callers will stop retrying, persist or
broadcast the logged-out state as appropriate, and ensure the lock-release path
communicates that no further automatic retries should occur until explicit
re-login.

Comment on lines +27 to +29
if (oldToken == token) {
withContext(NonCancellable) {
token = tokenRefresher.refresh()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider the null refresh result scenario.

If tokenRefresher.refresh() returns null (e.g. refresh token expired), token is set to null. Every subsequent caller will snapshot oldToken = null, enter the lock, see null == nulltrue, and re-attempt the refresh — potentially hammering the auth server.

If this is a realistic failure mode, consider short-circuiting when the refresh yields null (e.g. clear credentials and force re-login rather than retrying indefinitely).

🤖 Prompt for AI Agents
In
`@shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt`
around lines 27 - 29, The refresh result from tokenRefresher.refresh() may be
null, causing token to be set to null and every subsequent caller to repeatedly
attempt refresh; change the logic inside AuthProviderImpl where token is
assigned (the withContext(NonCancellable) block around tokenRefresher.refresh())
to handle a null return: if refresh() returns null, do not assign null to token
— instead clear stored credentials / set a "needsLogin" flag or throw a specific
AuthenticationRequiredException so callers will stop retrying, persist or
broadcast the logged-out state as appropriate, and ensure the lock-release path
communicates that no further automatic retries should occur until explicit
re-login.

@filwiesner filwiesner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good enough :)

@JuliaJakubcova JuliaJakubcova force-pushed the fix/kmp/auth-token-refresh branch from 428cb7d to 3f39c70 Compare March 5, 2026 13:43
@JuliaJakubcova JuliaJakubcova enabled auto-merge (squash) March 5, 2026 13:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt (1)

27-30: ⚠️ Potential issue | 🟠 Major

Handle null refresh results as a terminal auth state.

On Line 29, assigning tokenRefresher.refresh() directly to token allows null to persist and can cause repeated automatic refresh attempts after terminal failure (e.g., expired refresh token). Stop retrying until explicit re-login (or throw a dedicated auth-required exception).

♻️ Suggested hardening
 internal class AuthProviderImpl(
     private val settings: Settings,
     private val tokenRefresher: TokenRefresher,
 ) : AuthProvider {
+    private var requiresLogin = false

     override var token: String?
         get() = settings.getStringOrNull(TOKEN_KEY)
-        set(value) = settings.set(TOKEN_KEY, value)
+        set(value) {
+            settings.set(TOKEN_KEY, value)
+            if (value != null) requiresLogin = false
+        }

     private val mutex = Mutex()

     override suspend fun refreshToken(): String? {
         val oldToken = token

         mutex.withLock {
+            if (requiresLogin) return null
             if (oldToken == token) {
                 withContext(NonCancellable) {
-                    token = tokenRefresher.refresh()
+                    val refreshed = tokenRefresher.refresh()
+                    if (refreshed == null) {
+                        requiresLogin = true
+                        token = null
+                        return@withContext
+                    }
+                    token = refreshed
                 }
             }
             return token
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt`
around lines 27 - 30, The current refresh logic in AuthProviderImpl assigns
tokenRefresher.refresh() directly to token which allows a null to persist and
triggers repeated refresh attempts; change the block inside the oldToken ==
token branch so that after calling tokenRefresher.refresh() you check the result
and if it is null treat it as a terminal auth state: do not assign null to token
and instead either set an explicit unauthenticated flag or throw a dedicated
AuthRequiredException (e.g., throw AuthRequiredException() from the refresh
path) so callers stop retrying and require an explicit re-login; update the code
around the withContext(NonCancellable) { token = tokenRefresher.refresh() } call
in AuthProviderImpl to perform this null-check and take the terminal action.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt`:
- Around line 27-30: The current refresh logic in AuthProviderImpl assigns
tokenRefresher.refresh() directly to token which allows a null to persist and
triggers repeated refresh attempts; change the block inside the oldToken ==
token branch so that after calling tokenRefresher.refresh() you check the result
and if it is null treat it as a terminal auth state: do not assign null to token
and instead either set an explicit unauthenticated flag or throw a dedicated
AuthRequiredException (e.g., throw AuthRequiredException() from the refresh
path) so callers stop retrying and require an explicit re-login; update the code
around the withContext(NonCancellable) { token = tokenRefresher.refresh() } call
in AuthProviderImpl to perform this null-check and take the terminal action.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 569e5153-aa30-4ffc-8caa-1568df41d64c

📥 Commits

Reviewing files that changed from the base of the PR and between 428cb7d and 3f39c70.

📒 Files selected for processing (1)
  • shared/auth/src/commonMain/kotlin/kmp/shared/auth/data/provider/AuthProviderImpl.kt

@JuliaJakubcova JuliaJakubcova merged commit ac64be1 into main Mar 5, 2026
5 checks passed
@JuliaJakubcova JuliaJakubcova deleted the fix/kmp/auth-token-refresh branch March 5, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants