Skip to main content

Mobile

Building a Reliable Client-Side Token Management System in Flutter

Client-Side Token Management

In one of my recent Flutter projects, I had to implement a session token mechanism that behaved very differently from standard JWT-based authentication systems.

The backend issued a 15-minute session token, but with strict constraints:

  • No expiry timestamp was provided
  • The server extended the session only when the app made an API call
  • Long-running user workflows depended entirely on session continuity

If the session expired unexpectedly, users could lose progress mid-flow, leading to inconsistent states and broken experiences. This meant the entire token lifecycle had to be controlled on the client, in a predictable and self-healing way.

This is the architecture I designed.


  1. The Core Challenge

The server provided the token but not its expiry. The only rule:

“Token is valid for 15 minutes, and any API call extends the session.”

To protect long-running user interactions, the application needed to:

  • Track token lifespan locally
  • Refresh or extend sessions automatically
  • Work uniformly across REST and GraphQL
  • Survive app backgrounding and resuming
  • Preserve in-progress workflows without UI disruption

This required a fully client-driven token lifecycle engine.


  1. Client-Side Countdown Timer

Since expiry data was not available from the server, I implemented a local countdown timer to represent session validity.

How it works:

  • When token is obtained → start a 15-minute timer
  • When any API call happens → reset the timer (because backend extends session)
  • If the timer is about to expire:
    • Active user flow → show a visible countdown
    • Passive or static screens → attempt silent refresh
  • If refresh fails → gracefully log out in case of logged-in users

This timer became the foundation of the entire system.

 

Blank Diagram (3)


  1. Handling App Lifecycle Transitions

Users frequently minimize or switch apps. To maintain session correctness:

  • On background: pause the timer and store timestamp
  • On resume: calculate elapsed background time
    • If still valid → refresh & restart timer
    • If expired → re-authenticate or log out

This prevented accidental session expiry just because the app was minimized.

Blank Diagram (4)

 


  1. REST Auto-Refresh with Dio Interceptors

For REST APIs, Dio interceptors provided a clean, centralized way to manage token refresh.

Interceptor Responsibilities:

  • If timer is null → start timer
  • If timer exists but is inactive,
    • token expired → refresh token
    • perform silent re-login if needed
  • If timer is active → reset the timer
  • Inject updated token into headers

Conceptual Implementation:

class SessionInterceptor extends Interceptor {

  @override

  Future<void> onRequest(

    RequestOptions options,

    RequestInterceptorHandler handler,

  ) async {

    if (sessionTimer == null) {

      startSessionTimer();

    } else if (!sessionTimer.isActive) {

      await refreshSession();

      if (isAuthenticatedUser) {

        await silentReauthentication();

      }

    }

    options.headers[‘Authorization’] = ‘Bearer $currentToken’;

    resetSessionTimer();

    handler.next(options);

  }

}

This made REST calls self-healing, with no manual checks in individual services.


  1. GraphQL Auto-Refresh with Custom AuthLink

GraphQL required custom handling because it doesn’t support interceptors.
I implemented a custom AuthLink where token management happened inside getToken().

AuthLink Responsibilities:

  • Timer null → start
  • Timer inactive,
    • refresh token
    • update storage
    • silently re-login if necessary
  • Timer active → reset timer and continue

GraphQL operations then behaved consistently with REST, including auto-refresh and retry.

Conceptual implementation:

class CustomAuthLink extends AuthLink {

  CustomAuthLink()

      : super(

          getToken: () async {

            if (sessionTimer == null) {

              startSessionTimer();

              return currentToken;

            }

            if (!sessionTimer.isActive) {

              await refreshSession();

              if (isAuthenticatedUser) {

                await silentReauthentication();

              }

              return currentToken;

            }

            resetSessionTimer();

            return currentToken;

          },

        );

}


  1. Silent Session Extension for Authenticated Users

When authenticated users’ sessions extended:

  • token refresh happened in background
  • user data was re-synced silently
  • no screens were reset
  • no interruptions were shown

This was essential for long-running user workflows.


Engineering Lessons Learned

  • When token expiry information is not provided by the backend, session management must be treated as a first-class client responsibility rather than an auxiliary concern. Deferring this logic to individual API calls or UI layers leads to fragmentation and unpredictable behavior.
  • A client-side timer, when treated as the authoritative representation of session validity, significantly simplifies the overall design. By anchoring all refresh, retry, and termination decisions to a single timing mechanism, the system becomes easier to reason about, test, and maintain.
  • Application lifecycle events have a direct and often underestimated impact on session correctness. Explicitly handling backgrounding and resumption prevents sessions from expiring due to inactivity that does not reflect actual user intent or engagement.
  • Centralizing session logic for REST interactions through a global interceptor reduces duplication and eliminates inconsistent implementations across services. This approach ensures that every network call adheres to the same session rules without requiring feature-level awareness.
  • GraphQL requires a different integration point, but achieving behavioral parity with REST is essential. Embedding session handling within a custom authorization link proved to be the most reliable way to enforce consistent session behavior across both communication models.
  • Silent session extension for authenticated users is critical for preserving continuity during long-running interactions. Refreshing sessions transparently avoids unnecessary interruptions and prevents loss of in-progress work.
  • In systems where backend constraints limit visibility into session expiry, a client-driven lifecycle model is not merely a workaround. It is a necessary architectural decision that improves reliability, protects user progress, and provides predictable behavior under real-world usage conditions.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Soundariya Thirunavukkarasu

Soundariya is a Mobile Application Developer at Perficient, currently working on the Sun Country Mobile App project. She has strong expertise in Flutter, Riverbed, GraphQL, Android, and Xamarin. She is passionate about learning new technologies, mentoring teams, and creating user-centric mobile experiences.

More from this Author

Categories
Follow Us