Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(keycloak): adjust token handling #1209

Merged
merged 3 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
);
Original file line number Diff line number Diff line change
@@ -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<KeycloakAccessToken> 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<AccessTokenResponse> 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<AccessTokenResponse> RefreshToken() =>
token.RefreshExpiryTime > now
? RetrieveToken([
new("grant_type", "refresh_token"),
new("refresh_token", token.RefreshToken),
new("client_id", clientId)
])
: GetToken();

Task<AccessTokenResponse> RetrieveToken(IEnumerable<KeyValuePair<string, string>> keyValues) =>
url
.AppendPathSegments("realms", Url.Encode(realm), "protocol/openid-connect/token")
.WithHeader("Content-Type", "application/x-www-form-urlencoded")
.PostUrlEncodedAsync(keyValues, cancellationToken: cancellationToken)
.ReceiveJson<AccessTokenResponse>();
}

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
);
}
1 change: 0 additions & 1 deletion src/keycloak/Keycloak.Library/Clients/KeycloakClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("username", userName),
new KeyValuePair<string, string>("password", password),
new KeyValuePair<string, string>("client_id", "admin-cli")
},
cancellationToken: cancellationToken)
.ReceiveJson<AccessTokenResponse>().ConfigureAwait(ConfigureAwaitOptions.None);

return result.AccessToken;
}

private static async Task<string> 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<KeyValuePair<string, string>>
{
new("grant_type", "client_credentials"),
new("client_secret", clientSecret),
new("client_id", clientId ?? "admin-cli")
},
cancellationToken: cancellationToken)
.ReceiveJson<AccessTokenResponse>().ConfigureAwait(ConfigureAwaitOptions.None);

return result.AccessToken;
}

public static async Task<IFlurlRequest> WithAuthenticationAsync(this IFlurlRequest request, Func<CancellationToken, Task<string>>? 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))
Expand All @@ -110,8 +48,4 @@ public static IFlurlRequest WithForwardedHttpHeaders(this IFlurlRequest request,

return request;
}

public record AccessTokenResponse(
[property: JsonPropertyName("access_token")] string AccessToken
);
}
1 change: 0 additions & 1 deletion src/keycloak/Keycloak.Library/Groups/KeycloakClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 7 additions & 18 deletions src/keycloak/Keycloak.Library/KeycloakClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -44,10 +44,10 @@ public partial class KeycloakClient
private readonly string? _userName;
private readonly string? _password;
private readonly string? _clientSecret;
private readonly Func<CancellationToken, Task<string>>? _getTokenAsync;
private readonly string? _authRealm;
private readonly string? _clientId;
private readonly bool _useAuthTrail;
private KeycloakAccessToken? _token;

private KeycloakClient(string url)
{
Expand All @@ -74,26 +74,12 @@ private KeycloakClient(string url, string? userName, string? password, string? a
_useAuthTrail = useAuthTrail;
}

public KeycloakClient(string url, Func<string> getToken, string? authRealm = null)
: this(url)
{
_getTokenAsync = _ => Task.FromResult(getToken());
_authRealm = authRealm;
}

public KeycloakClient(string url, Func<CancellationToken, Task<string>> 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<IFlurlRequest> GetBaseUrlAsync(string targetRealm, CancellationToken cancellationToken = default)
private async Task<IFlurlRequest> GetBaseUrlAsync(string targetRealm, CancellationToken cancellationToken = default)
{
var url = new Url(_url);
if (_useAuthTrail)
Expand All @@ -102,8 +88,11 @@ private Task<IFlurlRequest> 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);
}
}
2 changes: 0 additions & 2 deletions src/keycloak/Keycloak.Library/RolesById/KeycloakClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

namespace Org.Eclipse.TractusX.Portal.Backend.Keycloak.Seeding.Tests.Extensions;

using System.Collections.Generic;
using Xunit;

public class KeycloakRealmSettingsTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading