Skip to content

Commit 715c44f

Browse files
committed
fix(Solution): Updated solution packages
fix(Infrastructure): Fixed the OAuth2TokenManager to implement all request encodings and client authentication methods
1 parent 2b50bef commit 715c44f

25 files changed

+161
-68
lines changed

src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<ItemGroup>
1212
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.8" />
13-
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha2.11" />
13+
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha2.12" />
1414
<PackageReference Include="System.Reactive" Version="6.0.1" />
1515
</ItemGroup>
1616

src/api/Synapse.Api.Http/Synapse.Api.Http.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Neuroglia.Mediation.AspNetCore" Version="4.15.0" />
14-
<PackageReference Include="Neuroglia.Security.AspNetCore" Version="4.15.0" />
13+
<PackageReference Include="Neuroglia.Mediation.AspNetCore" Version="4.15.2" />
14+
<PackageReference Include="Neuroglia.Security.AspNetCore" Version="4.15.2" />
1515
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.7.3" />
1616
</ItemGroup>
1717

src/cli/Synapse.Cli/Synapse.Cli.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
1616
<PackageReference Include="moment.net" Version="1.3.4" />
1717
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
18-
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha2.11" />
18+
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha2.12" />
1919
<PackageReference Include="Spectre.Console" Version="0.49.1" />
2020
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
2121
</ItemGroup>

src/core/Synapse.Core.Infrastructure/Services/Interfaces/IOAuth2TokenManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ public interface IOAuth2TokenManager
2727
/// <param name="configuration">The configuration that defines how to generate the <see cref="OAuth2Token"/> to get</param>
2828
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
2929
/// <returns>An <see cref="OAuth2Token"/></returns>
30-
Task<OAuth2Token> GetTokenAsync(OAuth2AuthenticationSchemeDefinition configuration, CancellationToken cancellationToken = default);
30+
Task<OAuth2Token> GetTokenAsync(OAuth2AuthenticationSchemeDefinitionBase configuration, CancellationToken cancellationToken = default);
3131

3232
}

src/core/Synapse.Core.Infrastructure/Services/OAuth2TokenManager.cs

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@
1313

1414
using IdentityModel.Client;
1515
using Microsoft.Extensions.Logging;
16+
using Microsoft.IdentityModel.Tokens;
1617
using Neuroglia.Serialization;
18+
using ServerlessWorkflow.Sdk;
1719
using ServerlessWorkflow.Sdk.Models.Authentication;
1820
using System.Collections.Concurrent;
21+
using System.IdentityModel.Tokens.Jwt;
22+
using System.Net.Mime;
23+
using System.Security.Claims;
24+
using System.Text;
25+
using YamlDotNet.Core.Tokens;
1926

2027
namespace Synapse.Core.Infrastructure.Services;
2128

