Skip to content
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

Add support for proactive token validation #110

Open
AristideVB opened this issue Nov 20, 2024 · 3 comments · May be fixed by #112
Open

Add support for proactive token validation #110

AristideVB opened this issue Nov 20, 2024 · 3 comments · May be fixed by #112
Assignees
Labels
enhancement New feature or request

Comments

@AristideVB
Copy link

AristideVB commented Nov 20, 2024

Description:

The Fresh package currently supports reactive token refresh via the shouldRefresh callback, which is triggered after requests. While this approach works well, many use cases (e.g., OAuth2 with expiresIn) can benefit from proactive token validation, where tokens are validated and refreshed before requests are made.

Proactively validating tokens:

  • Prevents sending expired tokens, reducing unnecessary backend errors (e.g., AUTH_NOT_AUTHENTICATED in GraphQL, 401 Unauthorized or invalid_token in REST APIs)
  • Improves efficiency by avoiding retries

This feature request proposes two potential solutions to enable proactive token validation in Fresh.


Proposed Solutions:

Here’s the updated GitHub issue with Option 1 (developer-managed logic) and Option 2 (built-in expiration handling with refreshIfExpired and enhanced OAuth2Token).

Option 1: Developer-Managed Expiration Logic

Introduce a validateTokenBeforeRequest callback, giving developers complete control over token expiration handling.

Example:

Fresh<OAuth2Token>(
  tokenStorage: tokenStorage,
  refreshToken: (token, client) async {
    // Refresh token logic
  },
 shouldRefresh: (response) {
 // GraphQL or REST should refresh logic
        return response.errors?.first.extensions?['code'] ==
            'AUTH_NOT_AUTHENTICATED';
      },
  validateTokenBeforeRequest: (token) async {
    final now = DateTime.now().millisecondsSinceEpoch / 1000;
    final issuedAt = await getIssuedAtFromStorage(); // Developer handles issuedAt
    return token.expiresIn != null && issuedAt != null
        ? now >= (issuedAt + token.expiresIn!)
        : false;
  },
);

How It Works:

  1. Fresh calls validateTokenBeforeRequest before every request.
  2. Developers define custom expiration logic, such as using expiresIn and a custom issuedAt field.
  3. If the token is expired, Fresh triggers the refreshToken callback to refresh it.

Pros:

  • Provides maximum flexibility.
  • Requires no changes to OAuth2Token or Fresh internals.
  • Fully backward compatible.

Cons:

  • Developers must manage and store issuedAt themselves.

Option 2 Built-In Expiration Handling with Enhanced OAuth2Token

Introduce a refreshIfExpired boolean and enhance OAuth2Token with an optional issuedAt field for built-in expiration logic.

Example:

class OAuth2Token {
  const OAuth2Token({
    required this.accessToken,
    this.tokenType = 'bearer',
    this.expiresIn,
    this.refreshToken,
    this.scope,
    this.issuedAt, // Optional field for token issuance time
  });

  final String accessToken;
  final String? tokenType;
  final int? expiresIn; // Duration in seconds
  final String? refreshToken;
  final String? scope;
  final int? issuedAt; // Timestamp or DateTime when the token was issued
}

Fresh<OAuth2Token>(
  tokenStorage: tokenStorage,
  refreshToken: (token, client) async {
    // Refresh token logic
  },
   shouldRefresh: (response) {
 // GraphQL or REST should refresh logic
        return response.errors?.first.extensions?['code'] ==
            'AUTH_NOT_AUTHENTICATED';
      },
  refreshIfExpired: true, // Enable built-in expiration handling
);

How It Works:

  1. Fresh uses expiresIn and issuedAt (if provided) to calculate the token's expiration time:

    final expirationTime = token.issuedAt + token.expiresIn!;
    if (DateTime.now().millisecondsSinceEpoch / 1000 >= expirationTime) {
      await refreshToken(token);
    }
  2. If the token is expired or nearing expiration, Fresh refreshes it before sending the request.

  3. If issuedAt is not provided, developers can handle expiration manually via shouldRefresh.

Pros:

  • Simple to use: Enable refreshIfExpired and let Fresh handle expiration logic.
  • Customizable: Developers can provide issuedAt for precise expiration handling or rely on default behavior.
  • Fully backward compatible and opt-in.

Cons:

  • Requires modifying the OAuth2Token class to include an optional issuedAt field.
  • Adds slight complexity to Fresh internals.
@AristideVB
Copy link
Author

Option 3: Trigger shouldRefresh both before & after request @felangel

  • Add token to shouldRefresh call back

Example:

Fresh<OAuth2Token>(
  tokenStorage: tokenStorage,
  refreshToken: (token, client) async {
    // Refresh token logic
  },
 shouldRefresh: (response, token) {
    final now = DateTime.now().millisecondsSinceEpoch / 1000;
    final issuedAt = await getIssuedAtFromStorage(); 
   if(token.expiresIn != null && issuedAt != null) {
   if(now >= (issuedAt + token.expiresIn)) return true;
}
 // GraphQL or REST should refresh logic
        return response.errors?.first.extensions?['code'] ==
            'AUTH_NOT_AUTHENTICATED';
      },
);

@felangel
Copy link
Owner

felangel commented Dec 5, 2024

I opened #112 to attempt to refresh expired tokens proactively. Let me know if that addresses your concerns and/or if you have any feedback on the API, thanks! 🙏

@felangel felangel self-assigned this Dec 5, 2024
@felangel felangel added the enhancement New feature or request label Dec 5, 2024
@AristideVB
Copy link
Author

AristideVB commented Dec 5, 2024

Thank you so much for this fast PR @felangel 🙂

Question how would people that want to persist the storage (for example : use FlutterSecureStorage) instead of using InMemoryTokenStorage be able to benefit from this great out of the box refresh mechanism ? They'd want _expiresAt to be stored in FlutterSecureStorage too & not just in cache

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants