13
13
14
14
using IdentityModel . Client ;
15
15
using Microsoft . Extensions . Logging ;
16
+ using Microsoft . IdentityModel . Tokens ;
16
17
using Neuroglia . Serialization ;
18
+ using ServerlessWorkflow . Sdk ;
17
19
using ServerlessWorkflow . Sdk . Models . Authentication ;
18
20
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 ;
19
26
20
27
namespace Synapse . Core . Infrastructure . Services ;
21
28
@@ -50,16 +57,49 @@ public class OAuth2TokenManager(ILogger<OAuth2TokenManager> logger, IJsonSeriali
50
57
protected ConcurrentDictionary < string , OAuth2Token > Tokens { get ; } = [ ] ;
51
58
52
59
/// <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 )
54
61
{
55
62
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 } ";
57
73
var properties = new Dictionary < string , string > ( )
58
74
{
59
- { "grant_type" , configuration . Grant } ,
60
- { "client_id" , configuration . Client . Id }
75
+ { "grant_type" , configuration . Grant }
61
76
} ;
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
+ }
63
103
if ( configuration . Scopes ? . Count > 0 ) properties [ "scope" ] = string . Join ( " " , configuration . Scopes ) ;
64
104
if ( configuration . Audiences ? . Count > 0 ) properties [ "audience" ] = string . Join ( " " , configuration . Audiences ) ;
65
105
if ( ! string . IsNullOrWhiteSpace ( configuration . Username ) ) properties [ "username" ] = configuration . Username ;
@@ -84,11 +124,18 @@ public virtual async Task<OAuth2Token> GetTokenAsync(OAuth2AuthenticationSchemeD
84
124
}
85
125
else return token ;
86
126
}
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
89
128
{
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")
91
132
} ;
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
+ }
92
139
using var response = await this . HttpClient . SendAsync ( request , cancellationToken ) . ConfigureAwait ( false ) ;
93
140
var json = await response . Content ? . ReadAsStringAsync ( cancellationToken ) ! ;
94
141
if ( ! response . IsSuccessStatusCode )
@@ -101,4 +148,36 @@ public virtual async Task<OAuth2Token> GetTokenAsync(OAuth2AuthenticationSchemeD
101
148
return token ;
102
149
}
103
150
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
+
104
183
}
0 commit comments