-
Notifications
You must be signed in to change notification settings - Fork 18
feat(Middleware): JWT refresh tokens #3856
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
base: importer-rework
Are you sure you want to change the base?
feat(Middleware): JWT refresh tokens #3856
Conversation
[+] Added `revokeRefreshToken` mutation to revoke tokens by setting `revoked_at`. [+] Added `storeRefreshToken` mutation to insert new tokens into the database.
…air` classes to handle token-related data and operations. [~] Updated `AuthQueries` static constructor to initialize refresh token queries.
[~] create table if not exists refresh token for upgrade script 9.0
[+] Token validation on api querys
[~] Updated `Authenticate` method to remove `sessionStorage` dependency. [~] Switched from `string` JWT to `TokenPair` for session restoration. [~] Improved error handling for null `AccessToken` during session restore.
remove unused `sessionStorage` parameter.
…tion in `fworch-texts.sql`. [+] Introduced `AccessTokenLifetimeHours` and `RefreshTokenLifetimeDays` properties in `ConfigData.cs`. [+] Added UI elements in `SettingsDefaults.razor` for configuring token lifetimes with validation.
Introduced a new `RevokeToken` endpoint in `AuthenticationTokenController` to explicitly revoke refresh tokens. Updated `RefreshTokenInfo` to include `created_at` and `revoked_at` fields. Enhanced `TokenService` to revoke tokens during logout and renamed `ClearTokenPair` to `RevokeTokens`. Refactored `UiUserHandler` to dynamically fetch expiration times based on configuration keys. Updated `JwtWriter` and `CreateTokenPair` to use configurable token lifetimes (`AccessTokenLifetimeHours` and `RefreshTokenLifetimeDays`). Improved code readability by replacing `var` with explicit types, enhancing formatting, and adding better error handling. Removed unused imports and added necessary ones for compatibility. CactuseSecurity#1689
Removed `ITokenRefreshService` interface and refactored token management to consolidate operations within `TokenService`. Updated `TokenService` to use record-style constructor syntax, added XML documentation, and introduced new methods for token handling (`SetTokenPair`, `RevokeTokens`). Removed redundant methods and fields, modernized dependency injection, and improved token persistence with `ProtectedSessionStorage`.
Changed exception type in `UiUserHandler.cs` for better clarity and added a fallback mechanism for missing database values. Adjusted `Logout.razor` to ensure proper deauthentication before resource cleanup. Simplified `TokenService` by removing `RefreshAccessToken`, making `Initialize` private, and ensuring token initialization in `IsAccessTokenExpired` and `RevokeTokens`.
Expanded and clarified API authentication documentation in HelpApiLogin.cshtml. Added localized texts for token actions and error messages, and provided detailed examples for login, token refresh, and revoke endpoints, including error scenarios and expected responses. Introduced .small-helpage CSS class for clearer response labeling. Enhances user guidance and internationalization for API authentication.
Removed [Order] attribute from all test classes to allow default test execution order. Minor change in Program.cs regarding JwtBearer usage, but no functional impact. fixed sonarqube csharpsquid:S125 in Program.cs
Changed JWT token retrieval endpoint from /Get to /GetTokenPair/ in test-api.yml and test-auth.yml. Updates both valid and invalid credentials test cases to use the new endpoint.
Removed commented-out and redundant ldapmodify tasks from modify_ldap_tree.yml. The playbook now only includes essential steps for adding or modifying the integration_testuser_jwt entry and for adding sample tenants, operators, and groups. Conditional logic and legacy code for roles and groups have been eliminated, resulting in a more streamlined and maintainable playbook.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements JWT refresh token functionality to improve authentication security and user experience. The implementation adds a two-token system (access + refresh tokens) with automatic token rotation, secure hash-based storage in the database, and client-side token management.
Key changes:
- New refresh token system with database storage using SHA256 hashing
- Automatic token refresh in UI when access tokens expire
- Token rotation on refresh for enhanced security
Reviewed changes
Copilot reviewed 52 out of 53 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
roles/ui/files/FWO.UI/Services/TokenService.cs |
New service managing token lifecycle (refresh, revoke, storage) |
roles/middleware/files/FWO.Middleware.Server/Controllers/AuthenticationTokenController.cs |
New endpoints for token refresh and revocation |
roles/ui/files/FWO.UI/Shared/MainLayout.razor |
Token refresh logic on navigation and initialization |
roles/database/files/upgrade/9.0.sql |
New refresh_tokens table with expiration/revocation tracking |
roles/lib/files/FWO.Data/Middleware/TokenPair.cs |
Data model for access+refresh token pairs |
roles/tests-unit/files/FWO.Test/TokenServiceTest.cs |
Comprehensive unit tests for TokenService |
roles/tests-unit/files/FWO.Test/AuthenticationTokenIntegrationTest.cs |
Integration tests for token workflows |
roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs |
Updated authentication flow to use TokenPair |
roles/lib/files/FWO.Config.Api/Data/ConfigData.cs |
New config parameters for token lifetimes |
Comments suppressed due to low confidence (2)
roles/ui/files/FWO.UI/Pages/Settings/SettingsDefaults.razor:310
- This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
roles/middleware/files/FWO.Middleware.Server/UiUserHandler.cs:184 - This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
foreach (string group in groupsOfUser)
{
string groupName = new DistName(group).Group;
if (!MatchesNamingConvention(groupName, namingConvention))
{
continue; // skip groups that do not match the naming convention
}
FwoOwner? owner = FindOwnerWithMatchingGroupName(groupName, allOwners);
if (owner != null)
{
user.Ownerships.Add(owner.Id);
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) | ||
| { | ||
| if (e == null || string.IsNullOrEmpty(e.UserDn)) | ||
| if (LastLocation != e.Location) | ||
| { | ||
| return; | ||
| LastLocation = e.Location; | ||
|
|
||
| await CheckAndRefreshTokenAsync(); | ||
| } | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OnLocationChanged event handler is marked as async void, which is problematic. Exceptions thrown in async void methods cannot be caught by the caller and will crash the application. This should be changed to async Task and errors should be properly handled. Additionally, there's no unsubscription of this event handler in the Dispose method, which could lead to memory leaks.
| top: calc(var(--bs-custom-modal-margin-top) + 1em - 1px); | ||
| } | ||
|
|
||
| .small-helpage { |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CSS class name contains a typo. "helpage" should be "helptext" or "help-text" to properly indicate this is styling for help text content.
| await refreshSemaphore.WaitAsync(); | ||
|
|
||
| try | ||
| { | ||
| if (!await IsAccessTokenExpired()) | ||
| { | ||
| return true; | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a potential race condition in the token refresh logic. The semaphore is acquired at line 114, but the token expiration is checked again at line 118. However, if multiple concurrent requests pass the initial check at line 154 (before acquiring the semaphore), they will all queue up. When the first request completes the refresh and releases the semaphore, subsequent requests will check expiration again at line 118 and may return true without refreshing (since the token was just refreshed), but this still results in multiple attempts hitting the middleware. Consider adding a check immediately after acquiring the semaphore to verify if another request has already refreshed the token.
| { | ||
| // Spin up local test server using WebApplicationFactory | ||
| Log.WriteInfo("Test Setup", "Creating WebApplicationFactory for local testing"); | ||
| factory = new WebApplicationFactory<Program>() |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disposable 'WebApplicationFactory' is created but not disposed.
| } | ||
| catch (Exception) { } | ||
| Log.WriteError(( title != "" ? title : "Unspecified Error" ), ( message != "" ? message : "Unspecified error occured: " ), exception, userConfig.User.Name, role); | ||
| catch(Exception) { } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Poor error handling: empty catch block.
| Assert.That(ConfigFile.ApiServerUri, Is.Null); | ||
|
|
||
| Assert.DoesNotThrow(() => { var _ = ConfigFile.MiddlewareServerUri; }); | ||
| Assert.DoesNotThrow(() => { var _ = ConfigFile.ApiServerUri; }); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to _ is useless, since its value is never read.
| Assert.Catch(typeof(ApplicationException), () => { var _ = ConfigFile.JwtPublicKey; }); | ||
|
|
||
| Assert.That(ConfigFile.JwtPublicKey, Is.Null); | ||
| Assert.DoesNotThrow(() => { var _ = ConfigFile.JwtPublicKey; }); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to _ is useless, since its value is never read.
| Assert.Catch(typeof(ApplicationException), () => { var _ = ConfigFile.JwtPrivateKey; }); | ||
|
|
||
| Assert.That(ConfigFile.JwtPrivateKey, Is.Null); | ||
| Assert.DoesNotThrow(() => { var _ = ConfigFile.JwtPrivateKey; }); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to _ is useless, since its value is never read.
| <input type="password" class="form-control @InputClass" id="PasswordInput" placeholder="@(globalConfig.GetText("password"))" value="@Password" @oninput="OnPasswordInput"> | ||
| </div> | ||
| @if (loginInProgress == false) | ||
| @if(loginInProgress == false) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A == false' can be simplified to '!A'.
| <input type="password" class="form-control @InputClass" id="NewPasswordInput2" placeholder="@(userConfig.GetText("new_password"))" value="@newPassword2" @oninput="OnNewPassword2Input"> | ||
| </div> | ||
| @if (passwordChangeInProgress == false) | ||
| @if(passwordChangeInProgress == false) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A == false' can be simplified to '!A'.



No description provided.