diff --git a/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs index ac441f7701..c7e91737ff 100644 --- a/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs @@ -36,7 +36,6 @@ using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Entities; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; -using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Identities; using Org.Eclipse.TractusX.Portal.Backend.Processes.ApplicationChecklist.Library; using Org.Eclipse.TractusX.Portal.Backend.Processes.Library; using Org.Eclipse.TractusX.Portal.Backend.Processes.Mailing.Library; diff --git a/src/keycloak/Keycloak.Library/Authentication/KeycloakAccessToken.cs b/src/keycloak/Keycloak.Library/Authentication/KeycloakAccessToken.cs new file mode 100644 index 0000000000..2a0bd02e84 --- /dev/null +++ b/src/keycloak/Keycloak.Library/Authentication/KeycloakAccessToken.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Authentication; + +public record KeycloakAccessToken( + string AccessToken, + DateTimeOffset ExpiryTime, + string RefreshToken, + DateTimeOffset RefreshExpiryTime +); diff --git a/src/keycloak/Keycloak.Library/Authentication/KeycloakAccessTokenExtensions.cs b/src/keycloak/Keycloak.Library/Authentication/KeycloakAccessTokenExtensions.cs new file mode 100644 index 0000000000..9342f8bc03 --- /dev/null +++ b/src/keycloak/Keycloak.Library/Authentication/KeycloakAccessTokenExtensions.cs @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Flurl; +using Flurl.Http; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Authentication; + +internal static class KeycloakAccessTokenExtensions +{ + public static async Task GetAccessToken(this KeycloakAccessToken? token, Url url, string realm, string? userName, string? password, string? clientSecret, string clientId, CancellationToken cancellationToken) + { + var now = DateTimeOffset.UtcNow; + + if (token != null && token.ExpiryTime > now) + { + return token; + } + + var accessTokenResponse = await (token is null + ? GetToken() + : RefreshToken()).ConfigureAwait(ConfigureAwaitOptions.None) ?? throw new ConflictException("accessTokenResponse should never be null"); + + return new KeycloakAccessToken(accessTokenResponse.AccessToken, now.AddSeconds(accessTokenResponse.ExpiresIn), accessTokenResponse.RefreshToken, now.AddSeconds(accessTokenResponse.RefreshExpiresIn)); + + Task GetToken() + { + if (clientSecret != null) + { + return RetrieveToken([ + new("grant_type", "client_credentials"), + new("client_secret", clientSecret), + new("client_id", clientId) + ]); + } + + if (userName != null) + { + return RetrieveToken([ + new("grant_type", "password"), + new("username", userName), + new("password", password ?? ""), + new("client_id", "admin-cli") + ]); + } + + throw new ArgumentException($"{nameof(userName)} and {nameof(clientSecret)} must not all be null"); + } + + Task RefreshToken() => + token.RefreshExpiryTime > now + ? RetrieveToken([ + new("grant_type", "refresh_token"), + new("refresh_token", token.RefreshToken), + new("client_id", clientId) + ]) + : GetToken(); + + Task RetrieveToken(IEnumerable> keyValues) => + url + .AppendPathSegments("realms", Url.Encode(realm), "protocol/openid-connect/token") + .WithHeader("Content-Type", "application/x-www-form-urlencoded") + .PostUrlEncodedAsync(keyValues, cancellationToken: cancellationToken) + .ReceiveJson(); + } + + private sealed record AccessTokenResponse( + [property: JsonPropertyName("access_token")] string AccessToken, + [property: JsonPropertyName("expires_in")] int ExpiresIn, + [property: JsonPropertyName("refresh_token")] string RefreshToken, + [property: JsonPropertyName("refresh_expires_in")] int RefreshExpiresIn + ); +} diff --git a/src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs index 63f8ec3a5a..701f74f75a 100644 --- a/src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs @@ -24,7 +24,6 @@ ********************************************************************************/ using Flurl.Http; -using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Common.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Clients; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.ClientScopes; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Common; diff --git a/src/keycloak/Keycloak.Library/Common/Extensions/FlurlRequestExtensions.cs b/src/keycloak/Keycloak.Library/Common/Extensions/FlurlRequestExtensions.cs index de67e3c0dd..30511adf55 100644 --- a/src/keycloak/Keycloak.Library/Common/Extensions/FlurlRequestExtensions.cs +++ b/src/keycloak/Keycloak.Library/Common/Extensions/FlurlRequestExtensions.cs @@ -23,74 +23,12 @@ * SOFTWARE. ********************************************************************************/ -using Flurl; using Flurl.Http; -using System.Text.Json.Serialization; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Common.Extensions; public static class FlurlRequestExtensions { - private static async Task GetAccessTokenAsync(string url, string realm, string userName, string password, bool useAuthTrail, CancellationToken cancellationToken) - { - var authTrail = useAuthTrail ? "/auth" : string.Empty; - var result = await url - .AppendPathSegment($"{authTrail}/realms/{realm}/protocol/openid-connect/token") - .WithHeader("Accept", "application/json") - .PostUrlEncodedAsync(new List> - { - new KeyValuePair("grant_type", "password"), - new KeyValuePair("username", userName), - new KeyValuePair("password", password), - new KeyValuePair("client_id", "admin-cli") - }, - cancellationToken: cancellationToken) - .ReceiveJson().ConfigureAwait(ConfigureAwaitOptions.None); - - return result.AccessToken; - } - - private static async Task GetAccessTokenWithClientIdAsync(string url, string realm, string clientSecret, string? clientId, bool useAuthTrail, CancellationToken cancellationToken) - { - var authTrail = useAuthTrail ? "/auth" : string.Empty; - var result = await url - .AppendPathSegment($"{authTrail}/realms/{realm}/protocol/openid-connect/token") - .WithHeader("Content-Type", "application/x-www-form-urlencoded") - .PostUrlEncodedAsync(new List> - { - new("grant_type", "client_credentials"), - new("client_secret", clientSecret), - new("client_id", clientId ?? "admin-cli") - }, - cancellationToken: cancellationToken) - .ReceiveJson().ConfigureAwait(ConfigureAwaitOptions.None); - - return result.AccessToken; - } - - public static async Task WithAuthenticationAsync(this IFlurlRequest request, Func>? getTokenAsync, string url, string realm, string? userName, string? password, string? clientSecret, string? clientId, bool useAuthTrail, CancellationToken cancellationToken) - { - string? token; - if (getTokenAsync != null) - { - token = await getTokenAsync(cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - } - else if (clientSecret != null) - { - token = await GetAccessTokenWithClientIdAsync(url, realm, clientSecret, clientId, useAuthTrail, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - } - else if (userName != null) - { - token = await GetAccessTokenAsync(url, realm, userName, password ?? "", useAuthTrail, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); - } - else - { - throw new ArgumentException($"{nameof(getTokenAsync)}, {nameof(userName)} and {nameof(clientSecret)} must not all be null"); - } - - return request.WithOAuthBearerToken(token); - } - public static IFlurlRequest WithForwardedHttpHeaders(this IFlurlRequest request, ForwardedHttpHeaders forwardedHeaders) { if (!string.IsNullOrEmpty(forwardedHeaders?.forwardedFor)) @@ -110,8 +48,4 @@ public static IFlurlRequest WithForwardedHttpHeaders(this IFlurlRequest request, return request; } - - public record AccessTokenResponse( - [property: JsonPropertyName("access_token")] string AccessToken - ); } diff --git a/src/keycloak/Keycloak.Library/Groups/KeycloakClient.cs b/src/keycloak/Keycloak.Library/Groups/KeycloakClient.cs index 666e90cf62..e9aa7e646f 100644 --- a/src/keycloak/Keycloak.Library/Groups/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/Groups/KeycloakClient.cs @@ -24,7 +24,6 @@ ********************************************************************************/ using Flurl.Http; -using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Common.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Common; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Groups; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Users; diff --git a/src/keycloak/Keycloak.Library/KeycloakClient.cs b/src/keycloak/Keycloak.Library/KeycloakClient.cs index cbdb5fadc5..1f3af925b9 100644 --- a/src/keycloak/Keycloak.Library/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/KeycloakClient.cs @@ -26,7 +26,7 @@ using Flurl; using Flurl.Http; using Flurl.Http.Configuration; -using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Common.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Authentication; using System.Text.Json; using System.Text.Json.Serialization; @@ -44,10 +44,10 @@ public partial class KeycloakClient private readonly string? _userName; private readonly string? _password; private readonly string? _clientSecret; - private readonly Func>? _getTokenAsync; private readonly string? _authRealm; private readonly string? _clientId; private readonly bool _useAuthTrail; + private KeycloakAccessToken? _token; private KeycloakClient(string url) { @@ -74,26 +74,12 @@ private KeycloakClient(string url, string? userName, string? password, string? a _useAuthTrail = useAuthTrail; } - public KeycloakClient(string url, Func getToken, string? authRealm = null) - : this(url) - { - _getTokenAsync = _ => Task.FromResult(getToken()); - _authRealm = authRealm; - } - - public KeycloakClient(string url, Func> getTokenAsync, string? authRealm = null) - : this(url) - { - _getTokenAsync = getTokenAsync; - _authRealm = authRealm; - } - public static KeycloakClient CreateWithClientId(string url, string? clientId, string? clientSecret, bool useAuthTrail, string? authRealm = null) { return new KeycloakClient(url, userName: null, password: null, authRealm, clientId, clientSecret, useAuthTrail); } - private Task GetBaseUrlAsync(string targetRealm, CancellationToken cancellationToken = default) + private async Task GetBaseUrlAsync(string targetRealm, CancellationToken cancellationToken = default) { var url = new Url(_url); if (_useAuthTrail) @@ -102,8 +88,11 @@ private Task GetBaseUrlAsync(string targetRealm, CancellationToke .AppendPathSegment("/auth"); } + _token = await _token + .GetAccessToken(url.Clone(), _authRealm ?? targetRealm, _userName, _password, _clientSecret, _clientId ?? "admin-cli", cancellationToken).ConfigureAwait(ConfigureAwaitOptions.None); + return url .WithSettings(s => s.JsonSerializer = new DefaultJsonSerializer(_jsonOptions)) - .WithAuthenticationAsync(_getTokenAsync, _url, _authRealm ?? targetRealm, _userName, _password, _clientSecret, _clientId, _useAuthTrail, cancellationToken); + .WithOAuthBearerToken(_token.AccessToken); } } diff --git a/src/keycloak/Keycloak.Library/RolesById/KeycloakClient.cs b/src/keycloak/Keycloak.Library/RolesById/KeycloakClient.cs index 101948b5d3..ebef9d2973 100755 --- a/src/keycloak/Keycloak.Library/RolesById/KeycloakClient.cs +++ b/src/keycloak/Keycloak.Library/RolesById/KeycloakClient.cs @@ -24,10 +24,8 @@ ********************************************************************************/ using Flurl.Http; -using Flurl.Http.Content; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Common; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Roles; -using System.Text.Json; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library; diff --git a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs index cd3ad2983e..64d16e5fea 100644 --- a/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs +++ b/src/keycloak/Keycloak.Seeding/BusinessLogic/KeecloakSeeder.cs @@ -18,7 +18,6 @@ ********************************************************************************/ using Microsoft.Extensions.Options; -using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Models; namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.BusinessLogic; diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/ITechnicalUserProfileRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/ITechnicalUserProfileRepository.cs index 4e2876d4eb..eac4b650d4 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/ITechnicalUserProfileRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/ITechnicalUserProfileRepository.cs @@ -18,7 +18,6 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Entities; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; diff --git a/tests/administration/Administration.Service.Tests/BusinessLogic/RegistrationBusinessLogicTest.cs b/tests/administration/Administration.Service.Tests/BusinessLogic/RegistrationBusinessLogicTest.cs index bcdd7ae152..b25a26f31f 100644 --- a/tests/administration/Administration.Service.Tests/BusinessLogic/RegistrationBusinessLogicTest.cs +++ b/tests/administration/Administration.Service.Tests/BusinessLogic/RegistrationBusinessLogicTest.cs @@ -34,7 +34,6 @@ using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Entities; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; -using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Identities; using Org.Eclipse.TractusX.Portal.Backend.Processes.ApplicationChecklist.Library; using Org.Eclipse.TractusX.Portal.Backend.Processes.Mailing.Library; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library; diff --git a/tests/keycloak/Keycloak.Seeding.Tests/Extensions/KeycloakRealmSettingsTests.cs b/tests/keycloak/Keycloak.Seeding.Tests/Extensions/KeycloakRealmSettingsTests.cs index 7152596a6e..6fb9afcc24 100644 --- a/tests/keycloak/Keycloak.Seeding.Tests/Extensions/KeycloakRealmSettingsTests.cs +++ b/tests/keycloak/Keycloak.Seeding.Tests/Extensions/KeycloakRealmSettingsTests.cs @@ -22,7 +22,6 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Tests.Extensions; -using System.Collections.Generic; using Xunit; public class KeycloakRealmSettingsTests diff --git a/tests/portalbackend/PortalBackend.DBAccess.Tests/UserRolesRepositoryTests.cs b/tests/portalbackend/PortalBackend.DBAccess.Tests/UserRolesRepositoryTests.cs index e0836f3e2b..e2487b394b 100644 --- a/tests/portalbackend/PortalBackend.DBAccess.Tests/UserRolesRepositoryTests.cs +++ b/tests/portalbackend/PortalBackend.DBAccess.Tests/UserRolesRepositoryTests.cs @@ -19,7 +19,6 @@ using Org.Eclipse.TractusX.Portal.Backend.Framework.Models; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; -using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Tests.Setup; using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; diff --git a/tests/shared/Tests.Shared/FlurlSetup/FlurlSetupExtensions.cs b/tests/shared/Tests.Shared/FlurlSetup/FlurlSetupExtensions.cs index 1e9996cf5f..75f1607706 100644 --- a/tests/shared/Tests.Shared/FlurlSetup/FlurlSetupExtensions.cs +++ b/tests/shared/Tests.Shared/FlurlSetup/FlurlSetupExtensions.cs @@ -25,7 +25,6 @@ using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.RealmsAdmin; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Roles; using Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.Users; -using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Models; using System.Net; using IdentityProvider = Org.Eclipse.TractusX.Portal.Backend.Keycloak.Library.Models.RealmsAdmin.IdentityProvider;