From 7d1f39f4e94ec292d5173a882d25d9c50123485b Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Thu, 30 Oct 2025 16:34:54 +0100 Subject: [PATCH 01/12] [+] Added `getRefreshToken` query to retrieve valid tokens by hash. [+] Added `revokeRefreshToken` mutation to revoke tokens by setting `revoked_at`. [+] Added `storeRefreshToken` mutation to insert new tokens into the database. --- .../files/fwo-api-calls/auth/getRefreshToken.graphql | 12 ++++++++++++ .../fwo-api-calls/auth/revokeRefreshToken.graphql | 10 ++++++++++ .../fwo-api-calls/auth/storeRefreshToken.graphql | 7 +++++++ 3 files changed, 29 insertions(+) create mode 100644 roles/common/files/fwo-api-calls/auth/getRefreshToken.graphql create mode 100644 roles/common/files/fwo-api-calls/auth/revokeRefreshToken.graphql create mode 100644 roles/common/files/fwo-api-calls/auth/storeRefreshToken.graphql diff --git a/roles/common/files/fwo-api-calls/auth/getRefreshToken.graphql b/roles/common/files/fwo-api-calls/auth/getRefreshToken.graphql new file mode 100644 index 000000000..52f780091 --- /dev/null +++ b/roles/common/files/fwo-api-calls/auth/getRefreshToken.graphql @@ -0,0 +1,12 @@ +# GraphQL query to retrieve a valid (not revoked) refresh token by its hash + +query getRefreshToken($tokenHash: String!, $currentTime: timestamptz!) { + refresh_tokens(where: { + token_hash: {_eq: $tokenHash}, + expires_at: {_gt: $currentTime}, + revoked_at: {_is_null: true} + }) { + user_id + expires_at + } +} \ No newline at end of file diff --git a/roles/common/files/fwo-api-calls/auth/revokeRefreshToken.graphql b/roles/common/files/fwo-api-calls/auth/revokeRefreshToken.graphql new file mode 100644 index 000000000..62a31a278 --- /dev/null +++ b/roles/common/files/fwo-api-calls/auth/revokeRefreshToken.graphql @@ -0,0 +1,10 @@ +# GraphQL mutation to revoke a refresh token by setting its revoked_at timestamp + +mutation revokeRefreshToken($tokenHash: String!, $revokedAt: timestamptz!) { + update_refresh_tokens( + where: {token_hash: {_eq: $tokenHash}}, + _set: {revoked_at: $revokedAt} + ) { + affected_rows + } +} \ No newline at end of file diff --git a/roles/common/files/fwo-api-calls/auth/storeRefreshToken.graphql b/roles/common/files/fwo-api-calls/auth/storeRefreshToken.graphql new file mode 100644 index 000000000..f2df7c359 --- /dev/null +++ b/roles/common/files/fwo-api-calls/auth/storeRefreshToken.graphql @@ -0,0 +1,7 @@ +# GraphQL mutation to store a refresh token in the database + +mutation storeRefreshToken($userId: Int!, $tokenHash: String!, $expiresAt: timestamptz!, $createdAt: timestamptz!) { + insert_refresh_tokens_one(object: {user_id: $userId, token_hash: $tokenHash, expires_at: $expiresAt, created_at: $createdAt}) { + id + } +} \ No newline at end of file From 000a0c6445fd0da6022f7d3abe5f0ddcb35b1469 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Thu, 30 Oct 2025 16:54:05 +0100 Subject: [PATCH 02/12] [+] Introduced `RefreshTokenInfo`, `RefreshTokenRequest`, and `TokenPair` classes to handle token-related data and operations. [~] Updated `AuthQueries` static constructor to initialize refresh token queries. --- .../lib/files/FWO.Api.Client/Queries/AuthQueries.cs | 12 +++++++++++- .../files/FWO.Data/Middleware/RefreshTokenInfo.cs | 13 +++++++++++++ .../FWO.Data/Middleware/RefreshTokenRequest.cs | 7 +++++++ roles/lib/files/FWO.Data/Middleware/TokenPair.cs | 10 ++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 roles/lib/files/FWO.Data/Middleware/RefreshTokenInfo.cs create mode 100644 roles/lib/files/FWO.Data/Middleware/RefreshTokenRequest.cs create mode 100644 roles/lib/files/FWO.Data/Middleware/TokenPair.cs diff --git a/roles/lib/files/FWO.Api.Client/Queries/AuthQueries.cs b/roles/lib/files/FWO.Api.Client/Queries/AuthQueries.cs index e27dfafa5..b1d821b59 100644 --- a/roles/lib/files/FWO.Api.Client/Queries/AuthQueries.cs +++ b/roles/lib/files/FWO.Api.Client/Queries/AuthQueries.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using FWO.Logging; @@ -42,6 +42,11 @@ public class AuthQueries : Queries public static readonly string updateLdapConnection; public static readonly string deleteLdapConnection; + // Refresh Token Queries + public static readonly string storeRefreshToken; + public static readonly string getRefreshToken; + public static readonly string revokeRefreshToken; + static AuthQueries() { try @@ -81,6 +86,11 @@ static AuthQueries() newLdapConnection = GetQueryText("auth/newLdapConnection.graphql"); updateLdapConnection = GetQueryText("auth/updateLdapConnection.graphql"); deleteLdapConnection = GetQueryText("auth/deleteLdapConnection.graphql"); + + // Refresh Token Queries + storeRefreshToken = GetQueryText(Path.Combine("auth", "storeRefreshToken.graphql")); + getRefreshToken = GetQueryText(Path.Combine("auth", "getRefreshToken.graphql")); + revokeRefreshToken = GetQueryText(Path.Combine("auth", "revokeRefreshToken.graphql")); } catch (Exception exception) { diff --git a/roles/lib/files/FWO.Data/Middleware/RefreshTokenInfo.cs b/roles/lib/files/FWO.Data/Middleware/RefreshTokenInfo.cs new file mode 100644 index 000000000..5b20e49d5 --- /dev/null +++ b/roles/lib/files/FWO.Data/Middleware/RefreshTokenInfo.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace FWO.Data.Middleware +{ + public class RefreshTokenInfo + { + [JsonPropertyName("user_id")] + public int UserId { get; set; } + + [JsonPropertyName("expires_at")] + public DateTime ExpiresAt { get; set; } + } +} diff --git a/roles/lib/files/FWO.Data/Middleware/RefreshTokenRequest.cs b/roles/lib/files/FWO.Data/Middleware/RefreshTokenRequest.cs new file mode 100644 index 000000000..1e3426c28 --- /dev/null +++ b/roles/lib/files/FWO.Data/Middleware/RefreshTokenRequest.cs @@ -0,0 +1,7 @@ +namespace FWO.Data.Middleware +{ + public class RefreshTokenRequest + { + public string RefreshToken { get; set; } = ""; + } +} diff --git a/roles/lib/files/FWO.Data/Middleware/TokenPair.cs b/roles/lib/files/FWO.Data/Middleware/TokenPair.cs new file mode 100644 index 000000000..935b284dd --- /dev/null +++ b/roles/lib/files/FWO.Data/Middleware/TokenPair.cs @@ -0,0 +1,10 @@ +namespace FWO.Data.Middleware +{ + public class TokenPair + { + public string AccessToken { get; set; } = ""; + public string RefreshToken { get; set; } = ""; + public DateTime AccessTokenExpires { get; set; } + public DateTime RefreshTokenExpires { get; set; } + } +} From 0bf04f672027b1f1684da842f89266fa7aa1dfa0 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Thu, 30 Oct 2025 17:44:26 +0100 Subject: [PATCH 03/12] [+] GenerateRefreshToken --- .../files/FWO.Middleware.Server/JwtWriter.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/roles/middleware/files/FWO.Middleware.Server/JwtWriter.cs b/roles/middleware/files/FWO.Middleware.Server/JwtWriter.cs index 050773165..baf14d6a1 100644 --- a/roles/middleware/files/FWO.Middleware.Server/JwtWriter.cs +++ b/roles/middleware/files/FWO.Middleware.Server/JwtWriter.cs @@ -1,10 +1,11 @@ -using FWO.Basics; +using FWO.Basics; using FWO.Data; using FWO.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using System.Security.Cryptography; using System.Text.Json; namespace FWO.Middleware.Server @@ -120,9 +121,9 @@ private static ClaimsIdentity SetClaims(UiUser user) claimsIdentity.AddClaim(new Claim("x-hasura-user-id", user.DbId.ToString())); if (user.Dn != null && user.Dn.Length > 0) claimsIdentity.AddClaim(new Claim("x-hasura-uuid", user.Dn)); // UUID used for access to reports via API - + if (user.Tenant != null) - { + { claimsIdentity.AddClaim(new Claim("x-hasura-tenant-id", user.Tenant.Id.ToString())); if(user.Tenant.VisibleGatewayIds != null && user.Tenant.VisibleManagementIds != null) { @@ -178,5 +179,13 @@ private static string GetDefaultRole(UiUser user, List hasuraRolesList) } return defaultRole; } - } + + /// + /// Generates a cryptographically secure refresh token + /// + public static string GenerateRefreshToken() + { + return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + } + } } From 9a5fd50c8d25ec2ec46d5a96ef8e0c304a8d3358 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Wed, 5 Nov 2025 12:03:12 +0100 Subject: [PATCH 04/12] [+] Added support for access and refresh tokens across backend and UI. (WIP) --- roles/lib/files/FWO.Config.Api/UserConfig.cs | 4 +- .../FWO.Middleware.Client/MiddlewareClient.cs | 38 +++- .../AuthenticationTokenController.cs | 198 ++++++++++++++++++ .../ui/files/FWO.UI/Auth/AuthStateProvider.cs | 20 +- roles/ui/files/FWO.UI/Pages/Login.razor | 22 +- roles/ui/files/FWO.UI/Program.cs | 7 +- roles/ui/files/FWO.UI/Shared/MainLayout.razor | 5 +- 7 files changed, 263 insertions(+), 31 deletions(-) diff --git a/roles/lib/files/FWO.Config.Api/UserConfig.cs b/roles/lib/files/FWO.Config.Api/UserConfig.cs index c5254c19c..d4e44458d 100644 --- a/roles/lib/files/FWO.Config.Api/UserConfig.cs +++ b/roles/lib/files/FWO.Config.Api/UserConfig.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FWO.Basics; using FWO.Logging; using FWO.Config.Api.Data; @@ -186,7 +186,7 @@ public string GetApiText(string key) Match m = Regex.Match(key, pattern); if (m.Success) { - string msg = GetText(key[..5]); + string msg = GetText(m.Value); if (msg != GlobalConst.kUndefinedText) { text = msg; diff --git a/roles/lib/files/FWO.Middleware.Client/MiddlewareClient.cs b/roles/lib/files/FWO.Middleware.Client/MiddlewareClient.cs index 09b082f36..2ac01d929 100644 --- a/roles/lib/files/FWO.Middleware.Client/MiddlewareClient.cs +++ b/roles/lib/files/FWO.Middleware.Client/MiddlewareClient.cs @@ -1,4 +1,4 @@ -using FWO.Api.Client; +using FWO.Api.Client; using FWO.Data.Middleware; using RestSharp; @@ -11,18 +11,18 @@ public class MiddlewareClient : RestApiClient, IDisposable public MiddlewareClient(string middlewareServerUri) : base(middlewareServerUri + "api/") { } - public async Task> AuthenticateUser(AuthenticationTokenGetParameters parameters) + public async Task> AuthenticateUser(AuthenticationTokenGetParameters parameters) { - RestRequest request = new ("AuthenticationToken/Get", Method.Post); + RestRequest request = new("AuthenticationToken/GetTokenPair", Method.Post); request.AddJsonBody(parameters); - return await restClient.ExecuteAsync(request); + return await restClient.ExecuteAsync(request); } - public async Task> CreateInitialJWT() + public async Task> CreateInitialJWT() { - RestRequest request = new ("AuthenticationToken/Get", Method.Post); + RestRequest request = new ("AuthenticationToken/GetTokenPair", Method.Post); request.AddJsonBody(new object()); - return await restClient.ExecuteAsync(request); + return await restClient.ExecuteAsync(request); } public async Task> TestConnection(LdapGetUpdateParameters parameters) @@ -247,6 +247,30 @@ public async Task> ImportCompianceMatrix(ComplianceImportMa return await restClient.ExecuteAsync(request); } + /// + /// Get a new token pair + /// + /// + /// + public async Task> RefreshToken(RefreshTokenRequest parameters) + { + RestRequest request = new("AuthenticationToken/Refresh", Method.Post); + request.AddJsonBody(parameters); + return await restClient.ExecuteAsync(request); + } + + /// + /// Revoke a refresh token + /// + /// + /// + public async Task RevokeRefreshToken(RefreshTokenRequest parameters) + { + RestRequest request = new("AuthenticationToken/Revoke", Method.Post); + request.AddJsonBody(parameters); + return await restClient.ExecuteAsync(request); + } + protected virtual void Dispose(bool disposing) { if (disposed) return; diff --git a/roles/middleware/files/FWO.Middleware.Server/Controllers/AuthenticationTokenController.cs b/roles/middleware/files/FWO.Middleware.Server/Controllers/AuthenticationTokenController.cs index d38bdf4a1..5cee2232a 100644 --- a/roles/middleware/files/FWO.Middleware.Server/Controllers/AuthenticationTokenController.cs +++ b/roles/middleware/files/FWO.Middleware.Server/Controllers/AuthenticationTokenController.cs @@ -8,6 +8,8 @@ using Novell.Directory.Ldap; using System.Data; using System.Security.Authentication; +using System.Text; +using System.Security.Cryptography; namespace FWO.Middleware.Server.Controllers { @@ -125,6 +127,90 @@ public async Task> GetAsyncForUser([FromBody] Authenticatio return BadRequest(e.Message); } } + + /// + /// Generates authentication tokens (access + refresh) given valid credentials. + /// + [HttpPost("GetTokenPair")] + public async Task> GetTokenPairAsync([FromBody] AuthenticationTokenGetParameters parameters) + { + try + { + UiUser? user = null; + + if(parameters != null) + { + string? username = parameters.Username; + string? password = parameters.Password; + + if(username != null && password != null) + user = new UiUser { Name = username, Password = password }; + } + + AuthManager authManager = new(jwtWriter, ldaps, apiConnection); + + await authManager.AuthorizeUserAsync(user, validatePassword: true); + + // Creates access and refresh token and stores the access token hash in DB + TokenPair tokenPair = await authManager.CreateTokenPair(user); + + return Ok(tokenPair); + } + catch(Exception ex) + { + Log.WriteError("Token Generation", "Error generating token pair", ex); + return BadRequest(ex.Message); + } + } + + /// + /// Refreshes an access token using a valid refresh token. + /// + /// Refresh token request + /// New token pair if refresh token is valid + [HttpPost("Refresh")] + public async Task> RefreshToken([FromBody] RefreshTokenRequest request) + { + try + { + if(string.IsNullOrEmpty(request.RefreshToken)) + { + return BadRequest("Refresh token is required"); + } + + AuthManager authManager = new(jwtWriter, ldaps, apiConnection); + + // Validate refresh token + RefreshTokenInfo? tokenInfo = await authManager.ValidateRefreshToken(request.RefreshToken); + + if(tokenInfo == null) + { + return Unauthorized("Invalid or expired refresh token"); + } + + UiUser[] users = await apiConnection.SendQueryAsync(AuthQueries.getUserByDbId, new { userId = tokenInfo.UserId }); + UiUser? user = users.FirstOrDefault(); + + if(user == null) + { + return Unauthorized("User not found"); + } + + // Revoke the old refresh token (token rotation for security) + await authManager.RevokeRefreshToken(request.RefreshToken); + + // Create new token pair + TokenPair newTokens = await authManager.CreateTokenPair(user); + + Log.WriteInfo("Token Refresh", $"Successfully refreshed tokens for user {user.Name}"); + return Ok(newTokens); + } + catch(Exception ex) + { + Log.WriteError("Token Refresh", "Failed to refresh token", ex); + return BadRequest(ex.Message); + } + } } class AuthManager @@ -423,5 +509,117 @@ private static async Task AddDevices(ApiConnection conn, Tenant tenant) Management[] managementIds = await conn.SendQueryAsync(AuthQueries.getVisibleManagementIdsPerTenant, tenIdObj, "getVisibleManagementIdsPerTenant"); tenant.VisibleManagementIds = Array.ConvertAll(managementIds, management => management.Id); } + + /// + /// Validates a refresh token and returns token info if valid + /// + public async Task ValidateRefreshToken(string refreshToken) + { + try + { + string tokenHash = GenerateTokenHash(refreshToken); + + var queryVariables = new + { + tokenHash = tokenHash, + currentTime = DateTime.UtcNow + }; + + RefreshTokenInfo[] result = await apiConnection.SendQueryAsync(AuthQueries.getRefreshToken, queryVariables); + + return result?.FirstOrDefault(); + } + catch(Exception ex) + { + Log.WriteError("Token Validation", "Error validating refresh token", ex); + return null; + } + } + + /// + /// Stores a refresh token in the database + /// + public async Task StoreRefreshToken(int userId, string refreshToken, DateTime expiresAt) + { + try + { + string tokenHash = GenerateTokenHash(refreshToken); + + var mutationVariables = new + { + userId = userId, + tokenHash = tokenHash, + expiresAt = expiresAt, + createdAt = DateTime.UtcNow + }; + + await apiConnection.SendQueryAsync(AuthQueries.storeRefreshToken, mutationVariables); + } + catch(Exception ex) + { + Log.WriteError("Token Storage", "Error storing refresh token", ex); + throw; + } + } + + /// + /// Revokes a refresh token by marking it as revoked + /// + public async Task RevokeRefreshToken(string refreshToken) + { + try + { + string tokenHash = GenerateTokenHash(refreshToken); + + var mutationVariables = new + { + tokenHash = tokenHash, + revokedAt = DateTime.UtcNow + }; + + await apiConnection.SendQueryAsync(AuthQueries.revokeRefreshToken, mutationVariables); + } + catch(Exception ex) + { + Log.WriteError("Token Revocation", "Error revoking refresh token", ex); + throw; + } + } + + /// + /// Generates a SHA256 hash of the refresh token for secure storage + /// + private static string GenerateTokenHash(string token) + { + byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(token)); + return Convert.ToBase64String(hash); + } + + /// + /// Create access and refresh token pair for given user + /// + /// + /// + /// + public async Task CreateTokenPair(UiUser? user = null, TimeSpan? accessTokenLifetime = null) + { + UiUserHandler uiUserHandler = new(jwtWriter.CreateJWTMiddlewareServer()); + + TimeSpan accessLifetime = accessTokenLifetime ?? TimeSpan.FromMinutes(await uiUserHandler.GetExpirationTime()); + string accessToken = await jwtWriter.CreateJWT(user, accessLifetime); + + string refreshToken = JwtWriter.GenerateRefreshToken(); + DateTime refreshExpiry = DateTime.UtcNow.AddDays(7); + + await StoreRefreshToken(user?.DbId ?? 0, refreshToken, refreshExpiry); + + return new TokenPair + { + AccessToken = accessToken, + RefreshToken = refreshToken, + AccessTokenExpires = DateTime.UtcNow.Add(accessLifetime), + RefreshTokenExpires = refreshExpiry + }; + } } } diff --git a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs index 7290370cd..231641221 100644 --- a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs +++ b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs @@ -1,4 +1,4 @@ -using FWO.Api.Client; +using FWO.Api.Client; using FWO.Api.Client.Queries; using FWO.Basics; using FWO.Config.Api; @@ -26,17 +26,23 @@ public override Task GetAuthenticationStateAsync() return Task.FromResult(new AuthenticationState(user)); } - public async Task> Authenticate(string username, string password, ApiConnection apiConnection, MiddlewareClient middlewareClient, + public async Task> Authenticate(string username, string password, ApiConnection apiConnection, MiddlewareClient middlewareClient, GlobalConfig globalConfig, UserConfig userConfig, ProtectedSessionStorage sessionStorage, CircuitHandlerService circuitHandler) { // There is no jwt in session storage. Get one from auth module. AuthenticationTokenGetParameters authenticationParameters = new() { Username = username, Password = password }; - RestResponse apiAuthResponse = await middlewareClient.AuthenticateUser(authenticationParameters); + RestResponse apiAuthResponse = await middlewareClient.AuthenticateUser(authenticationParameters); if (apiAuthResponse.StatusCode == HttpStatusCode.OK) { - string jwtString = apiAuthResponse.Data ?? throw new ArgumentException("no response data"); - await Authenticate(jwtString, apiConnection, middlewareClient, globalConfig, userConfig, circuitHandler, sessionStorage); + string tokenPairJson = apiAuthResponse.Content ?? throw new ArgumentException("no response content"); + + TokenPair tokenPair = System.Text.Json.JsonSerializer.Deserialize(tokenPairJson) ?? throw new ArgumentException("failed to deserialize token pair"); + + string jwtString = tokenPair.AccessToken ?? throw new ArgumentException("no access token in response"); + + await Authenticate(jwtString, apiConnection, middlewareClient, globalConfig, userConfig, circuitHandler, sessionStorage); + Log.WriteAudit("AuthenticateUser", $"user {username} successfully authenticated"); } @@ -62,7 +68,7 @@ public async Task Authenticate(string jwtString, ApiConnection apiConnection, Mi { throw new AuthenticationException("not_authorized"); } - + // Save jwt in session storage. await sessionStorage.SetAsync("jwt", jwtString); @@ -209,7 +215,7 @@ private static async Task> GetClaimList(string jwtString, string cl JwtReader jwtReader = new(jwtString); if (await jwtReader.Validate()) { - ClaimsIdentity identity = new + ClaimsIdentity identity = new ( claims: jwtReader.GetClaims(), authenticationType: "ldap", diff --git a/roles/ui/files/FWO.UI/Pages/Login.razor b/roles/ui/files/FWO.UI/Pages/Login.razor index f29f51cac..ea8d80bcc 100644 --- a/roles/ui/files/FWO.UI/Pages/Login.razor +++ b/roles/ui/files/FWO.UI/Pages/Login.razor @@ -20,7 +20,7 @@ @inject CircuitHandler circuitHandler @if (showLoginForm) -{ +{
@@ -32,7 +32,7 @@ { } -