@@ -50,16 +57,49 @@ public class OAuth2TokenManager(ILogger<OAuth2TokenManager> logger, IJsonSeriali
5057
protected ConcurrentDictionary<string, OAuth2Token> Tokens { get; } = [];
5158

5259
/// <inheritdoc/>
53-
public virtual async Task<OAuth2Token> GetTokenAsync(OAuth2AuthenticationSchemeDefinition configuration, CancellationToken cancellationToken = default)
60+
public virtual async Task<OAuth2Token> GetTokenAsync(OAuth2AuthenticationSchemeDefinitionBase configuration, CancellationToken cancellationToken = default)
5461
{
5562
ArgumentNullException.ThrowIfNull(configuration);
56-
var tokenKey = $"{configuration.Client.Id}@{configuration.Authority}";
63+
Uri tokenEndpoint;
64+
if (configuration is OpenIDConnectSchemeDefinition)
65+
{
66+
var discoveryDocument = await this.HttpClient.GetDiscoveryDocumentAsync(configuration.Authority.OriginalString, cancellationToken).ConfigureAwait(false);
67+
if (string.IsNullOrWhiteSpace(discoveryDocument.TokenEndpoint)) throw new NullReferenceException("The token endpoint is not documented by the OIDC discovery document");
68+
tokenEndpoint = new(discoveryDocument.TokenEndpoint!);
69+
}
70+
else if (configuration is OAuth2AuthenticationSchemeDefinition oauth2) tokenEndpoint = oauth2.Endpoints.Token;
71+
else throw new NotSupportedException($"The specified scheme type '{configuration.GetType().FullName}' is not supported in this context");
72+
var tokenKey = $"{configuration.Client?.Id}@{configuration.Authority}";
5773
var properties = new Dictionary<string, string>()
5874
{
59-
{ "grant_type", configuration.Grant },
60-
{ "client_id", configuration.Client.Id }
75+
{ "grant_type", configuration.Grant }
6176
};
62-
if (!string.IsNullOrWhiteSpace(configuration.Client.Secret)) properties["client_secret"] = configuration.Client.Secret;
77+
switch (configuration.Client?.Authentication)
78+
{
79+
case null:
80+
if(!string.IsNullOrWhiteSpace(configuration.Client?.Id) && !string.IsNullOrWhiteSpace(configuration.Client?.Secret))
81+
{
82+
properties["client_id"] = configuration.Client.Id!;
83+
properties["client_secret"] = configuration.Client.Secret!;
84+
}
85+
break;
86+
case OAuth2ClientAuthenticationMethod.Post:
87+
this.ThrowIfInvalidClientCredentials(configuration.Client);
88+
properties["client_id"] = configuration.Client.Id!;
89+
properties["client_secret"] = configuration.Client.Secret!;
90+
break;
91+
case OAuth2ClientAuthenticationMethod.JwT:
92+
this.ThrowIfInvalidClientCredentials(configuration.Client);
93+
properties["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
94+
properties["client_assertion"] = this.CreateClientAssertionJwt(configuration.Client.Id!, tokenEndpoint.OriginalString, new(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.Client.Secret!)), SecurityAlgorithms.HmacSha256));
95+
break;
96+
case OAuth2ClientAuthenticationMethod.PrivateKey:
97+
this.ThrowIfInvalidClientCredentials(configuration.Client);
98+
throw new NotImplementedException(); //todo
99+
case OAuth2ClientAuthenticationMethod.Basic:
100+
break;
101+
default: throw new NotSupportedException($"The specified OAUTH2 client authentication method '{configuration.Client?.Authentication}' is not supported");
102+
}
63103
if (configuration.Scopes?.Count > 0) properties["scope"] = string.Join(" ", configuration.Scopes);
64104
if (configuration.Audiences?.Count > 0) properties["audience"] = string.Join(" ", configuration.Audiences);
65105
if (!string.IsNullOrWhiteSpace(configuration.Username)) properties["username"] = configuration.Username;
@@ -84,11 +124,18 @@ public virtual async Task<OAuth2Token> GetTokenAsync(OAuth2AuthenticationSchemeD
84124
}
85125
else return token;
86126
}
87-
var discoveryDocument = await this.HttpClient.GetDiscoveryDocumentAsync(configuration.Authority.OriginalString, cancellationToken).ConfigureAwait(false);
88-
using var request = new HttpRequestMessage(HttpMethod.Post, discoveryDocument.TokenEndpoint)
127+
using var content = configuration.Request.Encoding switch
89128
{
90-
Content = new FormUrlEncodedContent(properties)
129+
OAuth2RequestEncoding.FormUrl => (HttpContent)new FormUrlEncodedContent(properties),
130+
OAuth2RequestEncoding.Json => new StringContent(this.JsonSerializer.SerializeToText(properties), Encoding.UTF8, MediaTypeNames.Application.Json),
131+
_ => throw new NotSupportedException($"The specified OAUTH2 request encoding '{configuration.Request.Encoding}' is not supported")
91132
};
133+
using var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint) { Content = content };
134+
if (configuration.Client?.Authentication == OAuth2ClientAuthenticationMethod.Basic)
135+
{
136+
this.ThrowIfInvalidClientCredentials(configuration.Client);
137+
request.Headers.Authorization = new("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{configuration.Client.Id}:{configuration.Client.Secret}")));
138+
}
92139
using var response = await this.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
93140
var json = await response.Content?.ReadAsStringAsync(cancellationToken)!;
94141
if (!response.IsSuccessStatusCode)
@@ -101,4 +148,36 @@ public virtual async Task<OAuth2Token> GetTokenAsync(OAuth2AuthenticationSchemeD
101148
return token;
102149
}
103150

151+
/// <summary>
152+
/// Throws a new <see cref="Exception"/> if the specified client credentials have not been properly configured, as required by the configured authentication method
153+
/// </summary>
154+
/// <param name="client">The client credentials to validate</param>
155+
protected virtual void ThrowIfInvalidClientCredentials(OAuth2AuthenticationClientDefinition? client)
156+
{
157+
if(string.IsNullOrWhiteSpace(client?.Id) || string.IsNullOrWhiteSpace(client?.Secret)) throw new NullReferenceException($"The client id and client secret must be configured when using the '{client?.Authentication}' OAUTH2 authentication method");
158+
}
159+
160+
/// <summary>
161+
/// Creates a JSON Web Token (JWT) for client authentication using the provided client ID, audience and signing credentials.
162+
/// </summary>
163+
/// <param name="clientId">The client ID used as the subject and issuer of the JWT</param>
164+
/// <param name="audience">The audience for which the JWT is intended, typically the token endpoint URL</param>
165+
/// <param name="signingCredentials">The credentials used to signed the JWT</param>
166+
/// <returns>A signed JWT in string format, to be used as a client assertion in OAuth 2.0 requests</returns>
167+
protected virtual string CreateClientAssertionJwt(string clientId, string audience, SigningCredentials signingCredentials)
168+
{
169+
ArgumentException.ThrowIfNullOrWhiteSpace(clientId);
170+
ArgumentException.ThrowIfNullOrWhiteSpace(audience);
171+
ArgumentNullException.ThrowIfNull(signingCredentials);
172+
var tokenHandler = new JwtSecurityTokenHandler();
173+
var claims = new List<Claim>
174+
{
175+
new(JwtRegisteredClaimNames.Sub, clientId),
176+
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
177+
new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
178+
};
179+
var token = new JwtSecurityToken(clientId, audience, claims, DateTime.UtcNow, DateTime.UtcNow.AddMinutes(5), signingCredentials);
180+
return tokenHandler.WriteToken(token);
181+
}
182+
104183
}

