From 2ac5662765b8e7bad645937b5157a357b5a77b26 Mon Sep 17 00:00:00 2001 From: antoineatstariongroup Date: Wed, 5 Mar 2025 14:16:08 +0100 Subject: [PATCH 1/4] Support token refresh using SDK 28.2.0 --- .../AuthenticationServiceTestFixture.cs | 33 +++- COMET.Web.Common/COMET.Web.Common.csproj | 12 +- .../Extensions/ServiceCollectionExtensions.cs | 5 + .../Model/DTO/OpenIdAuthenticationDto.cs | 52 ------ .../AuthenticationService.cs | 170 +++++++++++------- .../IAuthenticationService.cs | 2 +- COMETwebapp/COMETwebapp.csproj | 2 +- 7 files changed, 141 insertions(+), 135 deletions(-) delete mode 100644 COMET.Web.Common/Model/DTO/OpenIdAuthenticationDto.cs diff --git a/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs b/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs index fe7b9ace..c6f996bb 100644 --- a/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs +++ b/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs @@ -32,6 +32,9 @@ namespace COMET.Web.Common.Tests.Services.SessionManagement using CDP4DalCommon.Authentication; + using CDP4ServicesDal; + using CDP4ServicesDal.ExternalAuthenticationProviderService; + using COMET.Web.Common.Model.DTO; using COMET.Web.Common.Services.SessionManagement; @@ -50,6 +53,9 @@ public class AuthenticationServiceTestFixture private AuthenticationService authenticationService; private AuthenticationDto authenticationDto; private Mock sessionStorageService; + private Mock openIdConnectService; + private Mock automaticTokenRefreshService; + private Credentials credentials; [SetUp] public void SetUp() @@ -57,13 +63,20 @@ public void SetUp() this.session = new Mock(); this.sessionService = new Mock(); this.sessionStorageService = new Mock(); - + this.openIdConnectService = new Mock(); + this.automaticTokenRefreshService = new Mock(); + this.sessionService.Setup(x => x.Session).Returns(this.session.Object); this.sessionService.Setup(x => x.IsSessionOpen).Returns(false); this.cometWebAuthStateProvider = new CometWebAuthStateProvider(this.sessionService.Object); - this.authenticationService = new AuthenticationService(this.sessionService.Object, this.cometWebAuthStateProvider, this.sessionStorageService.Object); + + this.authenticationService = new AuthenticationService(this.sessionService.Object, this.cometWebAuthStateProvider, this.sessionStorageService.Object, + this.openIdConnectService.Object, this.automaticTokenRefreshService.Object); + this.credentials = new Credentials(new Uri("http://localhost:5000/")); + this.session.Setup(x => x.Credentials).Returns(this.credentials); + this.authenticationDto = new AuthenticationDto { SourceAddress = "https://www.stariongroup.eu/", @@ -120,16 +133,20 @@ public async Task VerifyLoginWithDefinedScheme() this.sessionStorageService.Verify(x => x.SetItemAsync("access_token", It.IsAny(), default), Times.Never); }); - var tokenBasedAuthenticationInfo = new AuthenticationInformation("token"); - this.sessionService.Setup(x => x.AuthenticateAndOpenSession(AuthenticationSchemeKind.ExternalJwtBearer, tokenBasedAuthenticationInfo)).ReturnsAsync(Result.Ok()); - this.sessionService.Setup(x => x.AuthenticateAndOpenSession(AuthenticationSchemeKind.LocalJwtBearer, tokenBasedAuthenticationInfo)).ReturnsAsync(Result.Ok()); - + var tokenBasedAuthenticationInfo = new AuthenticationInformation(new AuthenticationTokens("token", "refresh")); + + this.sessionService.Setup(x => x.AuthenticateAndOpenSession(AuthenticationSchemeKind.ExternalJwtBearer, tokenBasedAuthenticationInfo)).ReturnsAsync(Result.Ok()) + .Callback(() => this.credentials.ProvideUserToken(tokenBasedAuthenticationInfo.Token, AuthenticationSchemeKind.ExternalJwtBearer)); + + this.sessionService.Setup(x => x.AuthenticateAndOpenSession(AuthenticationSchemeKind.LocalJwtBearer, tokenBasedAuthenticationInfo)).ReturnsAsync(Result.Ok()) + .Callback(() => this.credentials.ProvideUserToken(tokenBasedAuthenticationInfo.Token, AuthenticationSchemeKind.LocalJwtBearer)); + loginResult = await this.authenticationService.LoginAsync(AuthenticationSchemeKind.LocalJwtBearer, tokenBasedAuthenticationInfo); Assert.Multiple(() => { Assert.That(loginResult.IsSuccess, Is.EqualTo(true)); - this.sessionStorageService.Verify(x => x.SetItemAsync("access_token", tokenBasedAuthenticationInfo.Token, default), Times.Once); + this.sessionStorageService.Verify(x => x.SetItemAsync("access_token", tokenBasedAuthenticationInfo.Token.AccessToken, default), Times.Once); }); loginResult = await this.authenticationService.LoginAsync(AuthenticationSchemeKind.ExternalJwtBearer, tokenBasedAuthenticationInfo); @@ -137,7 +154,7 @@ public async Task VerifyLoginWithDefinedScheme() Assert.Multiple(() => { Assert.That(loginResult.IsSuccess, Is.EqualTo(true)); - this.sessionStorageService.Verify(x => x.SetItemAsync("access_token", tokenBasedAuthenticationInfo.Token, default), Times.Exactly(2)); + this.sessionStorageService.Verify(x => x.SetItemAsync("access_token", tokenBasedAuthenticationInfo.Token.AccessToken, default), Times.Exactly(2)); }); } diff --git a/COMET.Web.Common/COMET.Web.Common.csproj b/COMET.Web.Common/COMET.Web.Common.csproj index 20c9c098..29426269 100644 --- a/COMET.Web.Common/COMET.Web.Common.csproj +++ b/COMET.Web.Common/COMET.Web.Common.csproj @@ -3,9 +3,9 @@ net9.0 Latest - 6.0.0 - 6.0.0 - 6.0.0 + 6.1.0 + 6.1.0 + 6.1.0 CDP4 WEB Common A Common Library for any Blazor based application related to ECSS-E-TM-10-25 Starion Group S.A. @@ -24,7 +24,7 @@ true snupkg - [Update] to CDP4-SDK 28.0.0 + [Update] to CDP4-SDK 28.2.0 [Add] Support of multiple authentication scheme @@ -32,8 +32,8 @@ - - + + diff --git a/COMET.Web.Common/Extensions/ServiceCollectionExtensions.cs b/COMET.Web.Common/Extensions/ServiceCollectionExtensions.cs index 41ba1a61..683b1fa9 100644 --- a/COMET.Web.Common/Extensions/ServiceCollectionExtensions.cs +++ b/COMET.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -31,6 +31,9 @@ namespace COMET.Web.Common.Extensions using CDP4Dal; + using CDP4ServicesDal; + using CDP4ServicesDal.ExternalAuthenticationProviderService; + using COMET.Web.Common.Model; using COMET.Web.Common.Server.Services.ConfigurationService; using COMET.Web.Common.Server.Services.StringTableService; @@ -91,6 +94,8 @@ public static void RegisterCdp4CometCommonServices(this IServiceCollection servi serviceProvider.AddScoped(); serviceProvider.AddSingleton(); serviceProvider.AddScoped(); + serviceProvider.AddScoped(); + serviceProvider.AddScoped(); serviceProvider.AddAuthorizationCore(); serviceProvider.AddDevExpressBlazor(configure => configure.SizeMode = SizeMode.Medium); serviceProvider.RegisterCommonViewModels(); diff --git a/COMET.Web.Common/Model/DTO/OpenIdAuthenticationDto.cs b/COMET.Web.Common/Model/DTO/OpenIdAuthenticationDto.cs deleted file mode 100644 index 4e42b7ea..00000000 --- a/COMET.Web.Common/Model/DTO/OpenIdAuthenticationDto.cs +++ /dev/null @@ -1,52 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2023-2025 Starion Group S.A. -// -// Authors: Sam Gerené, Alex Vorobiev, Alexander van Delft, Jaime Bernar, Théate Antoine, João Rua -// -// This file is part of COMET WEB Community Edition -// The COMET WEB Community Edition is the Starion Group Web Application implementation of ECSS-E-TM-10-25 Annex A and Annex C. -// -// The COMET WEB Community Edition is free software; you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public -// License as published by the Free Software Foundation; either -// version 3 of the License, or (at your option) any later version. -// -// The COMET WEB Community Edition is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace COMET.Web.Common.Model.DTO -{ - /// - /// The provides data model structure for response of a successfull openid authentication result - /// - public class OpenIdAuthenticationDto - { - /// - /// The generated access token - /// - public string AccessToken { get; set; } - - /// - /// The generated refresh token - /// - public string RefreshToken { get; set; } - - /// - /// The expires time of the , in seconds - /// - public int ExpiresIn { get; set; } - - /// - /// The expires time of the , in seconds - /// - public int RefreshExpiresIn { get; set; } - } -} diff --git a/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs b/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs index fe8fb9b6..679a5fe0 100644 --- a/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs +++ b/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs @@ -25,7 +25,6 @@ namespace COMET.Web.Common.Services.SessionManagement { using System.Net; - using System.Text.Json; using Blazored.SessionStorage; @@ -35,6 +34,9 @@ namespace COMET.Web.Common.Services.SessionManagement using CDP4DalCommon.Authentication; + using CDP4ServicesDal; + using CDP4ServicesDal.ExternalAuthenticationProviderService; + using CDP4Web.Extensions; using COMET.Web.Common.Model.DTO; @@ -43,8 +45,6 @@ namespace COMET.Web.Common.Services.SessionManagement using Microsoft.AspNetCore.Components.Authorization; - using JsonSerializer = System.Text.Json.JsonSerializer; - /// /// The purpose of the is to authenticate against /// a E-TM-10-25 Annex C.2 data source @@ -55,39 +55,46 @@ public class AuthenticationService : IAuthenticationService /// Gets the name of the key of the server url that is store within the session storage /// private const string ServerUrlKey = "cdp4-comet-url"; - + /// /// Gets the name of the key of the access token value that is store within the session storage /// - private const string AccessTokenKey ="access_token"; - + private const string AccessTokenKey = "access_token"; + /// /// Gets the name of the key of the refresh token value that is store within the session storage /// - private const string RefreshTokenKey ="refresh_token"; - + private const string RefreshTokenKey = "refresh_token"; + /// /// The (injected) /// private readonly AuthenticationStateProvider authStateProvider; + /// + /// Gets the injected that will provide token refresh features + /// + private readonly IAutomaticTokenRefreshService automaticTokenRefreshService; + + /// + /// Gets the injected used to communicate with the external authentication provider + /// + private readonly IOpenIdConnectService openIdConnectService; + /// /// The (injected) that provides access to the /// private readonly ISessionService sessionService; /// - /// The injected that allows interaction with the browser session storage + /// The injected that allows interaction with the browser session storage /// private readonly ISessionStorageService sessionStorageService; /// - /// Gets the + /// Stores the last retrieved to allow refresh token later /// - private static readonly JsonSerializerOptions JsonSerializerOptions = new () - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower - }; + private AuthenticationSchemeResponse lastSupportedAuthenticationSchemeResponse; /// /// Initializes a new instance of the class. @@ -98,12 +105,19 @@ public class AuthenticationService : IAuthenticationService /// /// The (injected) /// - /// The injected that allows interaction with the browser session storage - public AuthenticationService(ISessionService sessionService, AuthenticationStateProvider authenticationStateProvider, ISessionStorageService sessionStorageService) + /// The injected that allows interaction with the browser session storage + /// The injected used to communicate with the external authentication provider + /// The injected that will provide token refresh features + public AuthenticationService(ISessionService sessionService, AuthenticationStateProvider authenticationStateProvider, ISessionStorageService sessionStorageService, + IOpenIdConnectService openIdConnectService, IAutomaticTokenRefreshService automaticTokenRefreshService) { this.authStateProvider = authenticationStateProvider; this.sessionService = sessionService; this.sessionStorageService = sessionStorageService; + this.openIdConnectService = openIdConnectService; + this.automaticTokenRefreshService = automaticTokenRefreshService; + + this.automaticTokenRefreshService.TokenRefreshed += this.StoreAuthenticationTokens; } /// @@ -136,13 +150,13 @@ public async Task Login(AuthenticationDto authenticationDto) return result; } - + /// - /// Provides authentication capability against a pre-initialized and open it in case of success + /// Provides authentication capability against a pre-initialized and open it in case of success /// - /// The that has been selected - /// The that contains required information that should be used for authentication - /// An awaitable that contains the of the operation + /// The that has been selected + /// The that contains required information that should be used for authentication + /// An awaitable that contains the of the operation public async Task LoginAsync(AuthenticationSchemeKind authenticationSchemeKind, AuthenticationInformation authenticationInformation) { var authenticationResult = await this.sessionService.AuthenticateAndOpenSession(authenticationSchemeKind, authenticationInformation); @@ -153,20 +167,26 @@ public async Task LoginAsync(AuthenticationSchemeKind authenticationSche if (authenticationSchemeKind is AuthenticationSchemeKind.LocalJwtBearer or AuthenticationSchemeKind.ExternalJwtBearer) { - await this.sessionStorageService.SetItemAsync(AccessTokenKey, authenticationInformation.Token); + await this.StoreAuthenticationTokens(); + + this.automaticTokenRefreshService.Initialize(this.sessionService.Session, this.lastSupportedAuthenticationSchemeResponse); + _ = this.automaticTokenRefreshService.StartAsync().ConfigureAwait(false); } } - + return authenticationResult; } - + /// /// Request available that are supported by a server /// /// The url of the server to target /// A value indicating whether the connection shall be fully trusted or not (in case of HttpClient connections this includes trusing self signed SSL certificates) - /// An awaitable that contains the of the operation, with the returned - /// in case of success + /// + /// An awaitable that contains the of the operation, with the returned + /// + /// in case of success + /// public async Task> RequestAvailableAuthenticationSchemeAsync(string serverUrl, bool fullTrust = false) { if (string.IsNullOrEmpty(serverUrl)) @@ -182,16 +202,17 @@ public async Task> RequestAvailableAuthenti if (result.IsSuccess && result.Value.Schemes.Intersect([AuthenticationSchemeKind.ExternalJwtBearer, AuthenticationSchemeKind.LocalJwtBearer]).Any()) { // Required to be able to restore a session + this.lastSupportedAuthenticationSchemeResponse = result.Value; await this.sessionStorageService.SetItemAsync(ServerUrlKey, serverUrl); } - + return result; } /// /// Retrieves the last used server url /// - /// An awaitable with the retrieved server url + /// An awaitable with the retrieved server url public async Task RetrieveLastUsedServerUrlAsync() { return await this.sessionStorageService.GetItemAsync(ServerUrlKey); @@ -208,16 +229,18 @@ public async Task TryRestoreLastSessionAsync() { return; } - + var authenticationSchemeResponse = await this.RequestAvailableAuthenticationSchemeAsync(serverUrl); - if (authenticationSchemeResponse.IsFailed + if (authenticationSchemeResponse.IsFailed || !authenticationSchemeResponse.Value.Schemes.Intersect([AuthenticationSchemeKind.ExternalJwtBearer, AuthenticationSchemeKind.LocalJwtBearer]).Any()) { await this.CleanupStorageAsync(); return; } - + + this.lastSupportedAuthenticationSchemeResponse = authenticationSchemeResponse.Value; + var previousToken = await this.sessionStorageService.GetItemAsync(AccessTokenKey); if (string.IsNullOrEmpty(previousToken)) @@ -225,12 +248,28 @@ public async Task TryRestoreLastSessionAsync() await this.CleanupStorageAsync(); return; } - - var authenticationSchemeToBeUsed = authenticationSchemeResponse.Value.Schemes.Contains(AuthenticationSchemeKind.ExternalJwtBearer) - ? AuthenticationSchemeKind.ExternalJwtBearer + + var refreshToken = await this.sessionStorageService.GetItemAsync(RefreshTokenKey); + var authenticationToken = new AuthenticationTokens(previousToken, refreshToken); + + var authenticationSchemeToBeUsed = authenticationSchemeResponse.Value.Schemes.Contains(AuthenticationSchemeKind.ExternalJwtBearer) + ? AuthenticationSchemeKind.ExternalJwtBearer : AuthenticationSchemeKind.LocalJwtBearer; - - var result = await this.LoginAsync(authenticationSchemeToBeUsed, new AuthenticationInformation(previousToken)); + + this.sessionService.Session.Credentials.ProvideUserToken(authenticationToken, authenticationSchemeToBeUsed); + + try + { + this.automaticTokenRefreshService.Initialize(this.sessionService.Session, this.lastSupportedAuthenticationSchemeResponse); + await this.automaticTokenRefreshService.RefreshAuthenticationTokenAsync(); + } + catch + { + await this.CleanupStorageAsync(); + return; + } + + var result = await this.LoginAsync(authenticationSchemeToBeUsed, new AuthenticationInformation(this.sessionService.Session.Credentials.Tokens)); if (result.IsFailed) { @@ -242,50 +281,28 @@ public async Task TryRestoreLastSessionAsync() /// Exchange an OpenId connect to retrieve the generated JWT token /// /// The code provided by the issuer - /// The retrieved from the CDP4-COMET server + /// The retrieved from the CDP4-COMET server /// The redirect url /// An optional client secret - /// An awaitable + /// An awaitable public async Task ExchangeOpenIdConnectCodeAsync(string code, AuthenticationSchemeResponse authenticationSchemeResponse, string redirectUrl, string clientSecret = null) { Guard.ThrowIfNull(authenticationSchemeResponse, nameof(authenticationSchemeResponse)); Guard.ThrowIfNullOrEmpty(redirectUrl, nameof(redirectUrl)); - + if (!authenticationSchemeResponse.Schemes.Contains(AuthenticationSchemeKind.ExternalJwtBearer)) { throw new InvalidOperationException("Supported scheme should at least contains ExternalJwtBearer"); } - using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri(authenticationSchemeResponse.Authority); - - var parameters = new List> - { - new ("code", code), - new ("client_id", authenticationSchemeResponse.ClientId), - new ("redirect_uri", redirectUrl.TrimEnd('/')), - new ("grant_type", "authorization_code"), - }; - - if (!string.IsNullOrEmpty(clientSecret)) + try { - parameters.Add(new KeyValuePair("client_secret", clientSecret)); + var authenticationTokens = await this.openIdConnectService.RequestAuthenticationToken(code, authenticationSchemeResponse, redirectUrl, clientSecret); + await this.LoginAsync(AuthenticationSchemeKind.ExternalJwtBearer, new AuthenticationInformation(authenticationTokens)); } - - var httpMessage = new HttpRequestMessage(HttpMethod.Post, new Uri($"{authenticationSchemeResponse.Authority.TrimEnd('/')}/protocol/openid-connect/token")); - httpMessage.Content = new FormUrlEncodedContent(parameters); - using var httpResponse = await httpClient.SendAsync(httpMessage); - - if (httpResponse.StatusCode.IsSuccess()) + catch { - var content = await httpResponse.Content.ReadAsStringAsync(); - var openIdAuthentication = JsonSerializer.Deserialize(content, JsonSerializerOptions); - var result = await this.LoginAsync(AuthenticationSchemeKind.ExternalJwtBearer, new AuthenticationInformation(openIdAuthentication.AccessToken)); - - if (result.IsSuccess) - { - await this.sessionStorageService.SetItemAsync(RefreshTokenKey, openIdAuthentication.RefreshToken); - } + await this.CleanupStorageAsync(); } } @@ -303,10 +320,29 @@ public async Task Logout() } ((CometWebAuthStateProvider)this.authStateProvider).NotifyAuthenticationStateChanged(); - + this.automaticTokenRefreshService.Dispose(); await this.CleanupStorageAsync(); } + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + this.automaticTokenRefreshService.TokenRefreshed -= this.StoreAuthenticationTokens; + this.automaticTokenRefreshService.Dispose(); + } + + /// + /// Stores information into the session storage + /// + /// An awaitable + private async Task StoreAuthenticationTokens() + { + await this.sessionStorageService.SetItemAsync(AccessTokenKey, this.sessionService.Session.Credentials.Tokens.AccessToken); + await this.sessionStorageService.SetItemAsync(RefreshTokenKey, this.sessionService.Session.Credentials.Tokens.RefreshToken); + } + /// /// Cleans all values that could be present inside the Session Storage /// diff --git a/COMET.Web.Common/Services/SessionManagement/IAuthenticationService.cs b/COMET.Web.Common/Services/SessionManagement/IAuthenticationService.cs index bfd1abba..b26692ab 100644 --- a/COMET.Web.Common/Services/SessionManagement/IAuthenticationService.cs +++ b/COMET.Web.Common/Services/SessionManagement/IAuthenticationService.cs @@ -38,7 +38,7 @@ namespace COMET.Web.Common.Services.SessionManagement /// The purpose of the is to authenticate against /// a E-TM-10-25 Annex C.2 data source /// - public interface IAuthenticationService + public interface IAuthenticationService: IDisposable { /// /// Login (authenticate) with authentication information to a data source diff --git a/COMETwebapp/COMETwebapp.csproj b/COMETwebapp/COMETwebapp.csproj index 63e60efc..12fac4c2 100644 --- a/COMETwebapp/COMETwebapp.csproj +++ b/COMETwebapp/COMETwebapp.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.0 + 6.1.0 CDP4-COMET WEB A web application that implements ECSS-E-TM-10-25 Starion Group S.A. From 6259bb51145953a7cba6caa12266121057ae51ab Mon Sep 17 00:00:00 2001 From: antoineatstariongroup Date: Wed, 5 Mar 2025 15:02:12 +0100 Subject: [PATCH 2/4] code coverage --- .../AuthenticationServiceTestFixture.cs | 43 +++++++++++++++++++ .../AuthenticationService.cs | 9 ++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs b/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs index c6f996bb..83c67641 100644 --- a/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs +++ b/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs @@ -85,6 +85,12 @@ public void SetUp() }; } + [TearDown] + public void Teardown() + { + this.authenticationService.Dispose(); + } + [Test] public async Task VerifyLogout() { @@ -214,5 +220,42 @@ public async Task VerifyTryRestoreLastSession() await this.authenticationService.TryRestoreLastSessionAsync(); this.sessionService.Verify(x => x.AuthenticateAndOpenSession(AuthenticationSchemeKind.LocalJwtBearer, It.IsAny()), Times.Once); } + + [Test] + public async Task VerifyExchangeOpenIdConnectCodeAsync() + { + const string code = "aRandomCode"; + const string redirect = "http://localhost/callback"; + + var authenticationSchemeResponse = new AuthenticationSchemeResponse() + { + Schemes = [AuthenticationSchemeKind.Basic] + }; + + await Assert.MultipleAsync(async () => + { + await Assert.ThatAsync(() => this.authenticationService.ExchangeOpenIdConnectCodeAsync(null, authenticationSchemeResponse, redirect), Throws.Exception); + await Assert.ThatAsync(() => this.authenticationService.ExchangeOpenIdConnectCodeAsync(code, null, redirect), Throws.Exception); + await Assert.ThatAsync(() => this.authenticationService.ExchangeOpenIdConnectCodeAsync(code, authenticationSchemeResponse, null), Throws.Exception); + await Assert.ThatAsync(() => this.authenticationService.ExchangeOpenIdConnectCodeAsync(code, authenticationSchemeResponse, redirect), Throws.Exception); + }); + + authenticationSchemeResponse.Schemes = [AuthenticationSchemeKind.ExternalJwtBearer]; + var openIdDto = new OpenIdAuthenticationDto("access", "refresh", 1500, 15000); + + this.openIdConnectService.Setup(x => x.RequestAuthenticationToken(code, authenticationSchemeResponse, redirect, null)).ReturnsAsync(openIdDto); + + this.sessionService.Setup(x => x.AuthenticateAndOpenSession(AuthenticationSchemeKind.ExternalJwtBearer, It.IsAny())) + .ReturnsAsync(Result.Ok()) + .Callback(()=> this.credentials.ProvideUserToken(openIdDto, AuthenticationSchemeKind.ExternalJwtBearer)); + + await this.authenticationService.ExchangeOpenIdConnectCodeAsync(code, authenticationSchemeResponse, redirect); + this.sessionService.Verify(x => x.AuthenticateAndOpenSession(AuthenticationSchemeKind.ExternalJwtBearer, It.IsAny()), Times.Once); + + this.openIdConnectService.Setup(x => x.RequestAuthenticationToken(code, authenticationSchemeResponse, redirect, null)).ThrowsAsync(new InvalidOperationException()); + await this.authenticationService.ExchangeOpenIdConnectCodeAsync(code, authenticationSchemeResponse, redirect); + + this.sessionStorageService.Verify(x => x.SetItemAsync(It.IsAny(), string.Empty, default), Times.Exactly(3)); + } } } diff --git a/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs b/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs index 679a5fe0..111b59b1 100644 --- a/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs +++ b/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs @@ -117,7 +117,7 @@ public AuthenticationService(ISessionService sessionService, AuthenticationState this.openIdConnectService = openIdConnectService; this.automaticTokenRefreshService = automaticTokenRefreshService; - this.automaticTokenRefreshService.TokenRefreshed += this.StoreAuthenticationTokens; + this.automaticTokenRefreshService.TokenRefreshed += this.StoreAuthenticationTokensAsync; } /// @@ -167,7 +167,7 @@ public async Task LoginAsync(AuthenticationSchemeKind authenticationSche if (authenticationSchemeKind is AuthenticationSchemeKind.LocalJwtBearer or AuthenticationSchemeKind.ExternalJwtBearer) { - await this.StoreAuthenticationTokens(); + await this.StoreAuthenticationTokensAsync(); this.automaticTokenRefreshService.Initialize(this.sessionService.Session, this.lastSupportedAuthenticationSchemeResponse); _ = this.automaticTokenRefreshService.StartAsync().ConfigureAwait(false); @@ -289,6 +289,7 @@ public async Task ExchangeOpenIdConnectCodeAsync(string code, AuthenticationSche { Guard.ThrowIfNull(authenticationSchemeResponse, nameof(authenticationSchemeResponse)); Guard.ThrowIfNullOrEmpty(redirectUrl, nameof(redirectUrl)); + Guard.ThrowIfNullOrEmpty(code, nameof(code)); if (!authenticationSchemeResponse.Schemes.Contains(AuthenticationSchemeKind.ExternalJwtBearer)) { @@ -329,7 +330,7 @@ public async Task Logout() /// public void Dispose() { - this.automaticTokenRefreshService.TokenRefreshed -= this.StoreAuthenticationTokens; + this.automaticTokenRefreshService.TokenRefreshed -= this.StoreAuthenticationTokensAsync; this.automaticTokenRefreshService.Dispose(); } @@ -337,7 +338,7 @@ public void Dispose() /// Stores information into the session storage /// /// An awaitable - private async Task StoreAuthenticationTokens() + private async Task StoreAuthenticationTokensAsync() { await this.sessionStorageService.SetItemAsync(AccessTokenKey, this.sessionService.Session.Credentials.Tokens.AccessToken); await this.sessionStorageService.SetItemAsync(RefreshTokenKey, this.sessionService.Session.Credentials.Tokens.RefreshToken); From 18cc085c81b4f644573e76094164935e2e9249f5 Mon Sep 17 00:00:00 2001 From: antoineatstariongroup Date: Tue, 11 Mar 2025 08:55:53 +0100 Subject: [PATCH 3/4] Bump to latest SDK --- .../AuthenticationServiceTestFixture.cs | 15 ++++----- COMET.Web.Common/COMET.Web.Common.csproj | 7 ++-- .../Extensions/ServiceCollectionExtensions.cs | 4 +-- .../AuthenticationService.cs | 33 +++++++++---------- 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs b/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs index 83c67641..eb2a05a6 100644 --- a/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs +++ b/COMET.Web.Common.Tests/Services/SessionManagement/AuthenticationServiceTestFixture.cs @@ -32,9 +32,6 @@ namespace COMET.Web.Common.Tests.Services.SessionManagement using CDP4DalCommon.Authentication; - using CDP4ServicesDal; - using CDP4ServicesDal.ExternalAuthenticationProviderService; - using COMET.Web.Common.Model.DTO; using COMET.Web.Common.Services.SessionManagement; @@ -53,8 +50,8 @@ public class AuthenticationServiceTestFixture private AuthenticationService authenticationService; private AuthenticationDto authenticationDto; private Mock sessionStorageService; - private Mock openIdConnectService; - private Mock automaticTokenRefreshService; + private Mock openIdConnectService; + private Mock automaticTokenRefreshService; private Credentials credentials; [SetUp] @@ -63,8 +60,8 @@ public void SetUp() this.session = new Mock(); this.sessionService = new Mock(); this.sessionStorageService = new Mock(); - this.openIdConnectService = new Mock(); - this.automaticTokenRefreshService = new Mock(); + this.openIdConnectService = new Mock(); + this.automaticTokenRefreshService = new Mock(); this.sessionService.Setup(x => x.Session).Returns(this.session.Object); this.sessionService.Setup(x => x.IsSessionOpen).Returns(false); @@ -139,7 +136,7 @@ public async Task VerifyLoginWithDefinedScheme() this.sessionStorageService.Verify(x => x.SetItemAsync("access_token", It.IsAny(), default), Times.Never); }); - var tokenBasedAuthenticationInfo = new AuthenticationInformation(new AuthenticationTokens("token", "refresh")); + var tokenBasedAuthenticationInfo = new AuthenticationInformation(new AuthenticationToken("token", "refresh")); this.sessionService.Setup(x => x.AuthenticateAndOpenSession(AuthenticationSchemeKind.ExternalJwtBearer, tokenBasedAuthenticationInfo)).ReturnsAsync(Result.Ok()) .Callback(() => this.credentials.ProvideUserToken(tokenBasedAuthenticationInfo.Token, AuthenticationSchemeKind.ExternalJwtBearer)); @@ -241,7 +238,7 @@ await Assert.MultipleAsync(async () => }); authenticationSchemeResponse.Schemes = [AuthenticationSchemeKind.ExternalJwtBearer]; - var openIdDto = new OpenIdAuthenticationDto("access", "refresh", 1500, 15000); + var openIdDto = new AuthenticationToken("access", "refresh"); this.openIdConnectService.Setup(x => x.RequestAuthenticationToken(code, authenticationSchemeResponse, redirect, null)).ReturnsAsync(openIdDto); diff --git a/COMET.Web.Common/COMET.Web.Common.csproj b/COMET.Web.Common/COMET.Web.Common.csproj index 29426269..a6a52947 100644 --- a/COMET.Web.Common/COMET.Web.Common.csproj +++ b/COMET.Web.Common/COMET.Web.Common.csproj @@ -24,16 +24,15 @@ true snupkg - [Update] to CDP4-SDK 28.2.0 - [Add] Support of multiple authentication scheme + [Update] to CDP4-SDK 28.3.0 - - + + diff --git a/COMET.Web.Common/Extensions/ServiceCollectionExtensions.cs b/COMET.Web.Common/Extensions/ServiceCollectionExtensions.cs index 683b1fa9..4d7b45b6 100644 --- a/COMET.Web.Common/Extensions/ServiceCollectionExtensions.cs +++ b/COMET.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -94,8 +94,8 @@ public static void RegisterCdp4CometCommonServices(this IServiceCollection servi serviceProvider.AddScoped(); serviceProvider.AddSingleton(); serviceProvider.AddScoped(); - serviceProvider.AddScoped(); - serviceProvider.AddScoped(); + serviceProvider.AddScoped(); + serviceProvider.AddScoped(); serviceProvider.AddAuthorizationCore(); serviceProvider.AddDevExpressBlazor(configure => configure.SizeMode = SizeMode.Medium); serviceProvider.RegisterCommonViewModels(); diff --git a/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs b/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs index 111b59b1..c8ea5f5b 100644 --- a/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs +++ b/COMET.Web.Common/Services/SessionManagement/AuthenticationService.cs @@ -34,9 +34,6 @@ namespace COMET.Web.Common.Services.SessionManagement using CDP4DalCommon.Authentication; - using CDP4ServicesDal; - using CDP4ServicesDal.ExternalAuthenticationProviderService; - using CDP4Web.Extensions; using COMET.Web.Common.Model.DTO; @@ -72,14 +69,14 @@ public class AuthenticationService : IAuthenticationService private readonly AuthenticationStateProvider authStateProvider; /// - /// Gets the injected that will provide token refresh features + /// Gets the injected that will provide token refresh features /// - private readonly IAutomaticTokenRefreshService automaticTokenRefreshService; + private readonly IAuthenticationRefreshService automaticTokenRefreshService; /// - /// Gets the injected used to communicate with the external authentication provider + /// Gets the injected used to communicate with the external authentication provider /// - private readonly IOpenIdConnectService openIdConnectService; + private readonly IProvideExternalAuthenticationService openIdConnectService; /// /// The (injected) that provides access to the @@ -106,10 +103,10 @@ public class AuthenticationService : IAuthenticationService /// The (injected) /// /// The injected that allows interaction with the browser session storage - /// The injected used to communicate with the external authentication provider - /// The injected that will provide token refresh features + /// The injected used to communicate with the external authentication provider + /// The injected that will provide token refresh features public AuthenticationService(ISessionService sessionService, AuthenticationStateProvider authenticationStateProvider, ISessionStorageService sessionStorageService, - IOpenIdConnectService openIdConnectService, IAutomaticTokenRefreshService automaticTokenRefreshService) + IProvideExternalAuthenticationService openIdConnectService, IAuthenticationRefreshService automaticTokenRefreshService) { this.authStateProvider = authenticationStateProvider; this.sessionService = sessionService; @@ -117,7 +114,7 @@ public AuthenticationService(ISessionService sessionService, AuthenticationState this.openIdConnectService = openIdConnectService; this.automaticTokenRefreshService = automaticTokenRefreshService; - this.automaticTokenRefreshService.TokenRefreshed += this.StoreAuthenticationTokensAsync; + this.automaticTokenRefreshService.AuthenticationRefreshed += this.StoreAuthenticationTokensAsync; } /// @@ -250,7 +247,7 @@ public async Task TryRestoreLastSessionAsync() } var refreshToken = await this.sessionStorageService.GetItemAsync(RefreshTokenKey); - var authenticationToken = new AuthenticationTokens(previousToken, refreshToken); + var authenticationToken = new AuthenticationToken(previousToken, refreshToken); var authenticationSchemeToBeUsed = authenticationSchemeResponse.Value.Schemes.Contains(AuthenticationSchemeKind.ExternalJwtBearer) ? AuthenticationSchemeKind.ExternalJwtBearer @@ -261,7 +258,7 @@ public async Task TryRestoreLastSessionAsync() try { this.automaticTokenRefreshService.Initialize(this.sessionService.Session, this.lastSupportedAuthenticationSchemeResponse); - await this.automaticTokenRefreshService.RefreshAuthenticationTokenAsync(); + await this.automaticTokenRefreshService.RefreshAuthenticationInformationAsync(); } catch { @@ -269,7 +266,7 @@ public async Task TryRestoreLastSessionAsync() return; } - var result = await this.LoginAsync(authenticationSchemeToBeUsed, new AuthenticationInformation(this.sessionService.Session.Credentials.Tokens)); + var result = await this.LoginAsync(authenticationSchemeToBeUsed, new AuthenticationInformation(this.sessionService.Session.Credentials.Token)); if (result.IsFailed) { @@ -330,18 +327,18 @@ public async Task Logout() /// public void Dispose() { - this.automaticTokenRefreshService.TokenRefreshed -= this.StoreAuthenticationTokensAsync; + this.automaticTokenRefreshService.AuthenticationRefreshed -= this.StoreAuthenticationTokensAsync; this.automaticTokenRefreshService.Dispose(); } /// - /// Stores information into the session storage + /// Stores information into the session storage /// /// An awaitable private async Task StoreAuthenticationTokensAsync() { - await this.sessionStorageService.SetItemAsync(AccessTokenKey, this.sessionService.Session.Credentials.Tokens.AccessToken); - await this.sessionStorageService.SetItemAsync(RefreshTokenKey, this.sessionService.Session.Credentials.Tokens.RefreshToken); + await this.sessionStorageService.SetItemAsync(AccessTokenKey, this.sessionService.Session.Credentials.Token.AccessToken); + await this.sessionStorageService.SetItemAsync(RefreshTokenKey, this.sessionService.Session.Credentials.Token.RefreshToken); } /// From 8fe9e04366516c20165c48f6506d737a0e8ea775 Mon Sep 17 00:00:00 2001 From: antoineatstariongroup Date: Tue, 11 Mar 2025 09:03:47 +0100 Subject: [PATCH 4/4] Bump 6.2.0 --- COMET.Web.Common/COMET.Web.Common.csproj | 6 +++--- COMETwebapp/COMETwebapp.csproj | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/COMET.Web.Common/COMET.Web.Common.csproj b/COMET.Web.Common/COMET.Web.Common.csproj index a6a52947..c1e0b7ca 100644 --- a/COMET.Web.Common/COMET.Web.Common.csproj +++ b/COMET.Web.Common/COMET.Web.Common.csproj @@ -3,9 +3,9 @@ net9.0 Latest - 6.1.0 - 6.1.0 - 6.1.0 + 6.2.0 + 6.2.0 + 6.2.0 CDP4 WEB Common A Common Library for any Blazor based application related to ECSS-E-TM-10-25 Starion Group S.A. diff --git a/COMETwebapp/COMETwebapp.csproj b/COMETwebapp/COMETwebapp.csproj index 12fac4c2..2af80b85 100644 --- a/COMETwebapp/COMETwebapp.csproj +++ b/COMETwebapp/COMETwebapp.csproj @@ -2,7 +2,7 @@ net9.0 - 6.1.0 + 6.2.0 CDP4-COMET WEB A web application that implements ECSS-E-TM-10-25 Starion Group S.A.