Skip to content

Conversation

@evereq
Copy link
Member

@evereq evereq commented Nov 14, 2025

PR

Please note: we will close your PR without comment if you do not check the boxes above and provide ALL requested information.


samuelmbabhazi and others added 15 commits November 10, 2025 08:50
- Added expiresIn option to sign() method in getJwtAccessToken()
- Access tokens now expire after JWT_TOKEN_EXPIRATION_TIME (default: 1 day)
- This aligns with the behavior of generateToken() method
- Fixes issue where access tokens from /auth/login and /auth/refresh-token never expired
…tion

fix: add expiration time to JWT access token in getJwtAccessToken()
* feat(desktop-ui-lib): add token refresh service and interceptors; update auth services

* chore(desktop): wire auth/interceptors in app module and component

* feat(auth): add refresh token system

Implement a comprehensive refresh token system for seamless authentication and extended user sessions.
This change introduces:
- `RefreshTokenInterceptor`: Automatically handles 401 responses by refreshing expired access tokens, queuing concurrent requests, and retrying original requests.
- `TokenRefreshService`: Proactively refreshes tokens 5 minutes before expiration to prevent user-facing 401s.
- Updates to `AuthStrategyService` and `StoreService`: Manage token rotation, securely store refresh tokens, and track JWT expiry timestamps.
- Refined `UnauthorizedInterceptor`: Delegates 401 handling to the refresh interceptor, ensuring proper logout only when the refresh token is invalid or unavailable.
- Integration into desktop and agent applications: The new interceptors and services are registered and initialized.
This system provides a production-ready solution for managing token lifecycles, enhancing security, and improving user experience by minimizing login interruptions.
Extensive documentation for the implementation is included in `REFRESH_TOKEN_QUICKSTART.md`, `REFRESH_TOKEN_SUMMARY.md`, and `packages/desktop-ui-lib/REFRESH_TOKEN_IMPLEMENTATION.md`.

* refactor(token-refresh): simplify token refresh service API

Rename `startTokenRefreshTimer` to `start` and
`stopTokenRefreshTimer` to `stop` in the `TokenRefreshService`.
This change improves method brevity and consistency.
Update all call sites in `AppComponent` for both agent and desktop
applications to reflect the new method names.
Remove `REFRESH_TOKEN_QUICKSTART.md`, `REFRESH_TOKEN_SUMMARY.md`,
and `packages/desktop-ui-lib/REFRESH_TOKEN_IMPLEMENTATION.md`.
These documentation files are no longer current or have been
consolidated into other documentation.

* feat(auth): implement token refresh mechanism

Integrates TokenRefreshService to automatically renew authentication tokens.
Starts the refresh timer on application initialization for existing sessions
and after successful social login. Registers RefreshTokenInterceptor to
handle token refreshing for outgoing HTTP requests, ensuring continuous
user session without manual re-authentication.

* refactor(interceptors): centralize auth endpoint patterns

Extract authentication endpoint paths into a dedicated `AUTH_ENDPOINTS` constant.
This change reduces duplication and improves maintainability by providing a single source of truth for endpoints that should be excluded from automatic token refresh and unauthorized redirect logic.
Updated `RefreshTokenInterceptor` and `UnauthorizedInterceptor` to utilize this new constant.
Includes minor formatting improvements in `app.module.ts`.

* refactor(interceptors): reorder RefreshTokenInterceptor

Move RefreshTokenInterceptor after UnauthorizedInterceptor.
This ensures that unauthorized responses are processed by the
UnauthorizedInterceptor first, allowing for subsequent token
refresh attempts in the correct order within the HTTP interceptor
chain.
* feat: add automatic token refresh interceptor for web app

- Created AuthRefreshInterceptor to handle 401 errors and automatically refresh access tokens
- Prevents users from being logged out when access tokens expire (after 1 day)
- Uses BehaviorSubject pattern to coordinate multiple simultaneous requests during token refresh
- Registered interceptor in Gauzy web app module
- Skips refresh for auth endpoints to avoid infinite loops
- Falls back to error if refresh token is missing or refresh fails

This interceptor is necessary now that access tokens have expiration time.
Without it, users would be disconnected every 24 hours when tokens expire.

* remove unmecessary comment

* fix authendpoints

* feat: add automatic token refresh interceptor for web app

- Created AuthRefreshInterceptor to handle 401 errors and automatically refresh access tokens
- Prevents users from being logged out when access tokens expire
- Uses BehaviorSubject pattern to coordinate multiple simultaneous requests during token refresh
- Registered interceptor in Gauzy web app module
- Excludes only public auth endpoints to avoid infinite loops
- Protected endpoints like /auth/authenticated, /auth/role, /auth/permissions will trigger refresh
- Clears tokens on refresh failure to allow proper logout flow