src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010

1111
<ItemGroup>
1212
<PackageReference Include="IdentityModel" Version="7.0.0" />
13-
<PackageReference Include="Neuroglia.Data.Expressions.Abstractions" Version="4.15.0" />
14-
<PackageReference Include="Neuroglia.Data.Infrastructure.Redis" Version="4.15.0" />
15-
<PackageReference Include="Neuroglia.Data.Infrastructure.ResourceOriented.Redis" Version="4.15.0" />
16-
<PackageReference Include="Neuroglia.Mediation" Version="4.15.0" />
17-
<PackageReference Include="Neuroglia.Plugins" Version="4.15.0" />
18-
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha2.11" />
13+
<PackageReference Include="Neuroglia.Data.Expressions.Abstractions" Version="4.15.2" />
14+
<PackageReference Include="Neuroglia.Data.Infrastructure.Redis" Version="4.15.2" />
15+
<PackageReference Include="Neuroglia.Data.Infrastructure.ResourceOriented.Redis" Version="4.15.2" />
16+
<PackageReference Include="Neuroglia.Mediation" Version="4.15.2" />
17+
<PackageReference Include="Neuroglia.Plugins" Version="4.15.2" />
18+
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha2.12" />
19+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
1920
</ItemGroup>
2021

2122
<ItemGroup>

src/core/Synapse.Core/Synapse.Core.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@
3232
</ItemGroup>
3333

3434
<ItemGroup>
35-
<PackageReference Include="Neuroglia.Data.Infrastructure.ResourceOriented" Version="4.15.0" />
36-
<PackageReference Include="Neuroglia.Eventing.CloudEvents" Version="4.15.0" />
35+
<PackageReference Include="Neuroglia.Data.Infrastructure.ResourceOriented" Version="4.15.2" />
36+
<PackageReference Include="Neuroglia.Eventing.CloudEvents" Version="4.15.2" />
3737
<PackageReference Include="Semver" Version="2.3.0" />
38-
<PackageReference Include="ServerlessWorkflow.Sdk" Version="1.0.0-alpha2.11" />
38+
<PackageReference Include="ServerlessWorkflow.Sdk" Version="1.0.0-alpha2.12" />
3939
</ItemGroup>
4040

4141
</Project>

src/correlator/Synapse.Correlator/Synapse.Correlator.csproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
<PackageReference Include="Microsoft.Extensions.Configuration.KeyPerFile" Version="8.0.8" />
1717
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
1818
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
19-
<PackageReference Include="Neuroglia.Data.Expressions.JavaScript" Version="4.15.0" />
20-
<PackageReference Include="Neuroglia.Data.Expressions.JQ" Version="4.15.0" />
21-
<PackageReference Include="Neuroglia.Eventing.CloudEvents.AspNetCore" Version="4.15.0" />
22-
<PackageReference Include="Neuroglia.Mediation.AspNetCore" Version="4.15.0" />
23-
<PackageReference Include="Neuroglia.Eventing.CloudEvents.Infrastructure" Version="4.15.0" />
24-
<PackageReference Include="Neuroglia.Security.AspNetCore" Version="4.15.0" />
19+
<PackageReference Include="Neuroglia.Data.Expressions.JavaScript" Version="4.15.2" />
20+
<PackageReference Include="Neuroglia.Data.Expressions.JQ" Version="4.15.2" />
21+
<PackageReference Include="Neuroglia.Eventing.CloudEvents.AspNetCore" Version="4.15.2" />
22+
<PackageReference Include="Neuroglia.Mediation.AspNetCore" Version="4.15.2" />
23+
<PackageReference Include="Neuroglia.Eventing.CloudEvents.Infrastructure" Version="4.15.2" />
24+
<PackageReference Include="Neuroglia.Security.AspNetCore" Version="4.15.2" />
2525
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.7.3" />
2626
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.7.3" />
2727
</ItemGroup>

src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.8" />
3535
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" PrivateAssets="all" />
3636
<PackageReference Include="moment.net" Version="1.3.4" />
37-
<PackageReference Include="Neuroglia.Blazor.Dagre" Version="4.15.0" />
37+
<PackageReference Include="Neuroglia.Blazor.Dagre" Version="4.15.2" />
3838
</ItemGroup>
3939

4040
<ItemGroup>

src/runner/Synapse.Runner/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
services.AddPythonScriptExecutor();
4242
services.AddSynapseHttpApiClient(http =>
4343
{
44-
var configuration = new ServerlessWorkflow.Sdk.Models.Authentication.OAuth2AuthenticationSchemeDefinition()
44+
var configuration = new ServerlessWorkflow.Sdk.Models.Authentication.OpenIDConnectSchemeDefinition()
4545
{
4646
Authority = options.Api.BaseAddress,
4747
Grant = OAuth2GrantType.ClientCredentials,

0 commit comments

Comments
 (0)