Repository: soofstad/react-oauth2-pkce
Pull request #168 (2024-05-30 08:42:11)

Feat: add support to refresh token absolute lifetime
## What does this pull request change?

This pull request adds a new configuration option, `refreshTokenExpiryStrategy`. This option allows users to specify the refresh token expiration strategy, choosing between:

'**renewable**': The refresh token's expiration time is renewed each time it is used. 
'**absolute**': The refresh token's expiration time is fixed from its initial issuance and does not change.


## Why is this pull request needed?

We are currently using Auth0 with Absolute Expiration. Auth0 implements an absolute expiration strategy for refresh tokens, meaning the expiration time is fixed from the initial issuance and does not renew with each use. However, the library was designed with the assumption that the refresh token expiration would be renewed each time a new refresh token is requested.

This mismatch between the actual expiration behavior and the library's expectations is causing issues. The library incorrectly assumes the refresh token has a longer validity period than it actually does, leading to potential authentication failures when the refresh token expires earlier than anticipated.

By introducing the refreshTokenExpiryStrategy configuration option, we can ensure that the library can accurately handle both renewable and absolute expiration strategies, preventing such mismatches and improving compatibility with different OAuth2 providers like Auth0.

[Configure Refresh Token Expiration - Absolute Lifetime](https://auth0.com/docs/secure/tokens/refresh-tokens/configure-refresh-token-expiration) 

![image](https://github.com/soofstad/react-oauth2-pkce/assets/109530790/b0b2e26f-8c28-4344-a93e-f3d1103305da)

## Issues related to this change
https://github.com/soofstad/react-oauth2-pkce/discussions/167



Patch:
diff --git a/README.md b/README.md
index 80db753..408f7e0 100644
--- a/README.md
+++ b/README.md
@@ -164,6 +164,10 @@ type TAuthConfig = {
   tokenExpiresIn?: number // default: null
   // Can be used if auth provider doesn't return refresh token expiration time in token response
   refreshTokenExpiresIn?: number // default: null
+  // Defines the expiration strategy for the refresh token.
+  // - 'renewable': The refresh token's expiration time is renewed each time it is used, getting a new validity period.
+  // - 'absolute': The refresh token's expiration time is fixed from its initial issuance and does not change, regardless of how many times it is used.
+  refreshTokenExpiryStrategy?: 'renewable' | 'absolute' // default: renewable
   // Whether or not to post 'scope' when refreshing the access token
   refreshWithScope?: boolean // default: true
 }
diff --git a/src/AuthContext.tsx b/src/AuthContext.tsx
index 6d150ae..aae6b49 100644
--- a/src/AuthContext.tsx
+++ b/src/AuthContext.tsx
@@ -123,7 +123,9 @@ export const AuthProvider = ({ authConfig, children }: IAuthProvider) => {
     const refreshTokenExpiresIn = config.refreshTokenExpiresIn ?? getRefreshExpiresIn(tokenExpiresIn, response)
     if (response.refresh_token) {
       setRefreshToken(response.refresh_token)
-      setRefreshTokenExpire(epochAtSecondsFromNow(refreshTokenExpiresIn))
+      if (!refreshTokenExpire || config.refreshTokenExpiryStrategy !== 'absolute') {
+        setRefreshTokenExpire(epochAtSecondsFromNow(refreshTokenExpiresIn))
+      }
     }
   }
 
diff --git a/src/authConfig.ts b/src/authConfig.ts
index 0464372..178083f 100644
--- a/src/authConfig.ts
+++ b/src/authConfig.ts
@@ -18,6 +18,7 @@ export function createInternalConfig(passedConfig: TAuthConfig): TInternalConfig
     storage = 'local',
     storageKeyPrefix = 'ROCP_',
     refreshWithScope = true,
+    refreshTokenExpiryStrategy = 'renewable',
   }: TAuthConfig = passedConfig
 
   const config: TInternalConfig = {
@@ -32,6 +33,7 @@ export function createInternalConfig(passedConfig: TAuthConfig): TInternalConfig
     storage: storage,
     storageKeyPrefix: storageKeyPrefix,
     refreshWithScope: refreshWithScope,
+    refreshTokenExpiryStrategy: refreshTokenExpiryStrategy,
   }
   validateConfig(config)
   return config
diff --git a/src/types.ts b/src/types.ts
index fd5f214..10c42ad 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -41,7 +41,7 @@ export interface IAuthProvider {
 
 export interface IAuthContext {
   token: string
-  logIn: (state?: string, additionalParameters?: TPrimitiveRecord) => void
+  logIn: (state?: string, additionalParameters?: TPrimitiveRecord, method?: 'redirect' | 'popup') => void
   logOut: (state?: string, logoutHint?: string, additionalParameters?: TPrimitiveRecord) => void
   /** @deprecated Use `logIn` instead */
   login: (state?: string, additionalParameters?: TPrimitiveRecord) => void
@@ -77,15 +77,16 @@ export type TAuthConfig = {
   extraLogoutParameters?: TPrimitiveRecord
   tokenExpiresIn?: number
   refreshTokenExpiresIn?: number
+  refreshTokenExpiryStrategy?: 'renewable' | 'absolute'
   storage?: 'session' | 'local'
   storageKeyPrefix?: string
   refreshWithScope?: boolean
 }
 
 export type TRefreshTokenExpiredEvent = {
-  logIn: () => void
+  logIn: (state?: string, additionalParameters?: TPrimitiveRecord, method?: 'redirect' | 'popup') => void
   /** @deprecated Use `logIn` instead. Will be removed in a future version. */
-  login: () => void
+  login: (state?: string, additionalParameters?: TPrimitiveRecord, method?: 'redirect' | 'popup') => void
 }
 
 // The AuthProviders internal config type. All values will be set by user provided, or default values
@@ -111,6 +112,7 @@ export type TInternalConfig = {
   extraLogoutParameters?: TPrimitiveRecord
   tokenExpiresIn?: number
   refreshTokenExpiresIn?: number
+  refreshTokenExpiryStrategy: 'renewable' | 'absolute'
   storage: 'session' | 'local'
   storageKeyPrefix: string
   refreshWithScope: boolean


Test patch:
diff --git a/tests/auth-util.test.ts b/tests/auth-util.test.ts
index c94ccda..90a14ff 100644
--- a/tests/auth-util.test.ts
+++ b/tests/auth-util.test.ts
@@ -14,6 +14,7 @@ const authConfig: TInternalConfig = {
   scope: 'someScope openid',
   clearURL: false,
   storage: 'local',
+  refreshTokenExpiryStrategy: 'renewable',
   storageKeyPrefix: 'ROCP_',
   refreshWithScope: true,
   extraAuthParams: {