@@ -42,18 +42,30 @@ namespace RabbitMQ.Client.OAuth2
42
42
{
43
43
public class OAuth2ClientBuilder
44
44
{
45
+ /// <summary>Discovery endpoint subpath for all OpenID Connect issuers.</summary>
46
+ const string DISCOVERY_ENDPOINT = ".well-known/openid-configuration" ;
47
+
45
48
private readonly string _clientId ;
46
49
private readonly string _clientSecret ;
47
- private readonly Uri _tokenEndpoint ;
50
+
51
+ // At least one of the following Uris is not null
52
+ private readonly Uri ? _tokenEndpoint ;
53
+ private readonly Uri ? _issuer ;
54
+
48
55
private string ? _scope ;
49
56
private IDictionary < string , string > ? _additionalRequestParameters ;
50
57
private HttpClientHandler ? _httpClientHandler ;
51
58
52
- public OAuth2ClientBuilder ( string clientId , string clientSecret , Uri tokenEndpoint )
59
+ public OAuth2ClientBuilder ( string clientId , string clientSecret , Uri ? tokenEndpoint = null , Uri ? issuer = null )
53
60
{
54
61
_clientId = clientId ?? throw new ArgumentNullException ( nameof ( clientId ) ) ;
55
62
_clientSecret = clientSecret ?? throw new ArgumentNullException ( nameof ( clientSecret ) ) ;
56
- _tokenEndpoint = tokenEndpoint ?? throw new ArgumentNullException ( nameof ( tokenEndpoint ) ) ;
63
+
64
+ if ( tokenEndpoint is null && issuer is null )
65
+ throw new ArgumentException ( "Either token endpoint or issuer is required" ) ;
66
+
67
+ _tokenEndpoint = tokenEndpoint ;
68
+ _issuer = issuer ;
57
69
}
58
70
59
71
public OAuth2ClientBuilder SetScope ( string scope )
@@ -70,30 +82,84 @@ public OAuth2ClientBuilder SetHttpClientHandler(HttpClientHandler handler)
70
82
71
83
public OAuth2ClientBuilder AddRequestParameter ( string param , string paramValue )
72
84
{
73
- if ( param == null )
85
+ if ( param is null )
74
86
{
75
- throw new ArgumentNullException ( " param is null" ) ;
87
+ throw new ArgumentNullException ( nameof ( param ) ) ;
76
88
}
77
89
78
- if ( paramValue == null )
90
+ if ( paramValue is null )
79
91
{
80
- throw new ArgumentNullException ( " paramValue is null" ) ;
92
+ throw new ArgumentNullException ( nameof ( paramValue ) ) ;
81
93
}
82
94
83
- if ( _additionalRequestParameters == null )
84
- {
85
- _additionalRequestParameters = new Dictionary < string , string > ( ) ;
86
- }
95
+ _additionalRequestParameters ??= new Dictionary < string , string > ( ) ;
87
96
_additionalRequestParameters [ param ] = paramValue ;
88
97
89
98
return this ;
90
99
}
91
100
92
101
public IOAuth2Client Build ( )
93
102
{
103
+ if ( _tokenEndpoint is null )
104
+ {
105
+ // Don't know how to better handle backwards compatibily
106
+ Uri tokenEndpoint = GetTokenEndpointFromIssuerAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
107
+ return new OAuth2Client ( _clientId , _clientSecret , tokenEndpoint , _scope , _additionalRequestParameters , _httpClientHandler ) ;
108
+ }
109
+
94
110
return new OAuth2Client ( _clientId , _clientSecret , _tokenEndpoint ,
95
111
_scope , _additionalRequestParameters , _httpClientHandler ) ;
96
112
}
113
+
114
+ public async ValueTask < IOAuth2Client > BuildAsync ( CancellationToken cancellationToken = default )
115
+ {
116
+ if ( _tokenEndpoint is null )
117
+ {
118
+ Uri tokenEndpoint = await GetTokenEndpointFromIssuerAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
119
+ return new OAuth2Client ( _clientId , _clientSecret , tokenEndpoint ,
120
+ _scope , _additionalRequestParameters , _httpClientHandler ) ;
121
+ }
122
+
123
+ return new OAuth2Client ( _clientId , _clientSecret , _tokenEndpoint ,
124
+ _scope , _additionalRequestParameters , _httpClientHandler ) ;
125
+ }
126
+
127
+ private async Task < Uri > GetTokenEndpointFromIssuerAsync ( CancellationToken cancellationToken = default )
128
+ {
129
+ if ( _issuer is null )
130
+ {
131
+ throw new InvalidOperationException ( "The issuer is required" ) ;
132
+ }
133
+
134
+ using HttpClient httpClient = _httpClientHandler is null
135
+ ? new HttpClient ( )
136
+ : new HttpClient ( _httpClientHandler , false ) ;
137
+
138
+ httpClient . DefaultRequestHeaders . Accept . Clear ( ) ;
139
+ httpClient . DefaultRequestHeaders . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "application/json" ) ) ;
140
+
141
+ // Build endpoint from Issuer and dicovery endpoint, we can't use the Uri overload because the Issuer Uri may not have a trailing '/'
142
+ string tempIssuer = _issuer . AbsoluteUri . EndsWith ( "/" ) ? _issuer . AbsoluteUri : _issuer . AbsoluteUri + "/" ;
143
+ Uri discoveryEndpoint = new Uri ( tempIssuer + DISCOVERY_ENDPOINT ) ;
144
+
145
+ using HttpRequestMessage req = new HttpRequestMessage ( HttpMethod . Get , discoveryEndpoint ) ;
146
+ using HttpResponseMessage response = await httpClient . SendAsync ( req , cancellationToken )
147
+ . ConfigureAwait ( false ) ;
148
+
149
+ response . EnsureSuccessStatusCode ( ) ;
150
+
151
+ OpenIDConnectDiscovery ? discovery = await response . Content . ReadFromJsonAsync < OpenIDConnectDiscovery > ( cancellationToken : cancellationToken )
152
+ . ConfigureAwait ( false ) ;
153
+
154
+ if ( discovery is null || string . IsNullOrEmpty ( discovery . TokenEndpoint ) )
155
+ {
156
+ throw new InvalidOperationException ( "No token endpoint was found" ) ;
157
+ }
158
+ else
159
+ {
160
+ return new Uri ( discovery . TokenEndpoint ) ;
161
+ }
162
+ }
97
163
}
98
164
99
165
/**
@@ -119,7 +185,7 @@ internal class OAuth2Client : IOAuth2Client, IDisposable
119
185
120
186
public static readonly IDictionary < string , string > EMPTY = new Dictionary < string , string > ( ) ;
121
187
122
- private HttpClient _httpClient ;
188
+ private readonly HttpClient _httpClient ;
123
189
124
190
public OAuth2Client ( string clientId , string clientSecret , Uri tokenEndpoint ,
125
191
string ? scope ,
@@ -150,12 +216,12 @@ public async Task<IToken> RequestTokenAsync(CancellationToken cancellationToken
150
216
using HttpRequestMessage req = new HttpRequestMessage ( HttpMethod . Post , _tokenEndpoint ) ;
151
217
req . Content = new FormUrlEncodedContent ( BuildRequestParameters ( ) ) ;
152
218
153
- using HttpResponseMessage response = await _httpClient . SendAsync ( req )
219
+ using HttpResponseMessage response = await _httpClient . SendAsync ( req , cancellationToken )
154
220
. ConfigureAwait ( false ) ;
155
221
156
222
response . EnsureSuccessStatusCode ( ) ;
157
223
158
- JsonToken ? token = await response . Content . ReadFromJsonAsync < JsonToken > ( )
224
+ JsonToken ? token = await response . Content . ReadFromJsonAsync < JsonToken > ( cancellationToken : cancellationToken )
159
225
. ConfigureAwait ( false ) ;
160
226
161
227
if ( token is null )
@@ -172,22 +238,20 @@ public async Task<IToken> RequestTokenAsync(CancellationToken cancellationToken
172
238
public async Task < IToken > RefreshTokenAsync ( IToken token ,
173
239
CancellationToken cancellationToken = default )
174
240
{
175
- if ( token . RefreshToken == null )
241
+ if ( token . RefreshToken is null )
176
242
{
177
243
throw new InvalidOperationException ( "Token has no Refresh Token" ) ;
178
244
}
179
245
180
- using HttpRequestMessage req = new HttpRequestMessage ( HttpMethod . Post , _tokenEndpoint )
181
- {
182
- Content = new FormUrlEncodedContent ( BuildRefreshParameters ( token ) )
183
- } ;
246
+ using HttpRequestMessage req = new HttpRequestMessage ( HttpMethod . Post , _tokenEndpoint ) ;
247
+ req . Content = new FormUrlEncodedContent ( BuildRefreshParameters ( token ) ) ;
184
248
185
- using HttpResponseMessage response = await _httpClient . SendAsync ( req )
249
+ using HttpResponseMessage response = await _httpClient . SendAsync ( req , cancellationToken )
186
250
. ConfigureAwait ( false ) ;
187
251
188
252
response . EnsureSuccessStatusCode ( ) ;
189
253
190
- JsonToken ? refreshedToken = await response . Content . ReadFromJsonAsync < JsonToken > ( )
254
+ JsonToken ? refreshedToken = await response . Content . ReadFromJsonAsync < JsonToken > ( cancellationToken : cancellationToken )
191
255
. ConfigureAwait ( false ) ;
192
256
193
257
if ( refreshedToken is null )
@@ -214,9 +278,9 @@ private Dictionary<string, string> BuildRequestParameters()
214
278
{ CLIENT_SECRET , _clientSecret }
215
279
} ;
216
280
217
- if ( _scope != null && _scope . Length > 0 )
281
+ if ( ! string . IsNullOrEmpty ( _scope ) )
218
282
{
219
- dict . Add ( SCOPE , _scope ) ;
283
+ dict . Add ( SCOPE , _scope ! ) ;
220
284
}
221
285
222
286
dict . Add ( GRANT_TYPE , GRANT_TYPE_CLIENT_CREDENTIALS ) ;
@@ -284,4 +348,23 @@ public long ExpiresIn
284
348
get ; set ;
285
349
}
286
350
}
351
+
352
+ internal class OpenIDConnectDiscovery
353
+ {
354
+ public OpenIDConnectDiscovery ( )
355
+ {
356
+ TokenEndpoint = string . Empty ;
357
+ }
358
+
359
+ public OpenIDConnectDiscovery ( string tokenEndpoint )
360
+ {
361
+ TokenEndpoint = tokenEndpoint ;
362
+ }
363
+
364
+ [ JsonPropertyName ( "token_endpoint" ) ]
365
+ public string TokenEndpoint
366
+ {
367
+ get ; set ;
368
+ }
369
+ }
287
370
}
0 commit comments