+

@*

@(userConfig.GetText("login"))

*@
@@ -55,14 +55,14 @@ { } -
+
@if(ShowWelcomeMessage) { -
+
@@ -73,7 +73,7 @@
- } + } } @if (showPasswordChangeForm) @@ -138,7 +138,7 @@ //ApiConnection = new GraphQlApiConnection(ConfigFile.ApiServerUri); if (firstRender) - { + { // This might be a reconnect. Check if there is a jwt in session storage. ProtectedBrowserStorageResult jwtLoadRequest = await sessionStorage.GetAsync("jwt"); @@ -157,7 +157,7 @@ { SanitizedWelcomeMessage = globalConfig.WelcomeMessage.StripDangerousHtmlTags().Replace("\n", "
"); ShowWelcomeMessage = true; - } + } // else no reconnect / reconnect unsuccessful showLoginForm = true; @@ -205,7 +205,7 @@ try { - RestResponse authResponse = await ((AuthStateProvider)AuthService) + RestResponse authResponse = await ((AuthStateProvider)AuthService) .Authenticate(Username, Password, ApiConnection, middlewareClient, globalConfig, userConfig, sessionStorage, ((CircuitHandlerService)circuitHandler)); if (authResponse.StatusCode == HttpStatusCode.OK) @@ -220,10 +220,10 @@ else { // There was an error trying to authenticate the user. Probably invalid credentials or the middleware server is unreachable - if(authResponse.Data != null) + if(authResponse.Content != null) { // Probably invalid credentials - errorMessage = userConfig.GetApiText(authResponse.Data); + errorMessage = userConfig.GetApiText(authResponse.Content); // Visualisize the error by making border of all inputboxes red InputClass = "is-invalid"; Log.WriteInfo("Login", $"Login of user {Username} failed: " + errorMessage); @@ -239,7 +239,7 @@ StateHasChanged(); } } - // Authentication exception (raised by us) + // Authentication exception (raised by us) catch (AuthenticationException e) { errorMessage = userConfig.GetText(e.Message); diff --git a/roles/ui/files/FWO.UI/Program.cs b/roles/ui/files/FWO.UI/Program.cs index 63eea6f72..9915ca267 100644 --- a/roles/ui/files/FWO.UI/Program.cs +++ b/roles/ui/files/FWO.UI/Program.cs @@ -2,6 +2,7 @@ using FWO.Api.Client; using FWO.Config.Api; using FWO.Config.File; +using FWO.Data.Middleware; using FWO.Logging; using FWO.Middleware.Client; using FWO.Services; @@ -62,7 +63,7 @@ MiddlewareClient middlewareClient = new MiddlewareClient(MiddlewareUri); ApiConnection apiConn = new GraphQlApiConnection(ApiUri); -RestResponse createJWTResponse = middlewareClient.CreateInitialJWT().Result; +RestResponse createJWTResponse = middlewareClient.CreateInitialJWT().Result; bool connectionEstablished = createJWTResponse.IsSuccessful; int connectionAttemptsCount = 1; while (!connectionEstablished) @@ -77,7 +78,9 @@ connectionEstablished = createJWTResponse.IsSuccessful; } -string jwt = createJWTResponse.Data ?? throw new NullReferenceException("Received empty jwt."); +TokenPair tokenPair = System.Text.Json.JsonSerializer.Deserialize(createJWTResponse.Content) ?? throw new ArgumentException("failed to deserialize token pair"); + +string jwt = tokenPair.AccessToken ?? throw new NullReferenceException("Received empty jwt."); apiConn.SetAuthHeader(jwt); // Get all non-confidential configuration settings and add to a global service (for all users) diff --git a/roles/ui/files/FWO.UI/Shared/MainLayout.razor b/roles/ui/files/FWO.UI/Shared/MainLayout.razor index d4198f1be..d7e1ad133 100644 --- a/roles/ui/files/FWO.UI/Shared/MainLayout.razor +++ b/roles/ui/files/FWO.UI/Shared/MainLayout.razor @@ -1,4 +1,5 @@ @inherits LayoutComponentBase +@using FWO.Data.Middleware @using FWO.Middleware.Client @using FWO.Services.EventMediator @using FWO.Services.EventMediator.Events @@ -196,7 +197,7 @@ } try { - RestResponse authResponse = await ( (AuthStateProvider)authenticationProvider ).Authenticate(userConfig.User.Name, + RestResponse authResponse = await ( (AuthStateProvider)authenticationProvider ).Authenticate(userConfig.User.Name, password, apiConnection, middlewareClient, globalConfig, userConfig, sessionStorage, ( (CircuitHandlerService)circuitHandler )); if (authResponse.StatusCode == System.Net.HttpStatusCode.OK) { @@ -206,7 +207,7 @@ return; } // There was an error trying to authenticate the user. Probably invalid credentials - errorMessage = ( authResponse.Data != null ? userConfig.GetApiText(authResponse.Data) : "Middleware Api Error: " + authResponse.Content ); + errorMessage = (authResponse.Content != null ? userConfig.GetApiText(authResponse.Content) : "Middleware Api Error: " + authResponse.Content); } catch (Exception ex) { From 66b5c64691094925733cffe304edad115127d3d7 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Thu, 6 Nov 2025 08:34:10 +0100 Subject: [PATCH 05/12] [~] fix csharpsquid:S112 --- roles/ui/files/FWO.UI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/ui/files/FWO.UI/Program.cs b/roles/ui/files/FWO.UI/Program.cs index 9915ca267..a3fec7368 100644 --- a/roles/ui/files/FWO.UI/Program.cs +++ b/roles/ui/files/FWO.UI/Program.cs @@ -80,7 +80,7 @@ TokenPair tokenPair = System.Text.Json.JsonSerializer.Deserialize(createJWTResponse.Content) ?? throw new ArgumentException("failed to deserialize token pair"); -string jwt = tokenPair.AccessToken ?? throw new NullReferenceException("Received empty jwt."); +string jwt = tokenPair.AccessToken ?? throw new ArgumentException("Received empty jwt."); apiConn.SetAuthHeader(jwt); // Get all non-confidential configuration settings and add to a global service (for all users) From 717c6b2bc9063a9d2f047724fa1b630f230bb060 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Thu, 6 Nov 2025 08:49:06 +0100 Subject: [PATCH 06/12] [+] Added `refresh_tokens` table for token management --- roles/database/files/upgrade/9.0.sql | 225 ++++++++++++++------------- 1 file changed, 117 insertions(+), 108 deletions(-) diff --git a/roles/database/files/upgrade/9.0.sql b/roles/database/files/upgrade/9.0.sql index 9b0e58739..d771c0ffe 100644 --- a/roles/database/files/upgrade/9.0.sql +++ b/roles/database/files/upgrade/9.0.sql @@ -50,10 +50,10 @@ $$ LANGUAGE plpgsql VOLATILE; Alter table "ldap_connection" ADD COLUMN IF NOT EXISTS "ldap_writepath_for_groups" Varchar; CREATE OR REPLACE FUNCTION insertLocalLdapWithEncryptedPasswords( - serverName TEXT, + serverName TEXT, port INTEGER, userSearchPath TEXT, - roleSearchPath TEXT, + roleSearchPath TEXT, groupSearchPath TEXT, groupWritePath TEXT, tenantLevel INTEGER, @@ -114,7 +114,7 @@ insert into stm_track (track_id,track_name) VALUES (23,'detailed log') ON CONFLI insert into stm_track (track_id,track_name) VALUES (24,'extended log') ON CONFLICT DO NOTHING; -- check point R8x -- 8.8.8 -DO $$ +DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'fwo_ro') THEN CREATE ROLE fwo_ro WITH LOGIN NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE; @@ -342,19 +342,19 @@ BEGIN SELECT INTO i_mgm_id mgm_id FROM import_control WHERE control_id=NEW.import_id; -- before importing, delete all old interfaces and routes belonging to the current management: - -- now re-insert the currently found interfaces: + -- now re-insert the currently found interfaces: SELECT INTO i_count COUNT(*) FROM jsonb_populate_recordset(NULL::gw_interface, NEW.config -> 'interfaces'); IF i_count>0 THEN - DELETE FROM gw_interface WHERE routing_device IN + DELETE FROM gw_interface WHERE routing_device IN (SELECT dev_id FROM device LEFT JOIN management ON (device.mgm_id=management.mgm_id) WHERE management.mgm_id=i_mgm_id); INSERT INTO gw_interface SELECT * FROM jsonb_populate_recordset(NULL::gw_interface, NEW.config -> 'interfaces'); END IF; SELECT INTO i_count COUNT(*) FROM jsonb_populate_recordset(NULL::gw_route, NEW.config -> 'routing'); IF i_count>0 THEN - DELETE FROM gw_route WHERE routing_device IN + DELETE FROM gw_route WHERE routing_device IN (SELECT dev_id FROM device LEFT JOIN management ON (device.mgm_id=management.mgm_id) WHERE management.mgm_id=i_mgm_id); - -- now re-insert the currently found routes: + -- now re-insert the currently found routes: INSERT INTO gw_route SELECT * FROM jsonb_populate_recordset(NULL::gw_route, NEW.config -> 'routing'); END IF; @@ -392,7 +392,7 @@ BEGIN IF NEW.start_import_flag THEN -- finally start the stored procedure import - PERFORM import_all_main(NEW.import_id, NEW.debug_mode); + PERFORM import_all_main(NEW.import_id, NEW.debug_mode); END IF; RETURN NEW; END; @@ -421,7 +421,7 @@ ALTER TABLE device ADD COLUMN IF NOT EXISTS "dev_uid" Varchar NOT NULL DEFAULT ' Alter table stm_action add column if not exists allowed BOOLEAN NOT NULL DEFAULT TRUE; UPDATE stm_action SET allowed = FALSE WHERE action_name = 'deny' OR action_name = 'drop' OR action_name = 'reject'; -Create table IF NOT EXISTS "rulebase" +Create table IF NOT EXISTS "rulebase" ( "id" SERIAL primary key, "name" Varchar NOT NULL, @@ -491,7 +491,7 @@ ALTER table "import_control" ADD COLUMN IF NOT EXISTS "is_full_import" BOOLEAN D ----------------------------------------------- -Create Table IF NOT EXISTS "rule_enforced_on_gateway" +Create Table IF NOT EXISTS "rule_enforced_on_gateway" ( "rule_id" Integer NOT NULL, "dev_id" Integer, -- NULL if rule is available for all gateways of its management @@ -509,7 +509,7 @@ Alter table "rule_enforced_on_gateway" add CONSTRAINT fk_rule_enforced_on_gatewa ALTER TABLE "rule_enforced_on_gateway" DROP CONSTRAINT IF EXISTS "fk_rule_enforced_on_gateway_created_import_control_control_id" CASCADE; -Alter table "rule_enforced_on_gateway" add CONSTRAINT fk_rule_enforced_on_gateway_created_import_control_control_id +Alter table "rule_enforced_on_gateway" add CONSTRAINT fk_rule_enforced_on_gateway_created_import_control_control_id foreign key ("created") references "import_control" ("control_id") on update restrict on delete cascade; ALTER TABLE "rule_enforced_on_gateway" @@ -519,7 +519,7 @@ ALTER TABLE "rule_enforced_on_gateway" ALTER TABLE "rule_enforced_on_gateway" DROP CONSTRAINT IF EXISTS "fk_rule_enforced_on_gateway_deleted_import_control_control_id" CASCADE; -Alter table "rule_enforced_on_gateway" add CONSTRAINT fk_rule_enforced_on_gateway_removed_import_control_control_id +Alter table "rule_enforced_on_gateway" add CONSTRAINT fk_rule_enforced_on_gateway_removed_import_control_control_id foreign key ("removed") references "import_control" ("control_id") on update restrict on delete cascade; ----------------------------------------------- @@ -530,8 +530,8 @@ RETURNS NUMERIC AS $$ FROM rule r WHERE r.mgm_id = mgmId and active AND r.rule_num_numeric > ( - SELECT rule_num_numeric - FROM rule + SELECT rule_num_numeric + FROM rule WHERE rule_uid = current_rule_uid AND mgm_id = mgmId AND active LIMIT 1 ) @@ -552,7 +552,7 @@ ALTER TABLE "rule" ADD CONSTRAINT fk_rule_rulebase_id FOREIGN KEY ("rulebase_id" -- Alter table "rule" add constraint "rule_metadata_dev_id_rule_uid_f_key" -- foreign key ("dev_id", "rule_uid", "rulebase_id") references "rule_metadata" ("dev_id", "rule_uid", "rulebase_id") on update restrict on delete cascade; --- Create table IF NOT EXISTS "rule_hit" +-- Create table IF NOT EXISTS "rule_hit" -- ( -- "rule_id" BIGINT NOT NULL, -- "rule_uid" VARCHAR NOT NULL, @@ -565,9 +565,9 @@ ALTER TABLE "rule" ADD CONSTRAINT fk_rule_rulebase_id FOREIGN KEY ("rulebase_id" -- Alter table "rule_hit" DROP CONSTRAINT IF EXISTS fk_rule_hit_rule_id; -- Alter table "rule_hit" DROP CONSTRAINT IF EXISTS fk_hit_gw_id; -- Alter table "rule_hit" DROP CONSTRAINT IF EXISTS fk_hit_metadata_id; --- Alter table "rule_hit" add CONSTRAINT fk_hit_rule_id foreign key ("rule_id") references "rule" ("rule_id") on update restrict on delete cascade; --- Alter table "rule_hit" add CONSTRAINT fk_hit_gw_id foreign key ("gw_id") references "device" ("dev_id") on update restrict on delete cascade; --- Alter table "rule_hit" add CONSTRAINT fk_hit_metadata_id foreign key ("metadata_id") references "rule_metadata" ("dev_id") on update restrict on delete cascade; +-- Alter table "rule_hit" add CONSTRAINT fk_hit_rule_id foreign key ("rule_id") references "rule" ("rule_id") on update restrict on delete cascade; +-- Alter table "rule_hit" add CONSTRAINT fk_hit_gw_id foreign key ("gw_id") references "device" ("dev_id") on update restrict on delete cascade; +-- Alter table "rule_hit" add CONSTRAINT fk_hit_metadata_id foreign key ("metadata_id") references "rule_metadata" ("dev_id") on update restrict on delete cascade; ----------------------------------------------- -- METADATA part @@ -581,7 +581,7 @@ Alter Table "rule_metadata" drop Constraint IF EXISTS "rule_metadata_alt_key"; -- ALTER TABLE rule_metadata DROP Constraint IF EXISTS "rule_metadata_rule_uid_unique"; -- ALTER TABLE rule_metadata ADD Constraint "rule_metadata_rule_uid_unique" unique ("rule_uid"); -- causes error: - -- None: FEHLER: kann Constraint rule_metadata_rule_uid_unique für Tabelle rule_metadata nicht löschen, weil andere Objekte davon abhängen\nDETAIL: + -- None: FEHLER: kann Constraint rule_metadata_rule_uid_unique für Tabelle rule_metadata nicht löschen, weil andere Objekte davon abhängen\nDETAIL: -- Constraint rule_metadata_rule_uid_f_key für Tabelle rule hängt von Index rule_metadata_rule_uid_unique ab\nHINT: Verwenden Sie DROP ... CASCADE, um die abhängigen Objekte ebenfalls zu löschen.\n"} ALTER TABLE rule_metadata DROP Constraint IF EXISTS "rule_metadata_rule_uid_unique" CASCADE; @@ -610,9 +610,9 @@ CREATE OR REPLACE VIEW v_rule_with_rule_owner AS WHERE NOT ow.id IS NULL GROUP BY r.rule_id, ow.id, ow.name, met.rule_last_certified, met.rule_last_certifier; -CREATE OR REPLACE VIEW v_rule_with_src_owner AS +CREATE OR REPLACE VIEW v_rule_with_src_owner AS SELECT - r.rule_id, ow.id as owner_id, ow.name as owner_name, + r.rule_id, ow.id as owner_id, ow.name as owner_name, CASE WHEN onw.ip = onw.ip_end THEN SPLIT_PART(CAST(onw.ip AS VARCHAR), '/', 1) -- Single IP overlap, removing netmask @@ -642,9 +642,9 @@ CREATE OR REPLACE VIEW v_rule_with_src_owner AS END GROUP BY r.rule_id, o.obj_ip, o.obj_ip_end, onw.ip, onw.ip_end, ow.id, ow.name, met.rule_last_certified, met.rule_last_certifier; -CREATE OR REPLACE VIEW v_rule_with_dst_owner AS - SELECT - r.rule_id, ow.id as owner_id, ow.name as owner_name, +CREATE OR REPLACE VIEW v_rule_with_dst_owner AS + SELECT + r.rule_id, ow.id as owner_id, ow.name as owner_name, CASE WHEN onw.ip = onw.ip_end THEN SPLIT_PART(CAST(onw.ip AS VARCHAR), '/', 1) -- Single IP overlap, removing netmask @@ -746,14 +746,14 @@ Alter table "rulebase_link" add CONSTRAINT unique_rulebase_link "to_rulebase_id", "created" ); - + ALTER TABLE "rulebase_link" DROP CONSTRAINT IF EXISTS "fk_rulebase_link_created_import_control_control_id" CASCADE; -Alter table "rulebase_link" add CONSTRAINT fk_rulebase_link_created_import_control_control_id +Alter table "rulebase_link" add CONSTRAINT fk_rulebase_link_created_import_control_control_id foreign key ("created") references "import_control" ("control_id") on update restrict on delete cascade; ALTER TABLE "rulebase_link" DROP CONSTRAINT IF EXISTS "fk_rulebase_link_removed_import_control_control_id" CASCADE; -Alter table "rulebase_link" add CONSTRAINT fk_rulebase_link_removed_import_control_control_id +Alter table "rulebase_link" add CONSTRAINT fk_rulebase_link_removed_import_control_control_id foreign key ("removed") references "import_control" ("control_id") on update restrict on delete cascade; insert into stm_link_type (id, name) VALUES (2, 'ordered') ON CONFLICT DO NOTHING; @@ -797,7 +797,7 @@ AS $function$ a_target_gateways VARCHAR[]; v_gw_name VARCHAR; BEGIN - FOR r_rulebase IN + FOR r_rulebase IN SELECT * FROM rulebase LOOP -- collect all device ids for this rulebase @@ -806,7 +806,7 @@ AS $function$ WHERE to_rulebase_id=r_rulebase.id ) INTO a_all_dev_ids_of_rulebase; - FOR r_rule IN + FOR r_rule IN SELECT rule_installon, rule_id FROM rule LOOP -- depending on install_on field: @@ -814,9 +814,9 @@ AS $function$ -- or just add specific gateway entries IF r_rule.rule_installon='Policy Targets' THEN -- need to find out other platforms equivivalent keywords - FOREACH i_dev_id IN ARRAY a_all_dev_ids_of_rulebase + FOREACH i_dev_id IN ARRAY a_all_dev_ids_of_rulebase LOOP - INSERT INTO rule_enforced_on_gateway (rule_id, dev_id, created) + INSERT INTO rule_enforced_on_gateway (rule_id, dev_id, created) VALUES (r_rule.rule_id, i_dev_id, (SELECT * FROM get_last_import_id_for_mgmt(r_rulebase.mgm_id))); END LOOP; ELSE @@ -827,13 +827,13 @@ AS $function$ SELECT ARRAY( SELECT string_to_array(r_rule.rule_installon, '|') ) INTO a_target_gateways; - FOREACH v_gw_name IN ARRAY a_target_gateways + FOREACH v_gw_name IN ARRAY a_target_gateways LOOP -- get dev_id for gw_name SELECT INTO i_dev_id dev_id FROM device WHERE dev_name=v_gw_name; IF FOUND THEN - INSERT INTO rule_enforced_on_gateway (rule_id, dev_id, created) - VALUES (r_rule.rule_id, i_dev_id, (SELECT * FROM get_last_import_id_for_mgmt(r_rulebase.mgm_id))); + INSERT INTO rule_enforced_on_gateway (rule_id, dev_id, created) + VALUES (r_rule.rule_id, i_dev_id, (SELECT * FROM get_last_import_id_for_mgmt(r_rulebase.mgm_id))); ELSE -- decide what to do with misses END IF; @@ -851,7 +851,7 @@ AS $function$ DECLARE r_dev RECORD; BEGIN - FOR r_dev IN + FOR r_dev IN -- TODO: deal with global rulebases here SELECT d.dev_id, rb.id as rulebase_id FROM device d LEFT JOIN rulebase rb ON (d.local_rulebase_name=rb.name) LOOP @@ -859,9 +859,9 @@ AS $function$ END LOOP; -- now we can add the "not null" constraint for rule_metadata.rulebase_id IF EXISTS ( - SELECT 1 + SELECT 1 FROM information_schema.columns - WHERE table_name = 'rule_metadata' + WHERE table_name = 'rule_metadata' AND column_name = 'rulebase_id' AND is_nullable = 'YES' ) THEN @@ -882,7 +882,7 @@ AS $function$ i_rulebase_id INTEGER; i_initial_rulebase_id INTEGER; BEGIN - FOR r_dev IN + FOR r_dev IN SELECT * FROM device LOOP -- find the id of the matching rulebase @@ -891,7 +891,7 @@ AS $function$ IF i_rulebase_id IS NOT NULL THEN SELECT INTO r_dev_null * FROM rulebase_link WHERE to_rulebase_id=i_rulebase_id AND gw_id=r_dev.dev_id AND removed IS NULL; IF NOT FOUND THEN - INSERT INTO rulebase_link (gw_id, from_rule_id, to_rulebase_id, created, link_type, is_initial) + INSERT INTO rulebase_link (gw_id, from_rule_id, to_rulebase_id, created, link_type, is_initial) VALUES (r_dev.dev_id, NULL, i_rulebase_id, (SELECT * FROM get_last_import_id_for_mgmt(r_dev.mgm_id)), 2, True) RETURNING id INTO i_initial_rulebase_id; -- when migrating, there cannot be more than one (the initial) rb per device END IF; @@ -906,7 +906,7 @@ AS $function$ SELECT INTO r_dev_null * FROM rulebase_link WHERE to_rulebase_id=i_rulebase_id AND gw_id=r_dev.dev_id; IF NOT FOUND THEN INSERT INTO rulebase_link (gw_id, from_rule_id, to_rulebase_id, created, link_type, is_initial) - VALUES (r_dev.dev_id, NULL, i_rulebase_id, (SELECT * FROM get_last_import_id_for_mgmt(r_dev.mgm_id)), 2, TRUE); + VALUES (r_dev.dev_id, NULL, i_rulebase_id, (SELECT * FROM get_last_import_id_for_mgmt(r_dev.mgm_id)), 2, TRUE); END IF; END IF; END IF; @@ -925,15 +925,15 @@ AS $function$ i_new_rulebase_id INTEGER; BEGIN - FOR r_dev IN + FOR r_dev IN SELECT * FROM device LOOP -- if rulebase does not exist yet: insert it SELECT INTO r_dev_null * FROM rulebase WHERE name=r_dev.local_rulebase_name; IF NOT FOUND AND r_dev.local_rulebase_name IS NOT NULL THEN -- first create rulebase entries - INSERT INTO rulebase (name, uid, mgm_id, is_global, created) - VALUES (r_dev.local_rulebase_name, r_dev.local_rulebase_name, r_dev.mgm_id, FALSE, 1) + INSERT INTO rulebase (name, uid, mgm_id, is_global, created) + VALUES (r_dev.local_rulebase_name, r_dev.local_rulebase_name, r_dev.mgm_id, FALSE, 1) RETURNING id INTO i_new_rulebase_id; -- now update references in all rules to the newly created rulebase UPDATE rule SET rulebase_id=i_new_rulebase_id WHERE dev_id=r_dev.dev_id; @@ -941,8 +941,8 @@ AS $function$ SELECT INTO r_dev_null * FROM rulebase WHERE name=r_dev.global_rulebase_name; IF NOT FOUND AND r_dev.global_rulebase_name IS NOT NULL THEN - INSERT INTO rulebase (name, uid, mgm_id, is_global, created) - VALUES (r_dev.global_rulebase_name, r_dev.global_rulebase_name, r_dev.mgm_id, TRUE, 1) + INSERT INTO rulebase (name, uid, mgm_id, is_global, created) + VALUES (r_dev.global_rulebase_name, r_dev.global_rulebase_name, r_dev.mgm_id, TRUE, 1) RETURNING id INTO i_new_rulebase_id; -- now update references in all rules to the newly created rulebase UPDATE rule SET rulebase_id=i_new_rulebase_id WHERE dev_id=r_dev.dev_id; @@ -950,9 +950,9 @@ AS $function$ END IF; END LOOP; - -- now check for remaining rules without rulebase_id + -- now check for remaining rules without rulebase_id -- TODO: decide how to deal with this - ONLY DUMMY SOLUTION FOR NOW - FOR r_rule IN + FOR r_rule IN SELECT * FROM rule WHERE rulebase_id IS NULL -- how do we deal with this? we simply pick the smallest rulebase id for now LOOP @@ -962,9 +962,9 @@ AS $function$ -- now we can add the "not null" constraint for rule.rulebase_id IF EXISTS ( - SELECT 1 + SELECT 1 FROM information_schema.columns - WHERE table_name = 'rule' + WHERE table_name = 'rule' AND column_name = 'rulebase_id' AND is_nullable = 'YES' ) THEN @@ -974,7 +974,7 @@ AS $function$ END; $function$; --- in this migration, in scenarios where a rulebase is used on more than one gateway, +-- in this migration, in scenarios where a rulebase is used on more than one gateway, -- only the rules of the first gw get a rulebase_id, the others (copies) will be deleted CREATE OR REPLACE FUNCTION migrateToRulebases() RETURNS VOID LANGUAGE plpgsql @@ -1006,7 +1006,7 @@ CREATE OR REPLACE FUNCTION public.get_rulebase_for_owner(rulebase_row rulebase, RETURNS SETOF rule LANGUAGE plpgsql STABLE -AS +AS $function$ BEGIN RETURN QUERY @@ -1079,7 +1079,7 @@ $$; -- add new compliance tables -CREATE TABLE IF NOT EXISTS compliance.policy +CREATE TABLE IF NOT EXISTS compliance.policy ( id SERIAL PRIMARY KEY, name TEXT, @@ -1157,60 +1157,60 @@ PRIMARY KEY (network_zone_id, ip_range_start, ip_range_end, created); -- add FKs -ALTER TABLE compliance.network_zone +ALTER TABLE compliance.network_zone DROP CONSTRAINT IF EXISTS compliance_criterion_network_zone_foreign_key; -ALTER TABLE compliance.network_zone -ADD CONSTRAINT compliance_criterion_network_zone_foreign_key -FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) +ALTER TABLE compliance.network_zone +ADD CONSTRAINT compliance_criterion_network_zone_foreign_key +FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) ON UPDATE RESTRICT ON DELETE CASCADE; -ALTER TABLE compliance.ip_range +ALTER TABLE compliance.ip_range DROP CONSTRAINT IF EXISTS compliance_criterion_ip_range_foreign_key; -ALTER TABLE compliance.ip_range -ADD CONSTRAINT compliance_criterion_ip_range_foreign_key -FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) +ALTER TABLE compliance.ip_range +ADD CONSTRAINT compliance_criterion_ip_range_foreign_key +FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) ON UPDATE RESTRICT ON DELETE CASCADE; -ALTER TABLE compliance.network_zone_communication +ALTER TABLE compliance.network_zone_communication DROP CONSTRAINT IF EXISTS compliance_criterion_network_zone_communication_foreign_key; -ALTER TABLE compliance.network_zone_communication -ADD CONSTRAINT compliance_criterion_network_zone_communication_foreign_key -FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) +ALTER TABLE compliance.network_zone_communication +ADD CONSTRAINT compliance_criterion_network_zone_communication_foreign_key +FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) ON UPDATE RESTRICT ON DELETE CASCADE; -ALTER TABLE compliance.policy_criterion +ALTER TABLE compliance.policy_criterion DROP CONSTRAINT IF EXISTS compliance_policy_policy_criterion_foreign_key; -ALTER TABLE compliance.policy_criterion -ADD CONSTRAINT compliance_policy_policy_criterion_foreign_key -FOREIGN KEY (policy_id) REFERENCES compliance.policy(id) +ALTER TABLE compliance.policy_criterion +ADD CONSTRAINT compliance_policy_policy_criterion_foreign_key +FOREIGN KEY (policy_id) REFERENCES compliance.policy(id) ON UPDATE RESTRICT ON DELETE CASCADE; -ALTER TABLE compliance.policy_criterion +ALTER TABLE compliance.policy_criterion DROP CONSTRAINT IF EXISTS compliance_criterion_policy_criterion_foreign_key; -ALTER TABLE compliance.policy_criterion -ADD CONSTRAINT compliance_criterion_policy_criterion_foreign_key -FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) +ALTER TABLE compliance.policy_criterion +ADD CONSTRAINT compliance_criterion_policy_criterion_foreign_key +FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) ON UPDATE RESTRICT ON DELETE CASCADE; -ALTER TABLE compliance.violation +ALTER TABLE compliance.violation DROP CONSTRAINT IF EXISTS compliance_policy_violation_foreign_key; -ALTER TABLE compliance.violation -ADD CONSTRAINT compliance_policy_violation_foreign_key -FOREIGN KEY (policy_id) REFERENCES compliance.policy(id) +ALTER TABLE compliance.violation +ADD CONSTRAINT compliance_policy_violation_foreign_key +FOREIGN KEY (policy_id) REFERENCES compliance.policy(id) ON UPDATE RESTRICT ON DELETE CASCADE; -ALTER TABLE compliance.violation +ALTER TABLE compliance.violation DROP CONSTRAINT IF EXISTS compliance_criterion_violation_foreign_key; -ALTER TABLE compliance.violation -ADD CONSTRAINT compliance_criterion_violation_foreign_key -FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) +ALTER TABLE compliance.violation +ADD CONSTRAINT compliance_criterion_violation_foreign_key +FOREIGN KEY (criterion_id) REFERENCES compliance.criterion(id) ON UPDATE RESTRICT ON DELETE CASCADE; -ALTER TABLE compliance.violation +ALTER TABLE compliance.violation DROP CONSTRAINT IF EXISTS compliance_rule_violation_foreign_key; -ALTER TABLE compliance.violation -ADD CONSTRAINT compliance_rule_violation_foreign_key -FOREIGN KEY (rule_id) REFERENCES public.rule(rule_id) +ALTER TABLE compliance.violation +ADD CONSTRAINT compliance_rule_violation_foreign_key +FOREIGN KEY (rule_id) REFERENCES public.rule(rule_id) ON UPDATE RESTRICT ON DELETE CASCADE; -- add report type Compliance @@ -1228,13 +1228,13 @@ WHERE (removed IS NULL); -- add config parameter debugConfig if not exists -INSERT INTO config (config_key, config_value, config_user) +INSERT INTO config (config_key, config_value, config_user) VALUES ('debugConfig', '{"debugLevel":8, "extendedLogComplianceCheck":true, "extendedLogReportGeneration":true, "extendedLogScheduler":true}', 0) ON CONFLICT (config_key, config_user) DO NOTHING; -- add config parameter complianceCheckPolicy if not exists -INSERT INTO config (config_key, config_value, config_user) +INSERT INTO config (config_key, config_value, config_user) VALUES ('complianceCheckPolicy', '0', 0) ON CONFLICT (config_key, config_user) DO NOTHING; @@ -1259,10 +1259,10 @@ ALTER TABLE compliance.violation ADD COLUMN IF NOT EXISTS mgmt_uid TEXT; -- ); --- ALTER TABLE compliance.assessability_issue +-- ALTER TABLE compliance.assessability_issue -- DROP CONSTRAINT IF EXISTS compliance_assessability_issue_type_foreign_key; -- ALTER TABLE compliance.assessability_issue ADD CONSTRAINT compliance_assessability_issue_type_foreign_key FOREIGN KEY (type_id) REFERENCES compliance.assessability_issue_type(type_id) ON UPDATE RESTRICT ON DELETE CASCADE; --- ALTER TABLE compliance.assessability_issue +-- ALTER TABLE compliance.assessability_issue -- DROP CONSTRAINT IF EXISTS compliance_assessability_issue_violation_foreign_key; -- ALTER TABLE compliance.assessability_issue ADD CONSTRAINT compliance_assessability_issue_violation_foreign_key FOREIGN KEY (violation_id) REFERENCES compliance.violation(id) ON UPDATE RESTRICT ON DELETE CASCADE; @@ -1287,9 +1287,9 @@ END$$; -- add new report template for compliance: unresolved violations -INSERT INTO "report_template" ("report_filter","report_template_name","report_template_comment","report_template_owner", "report_parameters") +INSERT INTO "report_template" ("report_filter","report_template_name","report_template_comment","report_template_owner", "report_parameters") VALUES ('action=accept', - 'Compliance: Unresolved violations','T0108', 0, + 'Compliance: Unresolved violations','T0108', 0, '{"report_type":31,"device_filter":{"management":[]}, "time_filter": { "is_shortcut": true, @@ -1310,9 +1310,9 @@ ON CONFLICT (report_template_name) DO NOTHING; -- add new report template for compliance: diffs -INSERT INTO "report_template" ("report_filter","report_template_name","report_template_comment","report_template_owner", "report_parameters") +INSERT INTO "report_template" ("report_filter","report_template_name","report_template_comment","report_template_owner", "report_parameters") VALUES ('action=accept', - 'Compliance: Diffs','T0109', 0, + 'Compliance: Diffs','T0109', 0, '{"report_type":32,"device_filter":{"management":[]}, "time_filter": { "is_shortcut": true, @@ -1339,13 +1339,13 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- add parameter to persist report scheduler configs to config -INSERT INTO config (config_key, config_value, config_user) +INSERT INTO config (config_key, config_value, config_user) VALUES ('reportSchedulerConfig', '', 0) ON CONFLICT (config_key, config_user) DO NOTHING; -- add parameter to choose order by column of network matrix between name and id -INSERT INTO config (config_key, config_value, config_user) +INSERT INTO config (config_key, config_value, config_user) VALUES ('complianceCheckSortMatrixByID', 'false', 0) ON CONFLICT (config_key, config_user) DO NOTHING; @@ -1370,11 +1370,11 @@ INSERT INTO config (config_key, config_value, config_user) VALUES ('internalZone -- auto calculate special zone parameters -INSERT INTO config (config_key, config_value, config_user) +INSERT INTO config (config_key, config_value, config_user) VALUES ('autoCalculateInternetZone', 'true', 0) ON CONFLICT (config_key, config_user) DO NOTHING; -INSERT INTO config (config_key, config_value, config_user) +INSERT INTO config (config_key, config_value, config_user) VALUES ('autoCalculateUndefinedInternalZone', 'true', 0) ON CONFLICT (config_key, config_user) DO NOTHING; @@ -1411,7 +1411,7 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- TODO: fill all rulebase_id s and then add not null constraint --- TODOs +-- TODOs -- Rename table rulebase_on_gateways to gateway_rulebase to get correct plural gateway_rulebases in hasura @@ -1437,7 +1437,7 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- rule_last_hit -- rule_uid -- dev_id --- # here we do not have any rule details +-- # here we do not have any rule details -- } -- name: dev_name -- rulebase_on_gateways(order_by: {order_no: asc}) { @@ -1449,7 +1449,7 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- rules { -- mgm_id: mgm_id -- rule_metadatum { --- # here, the rule_metadata is always empty! +-- # here, the rule_metadata is always empty! -- rule_last_hit -- } -- ...ruleOverview @@ -1471,7 +1471,7 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- - statistics (optional: only count rules per gw which are active on gw) -- - adjust report tests (add column) --- import install on information (need to find out, where it is encoded) from +-- import install on information (need to find out, where it is encoded) from -- - fortimanger - simply add name of current gw? -- - fortios - simply add name of current gw? -- - others? - simply add name of current gw? @@ -1514,7 +1514,7 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- disabled in UI: -- recertification.razor -- in report.razor: --- - RSB +-- - RSB -- - TicketCreate Komponente -- 2024-10-09 planning @@ -1524,7 +1524,7 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- - instead get current config with every import -- - id for gateway needs to be fixated: --- - check point: +-- - check point: -- - read interface information from show-gateways-and-servers details-level=full -- - where to get routing infos? -- - optional: also get publish time per policy (push): @@ -1537,16 +1537,16 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- - goal: -- - in device table: -- - for CP only save policy-name per gateway (gotten from show-gateways-and-servers --- - in config file storage: +-- - in config file storage: -- - store all policies with the management rathen than with the gateway? -- - per gateway only store the ordered mapping gw --> policies -- - also allow for mapping a gateway to a policy from the manager's super-manager --- - TODO: set is_super_manager flag = true for MDS +-- - TODO: set is_super_manager flag = true for MDS -- { -- "ConfigFormat": "NORMALIZED", --- "ManagerSet": [ +-- "ManagerSet": [ -- { -- "ManagerUid": "6ae3760206b9bfbd2282b5964f6ea07869374f427533c72faa7418c28f7a77f2", -- "ManagerName": "schting2", @@ -1576,7 +1576,7 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- "second-layer", -- ":", -- ] --- EnforcedNatPolicyUids: List[str] = [] +-- EnforcedNatPolicyUids: List[str] = [] -- ] -- } -- ] @@ -1590,7 +1590,7 @@ ON CONFLICT (config_key, config_user) DO NOTHING; -- - get reports working -- - valentin: open issues for k01 UI problems -- - decide how to implement ordered layer (all must match) vs. e.g. global policies (first match) --- - allow for also importing native configs from file +-- - allow for also importing native configs from file -- TODOs after full importer migration @@ -1619,4 +1619,13 @@ insert into stm_dev_typ (dev_typ_id,dev_typ_name,dev_typ_version,dev_typ_manufac insert into stm_dev_typ (dev_typ_id,dev_typ_name,dev_typ_version,dev_typ_manufacturer,dev_typ_predef_svc,dev_typ_is_multi_mgmt,dev_typ_is_mgmt,is_pure_routing_device) VALUES (29,'Cisco Asa on FirePower','9','Cisco','',false,true,false) - ON CONFLICT (dev_typ_id) DO NOTHING; \ No newline at end of file + ON CONFLICT (dev_typ_id) DO NOTHING; + +CREATE TABLE refresh_tokens ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES uiuser(uiuser_id) ON DELETE CASCADE, + token_hash VARCHAR(88) UNIQUE NOT NULL, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + revoked_at TIMESTAMP WITH TIME ZONE NULL +); \ No newline at end of file From 138725c8e9e95046ff8512df1e1da7906bfe47ac Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Thu, 6 Nov 2025 09:48:16 +0100 Subject: [PATCH 07/12] [+] create table refresh tokens [~] create table if not exists refresh token for upgrade script 9.0 --- .../sql/creation/fworch-create-tables.sql | 33 ++++++++++++------- roles/database/files/upgrade/9.0.sql | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/roles/database/files/sql/creation/fworch-create-tables.sql b/roles/database/files/sql/creation/fworch-create-tables.sql index 0613d81a3..74526e8dc 100755 --- a/roles/database/files/sql/creation/fworch-create-tables.sql +++ b/roles/database/files/sql/creation/fworch-create-tables.sql @@ -6,7 +6,7 @@ Contact https://cactus.de/fworch Database PostgreSQL 9-13 */ -/* Create Sequence +/* Create Sequence the abs_hange_id is needed as it is incremented across 4 different tables @@ -59,7 +59,7 @@ Create table "management" -- contains an entry for each firewall management syst "mgm_name" Varchar NOT NULL, "mgm_comment" Text, "cloud_tenant_id" VARCHAR, - "cloud_subscription_id" VARCHAR, + "cloud_subscription_id" VARCHAR, "mgm_create" Timestamp NOT NULL Default now(), "mgm_update" Timestamp NOT NULL Default now(), "import_credential_id" Integer NOT NULL, @@ -212,7 +212,7 @@ Create table "rule_metadata" "last_change_admin" Integer, "rule_decert_date" Timestamp, "rule_recertification_comment" Varchar, - primary key ("rule_metadata_id") + primary key ("rule_metadata_id") ); -- adding direct link tables rule_[svc|nwobj|user]_resolved to make report object export easier @@ -485,7 +485,7 @@ Create table "tenant" "tenant_comment" Text, "tenant_report" Boolean Default true, "tenant_can_view_all_devices" Boolean NOT NULL Default false, - "tenant_is_superadmin" Boolean NOT NULL default false, + "tenant_is_superadmin" Boolean NOT NULL default false, "tenant_create" Timestamp NOT NULL Default now(), primary key ("tenant_id") ); @@ -1018,7 +1018,7 @@ Create table "report_schedule" "report_template_id" Integer, --FK "report_schedule_owner" Integer NOT NULL, --FK "report_schedule_start_time" Timestamp NOT NULL, -- if day is bigger than 28, simply use the 1st of the next month, 00:00 am - "report_schedule_repeat" Integer Not NULL Default 0, -- 0 do not repeat, 1 daily, 2 weekly, 3 monthly, 4 yearly + "report_schedule_repeat" Integer Not NULL Default 0, -- 0 do not repeat, 1 daily, 2 weekly, 3 monthly, 4 yearly "report_schedule_every" Integer Not NULL Default 1, -- x - every x days/weeks/months/years "report_schedule_active" Boolean Default TRUE, "report_schedule_repetitions" Integer, @@ -1124,7 +1124,7 @@ create table owner_network port int, ip_proto_id int, nw_type int, - import_source Varchar default 'manual', + import_source Varchar default 'manual', is_deleted boolean default false, custom_type int ); @@ -1156,7 +1156,7 @@ create table recertification owner_recert_id bigint ); -Create Table IF NOT EXISTS "rule_enforced_on_gateway" +Create Table IF NOT EXISTS "rule_enforced_on_gateway" ( "rule_id" Integer NOT NULL, "dev_id" Integer, -- NULL if rule is available for all gateways of its management @@ -1238,7 +1238,7 @@ CREATE TYPE rule_field_enum AS ENUM ('source', 'destination', 'service', 'rule', CREATE TYPE action_enum AS ENUM ('create', 'delete', 'modify', 'unchanged', 'addAfterCreation'); -- create tables -create table request.reqtask +create table request.reqtask ( id BIGSERIAL PRIMARY KEY, title VARCHAR, @@ -1267,7 +1267,7 @@ create table request.reqtask mgm_id int ); -create table request.reqelement +create table request.reqelement ( id BIGSERIAL PRIMARY KEY, request_action action_enum NOT NULL default 'create', @@ -1288,7 +1288,7 @@ create table request.reqelement name varchar ); -create table request.approval +create table request.approval ( id BIGSERIAL PRIMARY KEY, task_id bigint, @@ -1305,7 +1305,7 @@ create table request.approval state_id int NOT NULL ); -create table request.ticket +create table request.ticket ( id BIGSERIAL PRIMARY KEY, title VARCHAR NOT NULL, @@ -1326,7 +1326,7 @@ create table request.ticket ticket_priority int ); -create table request.comment +create table request.comment ( id BIGSERIAL PRIMARY KEY, ref_id bigint, @@ -1663,3 +1663,12 @@ create table modelling.change_history change_time Timestamp default now(), change_source Varchar default 'manual' ); + +CREATE TABLE refresh_tokens ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES uiuser(uiuser_id) ON DELETE CASCADE, + token_hash VARCHAR(88) UNIQUE NOT NULL, + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + revoked_at TIMESTAMP WITH TIME ZONE NULL +); \ No newline at end of file diff --git a/roles/database/files/upgrade/9.0.sql b/roles/database/files/upgrade/9.0.sql index d771c0ffe..4480b97df 100644 --- a/roles/database/files/upgrade/9.0.sql +++ b/roles/database/files/upgrade/9.0.sql @@ -1621,7 +1621,7 @@ insert into stm_dev_typ (dev_typ_id,dev_typ_name,dev_typ_version,dev_typ_manufac VALUES (29,'Cisco Asa on FirePower','9','Cisco','',false,true,false) ON CONFLICT (dev_typ_id) DO NOTHING; -CREATE TABLE refresh_tokens ( +CREATE TABLE IF NOT EXISTS refresh_tokens ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES uiuser(uiuser_id) ON DELETE CASCADE, token_hash VARCHAR(88) UNIQUE NOT NULL, From ad8ca5d6bd24255d374413203d3570521517c9d3 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Thu, 6 Nov 2025 16:19:25 +0100 Subject: [PATCH 08/12] [+] Token Service [+] Token validation on api querys --- .../FWO.Api.Client/GraphQlApiConnection.cs | 32 +++++ .../FWO.Api.Client/ITokenRefreshService.cs | 20 +++ .../ui/files/FWO.UI/Auth/AuthStateProvider.cs | 13 +- roles/ui/files/FWO.UI/Program.cs | 13 ++ .../ui/files/FWO.UI/Services/TokenService.cs | 114 ++++++++++++++++++ 5 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 roles/lib/files/FWO.Api.Client/ITokenRefreshService.cs create mode 100644 roles/ui/files/FWO.UI/Services/TokenService.cs diff --git a/roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs b/roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs index 8fd12808b..c5497d607 100644 --- a/roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs +++ b/roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs @@ -15,6 +15,7 @@ public class GraphQlApiConnection : ApiConnection public string ApiServerUri { get; private set; } private GraphQLHttpClient graphQlClient; + private ITokenRefreshService? tokenRefreshService; private string prevRole = ""; @@ -43,6 +44,13 @@ private void Initialize(string ApiServerUri) graphQlClient.HttpClient.Timeout = new TimeSpan(1, 0, 0); } + /// + /// Sets the token refresh service for automatic token renewal. + /// + public void SetTokenRefreshService(ITokenRefreshService? service) + { + tokenRefreshService = service; + } public GraphQlApiConnection(string ApiServerUri, string jwt) { @@ -135,6 +143,8 @@ public override async Task SendQueryAsync( { try { + await EnsureValidTokenAsync(); + Log.WriteDebug("API call", $"Sending API call {operationName} in role {GetActRole()}: {query.Substring(0, Math.Min(query.Length, 70)).Replace(Environment.NewLine, "")}... " + ( variables != null ? $"with variables: {JsonSerializer.Serialize(variables).Substring(0, Math.Min(JsonSerializer.Serialize(variables).Length, 50)).Replace(Environment.NewLine, "")}..." : "" )); GraphQLResponse response = await graphQlClient.SendQueryAsync(query, variables, operationName); @@ -209,6 +219,28 @@ public override GraphQlApiSubscription GetSubscription } } + private async Task EnsureValidTokenAsync(bool forceRefresh = false) + { + if(tokenRefreshService == null) + return true; + + if(forceRefresh || tokenRefreshService.IsAccessTokenExpired()) + { + Log.WriteDebug("Token Check", "Access token expired or expiring soon, refreshing..."); + bool refreshed = await tokenRefreshService.RefreshAccessTokenAsync(); + + if(!refreshed) + { + Log.WriteError("Token Check", "Failed to refresh expired token"); + return false; + } + + Log.WriteDebug("Token Check", "Token refreshed successfully"); + } + + return true; + } + protected override void Dispose(bool disposing) { if (disposing) diff --git a/roles/lib/files/FWO.Api.Client/ITokenRefreshService.cs b/roles/lib/files/FWO.Api.Client/ITokenRefreshService.cs new file mode 100644 index 000000000..8177e9151 --- /dev/null +++ b/roles/lib/files/FWO.Api.Client/ITokenRefreshService.cs @@ -0,0 +1,20 @@ +namespace FWO.Api.Client +{ + /// + /// Interface for token refresh operations. + /// Allows GraphQlApiConnection to refresh tokens without depending on UI layer. + /// + public interface ITokenRefreshService + { + /// + /// Checks if the current access token is expired or expiring soon. + /// + bool IsAccessTokenExpired(); + + /// + /// Refreshes the access token using the refresh token. + /// + /// True if refresh was successful, false otherwise. + Task RefreshAccessTokenAsync(); + } +} diff --git a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs index 231641221..3baf4c5e8 100644 --- a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs +++ b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs @@ -21,7 +21,14 @@ public class AuthStateProvider : AuthenticationStateProvider { private ClaimsPrincipal user = new(new ClaimsIdentity()); - public override Task GetAuthenticationStateAsync() + private readonly TokenService tokenService; + + public AuthStateProvider(TokenService tokenService) + { + this.tokenService = tokenService; + } + + public override Task GetAuthenticationStateAsync() { return Task.FromResult(new AuthenticationState(user)); } @@ -39,6 +46,8 @@ public async Task> Authenticate(string username, string TokenPair tokenPair = System.Text.Json.JsonSerializer.Deserialize(tokenPairJson) ?? throw new ArgumentException("failed to deserialize token pair"); + tokenService.SetTokenPair(tokenPair); + string jwtString = tokenPair.AccessToken ?? throw new ArgumentException("no access token in response"); await Authenticate(jwtString, apiConnection, middlewareClient, globalConfig, userConfig, circuitHandler, sessionStorage); @@ -232,6 +241,6 @@ private static async Task> GetClaimList(string jwtString, string cl } return claimList; } - } + } } diff --git a/roles/ui/files/FWO.UI/Program.cs b/roles/ui/files/FWO.UI/Program.cs index a3fec7368..9ba8a2675 100644 --- a/roles/ui/files/FWO.UI/Program.cs +++ b/roles/ui/files/FWO.UI/Program.cs @@ -58,6 +58,19 @@ builder.Services.AddScoped(_ => new GraphQlApiConnection(ApiUri)); builder.Services.AddScoped(_ => new MiddlewareClient(MiddlewareUri)); +builder.Services.AddScoped(_ => +{ + var middlewareClient = _.GetRequiredService(); + var apiConnection = _.GetRequiredService(); + var tokenService = new TokenService(middlewareClient, apiConnection); + + if(apiConnection is GraphQlApiConnection graphQlConn) + { + graphQlConn.SetTokenRefreshService(tokenService); + } + + return tokenService; +}); // Create "anonymous" (empty) jwt MiddlewareClient middlewareClient = new MiddlewareClient(MiddlewareUri); diff --git a/roles/ui/files/FWO.UI/Services/TokenService.cs b/roles/ui/files/FWO.UI/Services/TokenService.cs new file mode 100644 index 000000000..f2c64a23c --- /dev/null +++ b/roles/ui/files/FWO.UI/Services/TokenService.cs @@ -0,0 +1,114 @@ +using FWO.Api.Client; +using FWO.Data.Middleware; +using FWO.Middleware.Client; +using FWO.Logging; +using System.IdentityModel.Tokens.Jwt; + +namespace FWO.Ui.Services +{ + /// + /// Manages token pairs (access + refresh tokens) for the current user session. + /// + public class TokenService : ITokenRefreshService // ✅ Implements interface + { + private readonly MiddlewareClient middlewareClient; + private readonly ApiConnection apiConnection; + private TokenPair? currentTokenPair; + private readonly JwtSecurityTokenHandler jwtHandler = new(); + private readonly SemaphoreSlim refreshSemaphore = new(1, 1); + + public TokenService(MiddlewareClient middlewareClient, ApiConnection apiConnection) + { + this.middlewareClient = middlewareClient; + this.apiConnection = apiConnection; + } + + public void SetTokenPair(TokenPair tokenPair) + { + currentTokenPair = tokenPair; + } + + public async Task RefreshAccessTokenAsync() + { + await refreshSemaphore.WaitAsync(); + + try + { + // Double-check if still expired + if(!IsAccessTokenExpired()) + { + return true; + } + + if(currentTokenPair?.RefreshToken == null) + { + Log.WriteWarning("Token Refresh", "No refresh token available"); + return false; + } + + Log.WriteDebug("Token Refresh", "Attempting to refresh access token"); + + var refreshRequest = new RefreshTokenRequest + { + RefreshToken = currentTokenPair.RefreshToken + }; + + var response = await middlewareClient.RefreshToken(refreshRequest); + + if(response.IsSuccessful && response.Data != null) + { + currentTokenPair = response.Data; + + // Update auth headers + apiConnection.SetAuthHeader(currentTokenPair.AccessToken); + middlewareClient.SetAuthenticationToken(currentTokenPair.AccessToken); + + Log.WriteInfo("Token Refresh", "Access token refreshed successfully"); + return true; + } + else + { + Log.WriteError("Token Refresh", $"Failed to refresh token: {response.ErrorMessage}"); + return false; + } + } + catch(Exception ex) + { + Log.WriteError("Token Refresh", "Exception during token refresh", ex); + return false; + } + finally + { + refreshSemaphore.Release(); + } + } + + public bool IsAccessTokenExpired() + { + if(string.IsNullOrEmpty(currentTokenPair?.AccessToken)) + return true; + + try + { + var token = jwtHandler.ReadJwtToken(currentTokenPair.AccessToken); + // Refresh 2 minutes before actual expiry + return token.ValidTo <= DateTime.UtcNow.AddMinutes(2); + } + catch(Exception ex) + { + Log.WriteWarning("Token Check", $"Failed to read JWT: {ex.Message}"); + return true; + } + } + + public void ClearTokens() + { + currentTokenPair = null; + } + + public TokenPair? GetCurrentTokenPair() + { + return currentTokenPair; + } + } +} From 5674b8375adc39fe8757c61d978fc7784d027727 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Fri, 7 Nov 2025 12:09:00 +0100 Subject: [PATCH 09/12] [~] anon auth failing --- .../ui/files/FWO.UI/Services/TokenService.cs | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/roles/ui/files/FWO.UI/Services/TokenService.cs b/roles/ui/files/FWO.UI/Services/TokenService.cs index f2c64a23c..adee5624c 100644 --- a/roles/ui/files/FWO.UI/Services/TokenService.cs +++ b/roles/ui/files/FWO.UI/Services/TokenService.cs @@ -9,7 +9,7 @@ namespace FWO.Ui.Services /// /// Manages token pairs (access + refresh tokens) for the current user session. /// - public class TokenService : ITokenRefreshService // ✅ Implements interface + public class TokenService : ITokenRefreshService { private readonly MiddlewareClient middlewareClient; private readonly ApiConnection apiConnection; @@ -34,7 +34,6 @@ public async Task RefreshAccessTokenAsync() try { - // Double-check if still expired if(!IsAccessTokenExpired()) { return true; @@ -48,12 +47,12 @@ public async Task RefreshAccessTokenAsync() Log.WriteDebug("Token Refresh", "Attempting to refresh access token"); - var refreshRequest = new RefreshTokenRequest + RefreshTokenRequest refreshRequest = new RefreshTokenRequest { RefreshToken = currentTokenPair.RefreshToken }; - var response = await middlewareClient.RefreshToken(refreshRequest); + RestSharp.RestResponse response = await middlewareClient.RefreshToken(refreshRequest); if(response.IsSuccessful && response.Data != null) { @@ -90,9 +89,9 @@ public bool IsAccessTokenExpired() try { - var token = jwtHandler.ReadJwtToken(currentTokenPair.AccessToken); - // Refresh 2 minutes before actual expiry - return token.ValidTo <= DateTime.UtcNow.AddMinutes(2); + JwtSecurityToken token = jwtHandler.ReadJwtToken(currentTokenPair.AccessToken); + + return token.ValidTo <= DateTime.UtcNow.AddMinutes(1); } catch(Exception ex) { @@ -100,15 +99,5 @@ public bool IsAccessTokenExpired() return true; } } - - public void ClearTokens() - { - currentTokenPair = null; - } - - public TokenPair? GetCurrentTokenPair() - { - return currentTokenPair; - } } } From c467a334236351d636042d28e212bba1b1a37109 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Fri, 7 Nov 2025 13:56:52 +0100 Subject: [PATCH 10/12] [+] Add session storage support for token management --- .../ui/files/FWO.UI/Auth/AuthStateProvider.cs | 14 +++--- roles/ui/files/FWO.UI/Program.cs | 15 +----- .../ui/files/FWO.UI/Services/TokenService.cs | 46 +++++++++++++++---- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs index 3baf4c5e8..fa63fcd72 100644 --- a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs +++ b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs @@ -28,12 +28,14 @@ public AuthStateProvider(TokenService tokenService) this.tokenService = tokenService; } - public override Task GetAuthenticationStateAsync() - { - return Task.FromResult(new AuthenticationState(user)); - } + public override async Task GetAuthenticationStateAsync() + { + await tokenService.InitializeAsync(); + + return await Task.FromResult(new AuthenticationState(user)); + } - public async Task> Authenticate(string username, string password, ApiConnection apiConnection, MiddlewareClient middlewareClient, + public async Task> Authenticate(string username, string password, ApiConnection apiConnection, MiddlewareClient middlewareClient, GlobalConfig globalConfig, UserConfig userConfig, ProtectedSessionStorage sessionStorage, CircuitHandlerService circuitHandler) { // There is no jwt in session storage. Get one from auth module. @@ -46,7 +48,7 @@ public async Task> Authenticate(string username, string TokenPair tokenPair = System.Text.Json.JsonSerializer.Deserialize(tokenPairJson) ?? throw new ArgumentException("failed to deserialize token pair"); - tokenService.SetTokenPair(tokenPair); + await tokenService.SetTokenPair(tokenPair); string jwtString = tokenPair.AccessToken ?? throw new ArgumentException("no access token in response"); diff --git a/roles/ui/files/FWO.UI/Program.cs b/roles/ui/files/FWO.UI/Program.cs index 9ba8a2675..95dbb4c69 100644 --- a/roles/ui/files/FWO.UI/Program.cs +++ b/roles/ui/files/FWO.UI/Program.cs @@ -58,19 +58,8 @@ builder.Services.AddScoped(_ => new GraphQlApiConnection(ApiUri)); builder.Services.AddScoped(_ => new MiddlewareClient(MiddlewareUri)); -builder.Services.AddScoped(_ => -{ - var middlewareClient = _.GetRequiredService(); - var apiConnection = _.GetRequiredService(); - var tokenService = new TokenService(middlewareClient, apiConnection); - - if(apiConnection is GraphQlApiConnection graphQlConn) - { - graphQlConn.SetTokenRefreshService(tokenService); - } - - return tokenService; -}); +builder.Services.AddScoped(); +builder.Services.AddScoped(_ => _.GetRequiredService()); // Create "anonymous" (empty) jwt MiddlewareClient middlewareClient = new MiddlewareClient(MiddlewareUri); diff --git a/roles/ui/files/FWO.UI/Services/TokenService.cs b/roles/ui/files/FWO.UI/Services/TokenService.cs index adee5624c..19acd88b0 100644 --- a/roles/ui/files/FWO.UI/Services/TokenService.cs +++ b/roles/ui/files/FWO.UI/Services/TokenService.cs @@ -3,6 +3,7 @@ using FWO.Middleware.Client; using FWO.Logging; using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; namespace FWO.Ui.Services { @@ -11,21 +12,35 @@ namespace FWO.Ui.Services /// public class TokenService : ITokenRefreshService { - private readonly MiddlewareClient middlewareClient; - private readonly ApiConnection apiConnection; private TokenPair? currentTokenPair; + private readonly ProtectedSessionStorage sessionStorage; private readonly JwtSecurityTokenHandler jwtHandler = new(); private readonly SemaphoreSlim refreshSemaphore = new(1, 1); + private readonly MiddlewareClient middlewareClient; + private readonly ApiConnection apiConnection; + private const string TOKEN_PAIR_KEY = "token_pair"; - public TokenService(MiddlewareClient middlewareClient, ApiConnection apiConnection) + public TokenService(MiddlewareClient middlewareClient, ApiConnection apiConnection, ProtectedSessionStorage sessionStorage) { this.middlewareClient = middlewareClient; this.apiConnection = apiConnection; + this.sessionStorage = sessionStorage; + } + + public async Task InitializeAsync() + { + ProtectedBrowserStorageResult result = await sessionStorage.GetAsync(TOKEN_PAIR_KEY); + + if(result.Success && result.Value != null) + { + currentTokenPair = result.Value; + } } - public void SetTokenPair(TokenPair tokenPair) + public async Task SetTokenPair(TokenPair tokenPair) { currentTokenPair = tokenPair; + await sessionStorage.SetAsync(TOKEN_PAIR_KEY, tokenPair); } public async Task RefreshAccessTokenAsync() @@ -41,8 +56,12 @@ public async Task RefreshAccessTokenAsync() if(currentTokenPair?.RefreshToken == null) { - Log.WriteWarning("Token Refresh", "No refresh token available"); - return false; + await InitializeAsync(); + + if(currentTokenPair?.RefreshToken == null) + { + return false; + } } Log.WriteDebug("Token Refresh", "Attempting to refresh access token"); @@ -56,13 +75,12 @@ public async Task RefreshAccessTokenAsync() if(response.IsSuccessful && response.Data != null) { - currentTokenPair = response.Data; + await SetTokenPair(response.Data); - // Update auth headers - apiConnection.SetAuthHeader(currentTokenPair.AccessToken); - middlewareClient.SetAuthenticationToken(currentTokenPair.AccessToken); + apiConnection.SetAuthHeader(response.Data.AccessToken); Log.WriteInfo("Token Refresh", "Access token refreshed successfully"); + return true; } else @@ -74,6 +92,7 @@ public async Task RefreshAccessTokenAsync() catch(Exception ex) { Log.WriteError("Token Refresh", "Exception during token refresh", ex); + return false; } finally @@ -85,7 +104,9 @@ public async Task RefreshAccessTokenAsync() public bool IsAccessTokenExpired() { if(string.IsNullOrEmpty(currentTokenPair?.AccessToken)) + { return true; + } try { @@ -99,5 +120,10 @@ public bool IsAccessTokenExpired() return true; } } + public async Task ClearTokenPair() + { + currentTokenPair = null; + await sessionStorage.DeleteAsync(TOKEN_PAIR_KEY); + } } } From f0de9037e627bf17aae29e450ad7c6e705eb5c78 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Mon, 17 Nov 2025 12:23:43 +0100 Subject: [PATCH 11/12] WIP --- .../ui/files/FWO.UI/Auth/AuthStateProvider.cs | 5 +- roles/ui/files/FWO.UI/Pages/Logout.razor | 1 - .../FWO.UI/Pages/Settings/SettingsRoles.razor | 1 - .../files/FWO.UI/Services/JwtEventService.cs | 96 +++++++++---------- roles/ui/files/FWO.UI/Shared/MainLayout.razor | 31 ------ 5 files changed, 49 insertions(+), 85 deletions(-) diff --git a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs index fa63fcd72..512d107ff 100644 --- a/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs +++ b/roles/ui/files/FWO.UI/Auth/AuthStateProvider.cs @@ -107,10 +107,7 @@ public async Task Authenticate(string jwtString, ApiConnection apiConnection, Mi userConfig.User.Roles = await GetAllowedRoles(userConfig.User.Jwt); userConfig.User.Ownerships = await GetAssignedOwners(userConfig.User.Jwt); circuitHandler.User = userConfig.User; - - // Add jwt expiry timer - JwtEventService.AddJwtTimers(userDn, (int)jwtReader.TimeUntilExpiry().TotalMilliseconds, 1000 * 60 * globalConfig.SessionTimeoutNoticePeriod); - + if (!userConfig.User.PasswordMustBeChanged) { NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); diff --git a/roles/ui/files/FWO.UI/Pages/Logout.razor b/roles/ui/files/FWO.UI/Pages/Logout.razor index 9c46b76e7..7cbddbbf2 100644 --- a/roles/ui/files/FWO.UI/Pages/Logout.razor +++ b/roles/ui/files/FWO.UI/Pages/Logout.razor @@ -27,7 +27,6 @@ // Clear the jwt and deauthenticate await sessionStorage.DeleteAsync("jwt"); ((AuthStateProvider)AuthService).Deauthenticate(); - JwtEventService.RemoveJwtTimers(userConfig.User.Dn); // Write an audit log that the user logged out UiUser user = userConfig.User; diff --git a/roles/ui/files/FWO.UI/Pages/Settings/SettingsRoles.razor b/roles/ui/files/FWO.UI/Pages/Settings/SettingsRoles.razor index e59c34b42..f3a36f496 100644 --- a/roles/ui/files/FWO.UI/Pages/Settings/SettingsRoles.razor +++ b/roles/ui/files/FWO.UI/Pages/Settings/SettingsRoles.razor @@ -138,7 +138,6 @@ actRole.Users.Add(user); roles[roles.FindIndex(x => x.Name == actRole.Name)] = actRole; AddMode = false; - JwtEventService.PermissionsChanged(user.Dn); Log.WriteAudit( Title: $"Roles Settings", diff --git a/roles/ui/files/FWO.UI/Services/JwtEventService.cs b/roles/ui/files/FWO.UI/Services/JwtEventService.cs index ac01859fa..7a059fb54 100644 --- a/roles/ui/files/FWO.UI/Services/JwtEventService.cs +++ b/roles/ui/files/FWO.UI/Services/JwtEventService.cs @@ -1,64 +1,64 @@ -using FWO.Basics; +using FWO.Basics; using FWO.Config.Api; namespace FWO.Ui.Services { - public static class JwtEventService - { - public static event EventHandler? OnPermissionChanged; + //public static class JwtEventService + //{ + // public static event EventHandler? OnPermissionChanged; - public static event EventHandler? OnJwtAboutToExpire; + // public static event EventHandler? OnJwtAboutToExpire; - public static event EventHandler? OnJwtExpired; + // public static event EventHandler? OnJwtExpired; - private static readonly Dictionary jwtAboutToExpireTimers = new(); + // private static readonly Dictionary jwtAboutToExpireTimers = new(); - private static readonly Dictionary jwtExpiredTimers = new(); + // private static readonly Dictionary jwtExpiredTimers = new(); - public static void PermissionsChanged(string userDn) - { - OnPermissionChanged?.Invoke(null, userDn); - } + // public static void PermissionsChanged(string userDn) + // { + // OnPermissionChanged?.Invoke(null, userDn); + // } - public static void JwtAboutToExpire(string userDn) - { - OnJwtAboutToExpire?.Invoke(null, userDn); - } + // public static void JwtAboutToExpire(string userDn) + // { + // OnJwtAboutToExpire?.Invoke(null, userDn); + // } - public static void JwtExpired(string userDn) - { - OnJwtExpired?.Invoke(null, userDn); - } + // public static void JwtExpired(string userDn) + // { + // OnJwtExpired?.Invoke(null, userDn); + // } - public static void AddJwtTimers(string userDn, int timeUntilyExpiry, int notificationTime) - { - // Dispose old timer (if existing) - RemoveJwtTimers(userDn); + // public static void AddJwtTimers(string userDn, int timeUntilyExpiry, int notificationTime) + // { + // // Dispose old timer (if existing) + // RemoveJwtTimers(userDn); - // Create new timers - if (notificationTime > 0 && timeUntilyExpiry - notificationTime > 0) - { - jwtAboutToExpireTimers[userDn] = new Timer(_ => JwtAboutToExpire(userDn), null, timeUntilyExpiry - notificationTime, int.MaxValue); - } - if (timeUntilyExpiry > 0) - { - jwtExpiredTimers[userDn] = new Timer(_ => JwtExpired(userDn), null, timeUntilyExpiry, int.MaxValue); - } - } + // // Create new timers + // if (notificationTime > 0 && timeUntilyExpiry - notificationTime > 0) + // { + // jwtAboutToExpireTimers[userDn] = new Timer(_ => JwtAboutToExpire(userDn), null, timeUntilyExpiry - notificationTime, int.MaxValue); + // } + // if (timeUntilyExpiry > 0) + // { + // jwtExpiredTimers[userDn] = new Timer(_ => JwtExpired(userDn), null, timeUntilyExpiry, int.MaxValue); + // } + // } - public static void RemoveJwtTimers(string userDn) - { - if (jwtAboutToExpireTimers.TryGetValue(userDn, out Timer? aboutToExpire)) - { - aboutToExpire.Dispose(); - jwtAboutToExpireTimers.Remove(userDn); - } + // public static void RemoveJwtTimers(string userDn) + // { + // if (jwtAboutToExpireTimers.TryGetValue(userDn, out Timer? aboutToExpire)) + // { + // aboutToExpire.Dispose(); + // jwtAboutToExpireTimers.Remove(userDn); + // } - if (jwtExpiredTimers.TryGetValue(userDn, out Timer? expired)) - { - expired.Dispose(); - jwtExpiredTimers.Remove(userDn); - } - } - } + // if (jwtExpiredTimers.TryGetValue(userDn, out Timer? expired)) + // { + // expired.Dispose(); + // jwtExpiredTimers.Remove(userDn); + // } + // } + //} } diff --git a/roles/ui/files/FWO.UI/Shared/MainLayout.razor b/roles/ui/files/FWO.UI/Shared/MainLayout.razor index d7e1ad133..76a33790b 100644 --- a/roles/ui/files/FWO.UI/Shared/MainLayout.razor +++ b/roles/ui/files/FWO.UI/Shared/MainLayout.razor @@ -21,7 +21,6 @@ @inject AuthenticationStateProvider authenticationProvider @inject ProtectedSessionStorage sessionStorage @inject CircuitHandler circuitHandler -@inject IEventMediator EventMediator @implements IDisposable @@ -116,10 +115,6 @@ user = authenticationStateTask!.Result.User; DisplayMessageInUiFunction = DisplayMessageInUi; - JwtEventService.OnPermissionChanged += OnPermissionsChanged; - JwtEventService.OnJwtAboutToExpire += OnJwtAboutToExpire; - JwtEventService.OnJwtExpired += OnJwtExpired; - alertSubscription = apiConnection.GetSubscription>(ApiExceptionHandler, OnAlertUpdate, MonitorQueries.subscribeAlertChanges); UIMessageQueTimer = new() @@ -130,18 +125,6 @@ }; UIMessageQueTimer.Elapsed += OnUIMessageTimerTick; - - EventMediator.Subscribe(nameof(CircuitHandlerService), _ => OnUserSessionClosedEvent(_.EventArgs)); - } - - private void OnUserSessionClosedEvent(UserSessionClosedEventArgs? e) - { - if (e == null || string.IsNullOrEmpty(e.UserDn)) - { - return; - } - - JwtEventService.RemoveJwtTimers(e.UserDn); } private async Task KeyHandler(KeyboardEventArgs ev) @@ -161,14 +144,6 @@ } } - private void OnJwtAboutToExpire(object? sender, string userDn) - { - if (userDn == userConfig.User.Dn) - { - ShowReloginDialog(userConfig.GetText("jwt_expiry_title"), userConfig.GetText("jwt_expiry_text"), reloginAbortable: true); - } - } - private void OnJwtExpired(object? sender, string userDn) { if (userDn == userConfig.User.Dn) @@ -477,10 +452,6 @@ public void Dispose() { - JwtEventService.OnPermissionChanged -= OnPermissionsChanged; - JwtEventService.OnJwtAboutToExpire -= OnJwtAboutToExpire; - JwtEventService.OnJwtExpired -= OnJwtExpired; - if(UIMessageQueTimer is not null) { UIMessageQueTimer.Stop(); @@ -489,7 +460,5 @@ } alertSubscription?.Dispose(); - - EventMediator.Unsubscribe(nameof(CircuitHandlerService)); } } From 1dec96c9f615317df99551dd173aae4b2f01a871 Mon Sep 17 00:00:00 2001 From: solidprogramming Date: Mon, 17 Nov 2025 12:45:55 +0100 Subject: [PATCH 12/12] . --- roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs b/roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs index 2061e1e5d..cec104978 100644 --- a/roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs +++ b/roles/lib/files/FWO.Api.Client/GraphQlApiConnection.cs @@ -143,7 +143,7 @@ public override async Task SendQueryAsync( { try { - await EnsureValidTokenAsync(); + //await EnsureValidTokenAsync(); Log.WriteDebug("API call", $"Sending API call {operationName} in role {GetActRole()}: {query.Substring(0, Math.Min(query.Length, 70)).Replace(Environment.NewLine, "")}... " + (variables != null ? $"with variables: {JsonSerializer.Serialize(variables).Substring(0, Math.Min(JsonSerializer.Serialize(variables).Length, 50)).Replace(Environment.NewLine, "")}..." : ""));