This interceptor is necessary now that access tokens have expiration time.
Without it, users would be disconnected every 24 hours when tokens expire.
* fix: agent refresh token handled

* fix: remove unnecessary async

* fix: wrong type of refreshtoken
* fix: reset refreshTokenSubject on token refresh failure

- Added refreshTokenSubject.next(null) in catchError block when refresh fails
- Ensures pending requests are unblocked and notified of refresh failure
- Prevents requests from waiting indefinitely when refresh token is invalid
- Maintains proper cleanup sequence: clear tokens -> reset subject -> throw error

* fix: properly handle token refresh failure with error emission and login redirect

- Changed from BehaviorSubject<string | null> to Subject<string> to properly handle errors
- Emit error through subject on refresh failure to unblock all waiting requests
- Added automatic redirect to login page when refresh token fails
- Create new Subject after error to allow future refresh attempts
- Removed filter for non-null values that was causing requests to hang indefinitely

This fixes the critical bug where pending requests would hang forever when refresh token
is invalid or expired. Now all waiting requests are properly notified of the failure and
users are redirected to the login page.

* refactor: use inject() function for dependency injection in AuthRefreshInterceptor

- Replaced constructor injection with modern inject() function pattern
- Removed Injector dependency in favor of direct inject() calls
- Simplified refreshAccessToken() to use injected authService directly
- Aligns with Angular best practices and modern dependency injection patterns
[FIX] remove `strict`  and use only `lax` to avoid the oauth flow to be restrictive(Avoiding crsf issue)
feat: migrate GitHub workflows to Ubicloud/Warp runners
Bumps [min-document](https://github.com/Raynos/min-document) from 2.19.0 to 2.19.2.
- [Commits](Raynos/min-document@v2.19.0...v2.19.2)

---
updated-dependencies:
- dependency-name: min-document
  dependency-version: 2.19.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
…ument-2.19.2

chore(deps): bump min-document from 2.19.0 to 2.19.2
* feat(redis): improve Redis connection configuration

- Add pingInterval (30s) to prevent LB/firewall disconnections
- Add keepAlive (10s) for TCP keepalive
- Add reconnectStrategy with exponential backoff (1s-5s)
- Add connectTimeout (10s) for connection stability
- Apply improvements to session store, cache store, and health checks
- Optimize for DigitalOcean Valkey 8 compatibility

Resolves #9162 (step 4)

* fix typos

* fix(redis): implement true exponential backoff in reconnectStrategy

- Replace linear progression (1000 + retries * 200) with exponential backoff (1000 * 2^retries)
- Retry delays now: 1000ms, 2000ms, 4000ms, 5000ms (capped)
- Previous formula was arithmetic, not exponential as claimed in comment
- Apply fix to session store, cache store, and health checks

Resolves #9162 (reconnectStrategy fix)

* feat: migrate from cache-manager-redis-yet to @keyv/redis with production-ready options

- Replace outdated cache-manager-redis-yet with modern @keyv/redis adapter
- Create new CacheModule with Keyv Redis integration
- Add production-ready Redis connection options:
  - keepAlive: 10_000ms for TCP keepalive
  - reconnectStrategy with exponential backoff (1s-5s)
  - connectTimeout: 10_000ms for connection stability
  - pingInterval: 30_000ms to prevent LB/firewall disconnections
  - isolationPoolOptions with connection pooling (min: 1, max: 100)
  - TLS support for secure connections
- Configure Keyv options for optimal performance:
  - namespace: 'gauzy-cache' for key organization
  - useUnlink: true for better performance (UNLINK vs DEL)
  - clearBatchSize: 1000 for batch operations
  - throwOnConnectError: true for error handling
- Create CacheService with get/set/delete/clear methods
- Update app.module.ts to use new CacheModule
- Remove cache-manager-redis-yet dependency from package.json

* fix cspell

* fix redis config

* fix ttl config

* fix ttl config

* remove unused import

* chore: sanitize .env.sample - remove sensitive credentials

* Refactor cache service

* refactor(cache): migrate from cache-manager-redis-yet to cacheable with 2-layer non-blocking cache

- Replace cache-manager-redis-yet with cacheable package
- Implement 2-layer caching (in-memory L1 + Redis L2)
- Configure non-blocking mode for Redis operations
- Add Redis URL validation to prevent invalid URLs
- Remove custom cache module in favor of NestJS cache-manager integration
- Add fallback to in-memory cache if Redis connection fails

Follows NestJS caching documentation and cacheable best practices:
https://docs.nestjs.com/techniques/caching
https://cacheable.org/docs/cacheable/#non-blocking-with-keyvredis

* refactor(cache): simplify cache configuration using createKeyvNonBlocking helper

- Use createKeyvNonBlocking() helper function from @keyv/redis for non-blocking Redis setup
- Remove manual Redis client configuration (disableOfflineQueue, reconnectStrategy, event listeners)
- Remove explicit primary cache configuration (Cacheable manages in-memory LRU by default)
- Replace 'as any' type cast with factory function for type safety
- Simplify code from ~120 lines to ~40 lines while maintaining same functionality

Benefits:
- Automatic non-blocking configuration (disableOfflineQueue: true, reconnectStrategy: false, throwOnConnectError: false)
- Type-safe cache-manager integration without 'as any' cast
- Cleaner, more maintainable code following cacheable best practices
- Default in-memory LRU cache managed by Cacheable (Layer 1)
- Non-blocking Redis cache for distributed persistence (Layer 2)

Follows cacheable documentation:
https://cacheable.org/docs/cacheable/#non-blocking-with-keyvredis

* fix(cache): improve security and error handling

Security improvements:
- Remove credential logging: Replace plain-text Redis URL logging with sanitized connection info
- Log only host, port, and protocol (rediss/redis) without username/password
- Prevent credential exposure in application logs (compliance & security best practice)

Error handling improvements:
- Fix invalid fallback: Replace 'store: undefined' with proper in-memory config
- Add try-catch around Redis initialization to handle connection failures gracefully
- Return '{ isGlobal: true }' for in-memory fallback instead of broken 'store: undefined'
- Ensure cache operations continue to work even when Redis is unavailable

Before (security risk):
  console.log('REDIS_URL: ', url); // Logs: redis://user:password@host:6379

After (secure):
  console.log('Redis Cache: Connecting to redis://host:6379'); // No credentials

Before (broken fallback):
  return { store: undefined }; // Cache operations will fail

After (working fallback):
  return { isGlobal: true }; // In-memory cache works correctly

* fix 2layer cache

* add primary store

* fix config

* fix return

* fix deeepscan

* feat: add comprehensive Redis configuration properties for cache consistency

- Parse Redis URL to extract username, password, host, port
- Add conditional socket configuration based on TLS/TCP mode
- TLS mode: tls, passphrase, rejectUnauthorized, connectTimeout
- TCP mode: keepAlive, keepAliveInitialDelay, connectTimeout
- Add pingInterval for connection keep-alive
- Maintain consistency with RedisHealthIndicator configuration
- Respect non-blocking mode constraints (reconnectStrategy omitted)

* fix parse

* fix parse

* add default port

* improve parse int

* fix comment
* fix: remove  and use only lax to avoid the oauth flow to be restrictive

* feat: migrate GitHub workflows to Ubicloud/Warp runners and normalize YAML quoting

* chore(deps): bump min-document from 2.19.0 to 2.19.2

Bumps [min-document](https://github.com/Raynos/min-document) from 2.19.0 to 2.19.2.
- [Commits](Raynos/min-document@v2.19.0...v2.19.2)

---
updated-dependencies:
- dependency-name: min-document
  dependency-version: 2.19.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

* feat(redis): migrate from cache-manager-redis-yet to @keyv/redis (#9172)

* feat(redis): improve Redis connection configuration

- Add pingInterval (30s) to prevent LB/firewall disconnections
- Add keepAlive (10s) for TCP keepalive
- Add reconnectStrategy with exponential backoff (1s-5s)
- Add connectTimeout (10s) for connection stability
- Apply improvements to session store, cache store, and health checks
- Optimize for DigitalOcean Valkey 8 compatibility

Resolves #9162 (step 4)

* fix typos

* fix(redis): implement true exponential backoff in reconnectStrategy

- Replace linear progression (1000 + retries * 200) with exponential backoff (1000 * 2^retries)
- Retry delays now: 1000ms, 2000ms, 4000ms, 5000ms (capped)
- Previous formula was arithmetic, not exponential as claimed in comment
- Apply fix to session store, cache store, and health checks

Resolves #9162 (reconnectStrategy fix)

* feat: migrate from cache-manager-redis-yet to @keyv/redis with production-ready options

- Replace outdated cache-manager-redis-yet with modern @keyv/redis adapter
- Create new CacheModule with Keyv Redis integration
- Add production-ready Redis connection options:
  - keepAlive: 10_000ms for TCP keepalive
  - reconnectStrategy with exponential backoff (1s-5s)
  - connectTimeout: 10_000ms for connection stability
  - pingInterval: 30_000ms to prevent LB/firewall disconnections
  - isolationPoolOptions with connection pooling (min: 1, max: 100)
  - TLS support for secure connections
- Configure Keyv options for optimal performance:
  - namespace: 'gauzy-cache' for key organization
  - useUnlink: true for better performance (UNLINK vs DEL)
  - clearBatchSize: 1000 for batch operations
  - throwOnConnectError: true for error handling
- Create CacheService with get/set/delete/clear methods
- Update app.module.ts to use new CacheModule
- Remove cache-manager-redis-yet dependency from package.json

* fix cspell

* fix redis config

* fix ttl config

* fix ttl config

* remove unused import

* chore: sanitize .env.sample - remove sensitive credentials

* Refactor cache service

* refactor(cache): migrate from cache-manager-redis-yet to cacheable with 2-layer non-blocking cache

- Replace cache-manager-redis-yet with cacheable package
- Implement 2-layer caching (in-memory L1 + Redis L2)
- Configure non-blocking mode for Redis operations
- Add Redis URL validation to prevent invalid URLs
- Remove custom cache module in favor of NestJS cache-manager integration
- Add fallback to in-memory cache if Redis connection fails

Follows NestJS caching documentation and cacheable best practices:
https://docs.nestjs.com/techniques/caching
https://cacheable.org/docs/cacheable/#non-blocking-with-keyvredis

* refactor(cache): simplify cache configuration using createKeyvNonBlocking helper

- Use createKeyvNonBlocking() helper function from @keyv/redis for non-blocking Redis setup
- Remove manual Redis client configuration (disableOfflineQueue, reconnectStrategy, event listeners)
- Remove explicit primary cache configuration (Cacheable manages in-memory LRU by default)
- Replace 'as any' type cast with factory function for type safety
- Simplify code from ~120 lines to ~40 lines while maintaining same functionality

Benefits:
- Automatic non-blocking configuration (disableOfflineQueue: true, reconnectStrategy: false, throwOnConnectError: false)
- Type-safe cache-manager integration without 'as any' cast
- Cleaner, more maintainable code following cacheable best practices
- Default in-memory LRU cache managed by Cacheable (Layer 1)
- Non-blocking Redis cache for distributed persistence (Layer 2)

Follows cacheable documentation:
https://cacheable.org/docs/cacheable/#non-blocking-with-keyvredis

* fix(cache): improve security and error handling

Security improvements:
- Remove credential logging: Replace plain-text Redis URL logging with sanitized connection info
- Log only host, port, and protocol (rediss/redis) without username/password
- Prevent credential exposure in application logs (compliance & security best practice)

Error handling improvements:
- Fix invalid fallback: Replace 'store: undefined' with proper in-memory config
- Add try-catch around Redis initialization to handle connection failures gracefully
- Return '{ isGlobal: true }' for in-memory fallback instead of broken 'store: undefined'
- Ensure cache operations continue to work even when Redis is unavailable

Before (security risk):
  console.log('REDIS_URL: ', url); // Logs: redis://user:password@host:6379

After (secure):
  console.log('Redis Cache: Connecting to redis://host:6379'); // No credentials

Before (broken fallback):
  return { store: undefined }; // Cache operations will fail

After (working fallback):
  return { isGlobal: true }; // In-memory cache works correctly

* fix 2layer cache

* add primary store

* fix config

* fix return

* fix deeepscan

* feat: add comprehensive Redis configuration properties for cache consistency

- Parse Redis URL to extract username, password, host, port
- Add conditional socket configuration based on TLS/TCP mode
- TLS mode: tls, passphrase, rejectUnauthorized, connectTimeout
- TCP mode: keepAlive, keepAliveInitialDelay, connectTimeout
- Add pingInterval for connection keep-alive
- Maintain consistency with RedisHealthIndicator configuration
- Respect non-blocking mode constraints (reconnectStrategy omitted)

* fix parse

* fix parse

* add default port

* improve parse int

* fix comment

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: rolandm99 <[email protected]>
Co-authored-by: Paradoxe Ngwasi <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: samuel mbabhazi <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch stage

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.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
6 out of 7 committers have signed the CLA.

✅ samuelmbabhazi
✅ evereq
✅ syns2191
✅ RolandM99
✅ paradoxe35
✅ adkif
❌ dependabot[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@evereq evereq merged commit f941fb4 into stage-apps Nov 14, 2025
16 of 20 checks passed
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 14, 2025

Greptile Overview

Greptile Summary

This PR implements comprehensive token refresh functionality across web and desktop applications, and migrates Redis caching from cache-manager-redis-yet to @keyv/redis.

Key Changes

  • Token Refresh for Desktop Apps: Added proactive token refresh service that checks token expiry every minute and refreshes tokens 5 minutes before expiration
  • Reactive Token Refresh: Implemented interceptors that automatically refresh tokens on 401 errors for both web (AuthRefreshInterceptor) and desktop (RefreshTokenInterceptor) applications
  • Store Updates: Added refreshToken, tokenExpiresAt properties and isTokenExpired() method to track token expiry across desktop and web stores
  • Redis Migration: Replaced cache-manager-redis-yet with @keyv/redis implementing 2-layer caching (in-memory L1 + Redis L2) with non-blocking mode and comprehensive error handling
  • Workflow Updates: Extensive GitHub Actions workflow updates (appears to be formatting/reorganization changes)

Technical Implementation

The authentication flow now supports both proactive (desktop-only, timer-based) and reactive (all apps, 401-based) token refresh strategies with proper request queueing to prevent duplicate refresh attempts.

Confidence Score: 4/5

  • This PR is generally safe to merge with one minor syntax issue that should be addressed
  • The implementation follows best practices for token refresh with proper request queueing, offline handling, and auth endpoint exclusion. The Redis migration includes comprehensive error handling and fallback mechanisms. However, there's a syntax issue with using atob in Node.js/Electron context that needs correction. The extensive workflow changes appear to be formatting updates with no functional impact.
  • Pay attention to packages/desktop-ui-lib/src/lib/auth/services/auth-strategy.service.ts - the atob usage needs to be replaced with Buffer for Node.js compatibility

Important Files Changed

File Analysis

Filename Score Overview
packages/desktop-ui-lib/src/lib/interceptors/refresh-token.interceptor.ts 4/5 New interceptor for reactive token refresh on 401 errors with proper request queueing and offline handling
packages/ui-core/core/src/lib/interceptors/auth-refresh.interceptor.ts 4/5 New interceptor for web app token refresh on 401 errors with comprehensive auth endpoint exclusion list
packages/desktop-ui-lib/src/lib/auth/services/auth-strategy.service.ts 3/5 Implements refresh token storage, JWT expiry parsing with atob, and proactive token refresh logic
packages/desktop-ui-lib/src/lib/services/store.service.ts 5/5 Added refreshToken, tokenExpiresAt properties and isTokenExpired() method with 5-minute buffer
packages/core/src/lib/app/app.module.ts 4/5 Migrated from cache-manager-redis-yet to @keyv/redis with 2-layer caching (in-memory L1 + Redis L2) and proper error handling

Sequence Diagram

sequenceDiagram
    participant User
    participant App
    participant TokenInterceptor
    participant RefreshInterceptor
    participant AuthService
    participant Store
    participant API
    
    Note over User,API: Initial Login Flow
    User->>App: Login with credentials
    App->>AuthService: authenticate(email, password)
    AuthService->>API: POST /auth/login
    API-->>AuthService: {token, refresh_token, user}
    AuthService->>Store: Save token, refresh_token, tokenExpiresAt
    AuthService-->>App: Success
    
    Note over User,API: Token Refresh - Proactive (Desktop Apps)
    loop Every 1 minute
        Store->>Store: Check isTokenExpired()
        alt Token expiring within 5 min
            Store->>AuthService: refreshToken()
            AuthService->>API: POST /auth/refresh-token
            API-->>AuthService: {token}
            AuthService->>Store: Update token, tokenExpiresAt
        end
    end
    
    Note over User,API: Token Refresh - Reactive (401 Handling)
    User->>App: Make API request
    App->>TokenInterceptor: Add Bearer token
    TokenInterceptor->>API: Request with token
    API-->>RefreshInterceptor: 401 Unauthorized
    RefreshInterceptor->>Store: Get refresh_token
    RefreshInterceptor->>AuthService: refreshToken()
    AuthService->>API: POST /auth/refresh-token
    API-->>AuthService: {token}
    AuthService->>Store: Update token
    RefreshInterceptor->>API: Retry original request
    API-->>User: Success response
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

89 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +262 to +263
console.warn('Failed to parse token expiry, using default 1 hour:', error);
// Default to 1 hour
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: atob may not be available in all Electron/Node.js contexts

Suggested change
console.warn('Failed to parse token expiry, using default 1 hour:', error);
// Default to 1 hour
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/desktop-ui-lib/src/lib/auth/services/auth-strategy.service.ts
Line: 262:263

Comment:
**syntax:** `atob` may not be available in all Electron/Node.js contexts

```suggestion
			const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
```

How can I resolve this? If you propose a fix, please make it concise.

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.

8 participants