Skip to content

Commit 81e3846

Browse files
committed
Implemented basis Token Endpoint fetching
Added retrieving of Token Endpoint if only the Issuer is provided to the builder
1 parent f3f6bb2 commit 81e3846

File tree

3 files changed

+108
-24
lines changed

3 files changed

+108
-24
lines changed

projects/RabbitMQ.Client.OAuth2/OAuth2Client.cs

Lines changed: 106 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,30 @@ namespace RabbitMQ.Client.OAuth2
4242
{
4343
public class OAuth2ClientBuilder
4444
{
45+
/// <summary>Discovery endpoint subpath for all OpenID Connect issuers.</summary>
46+
const string DISCOVERY_ENDPOINT = ".well-known/openid-configuration";
47+
4548
private readonly string _clientId;
4649
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+
4855
private string? _scope;
4956
private IDictionary<string, string>? _additionalRequestParameters;
5057
private HttpClientHandler? _httpClientHandler;
5158

52-
public OAuth2ClientBuilder(string clientId, string clientSecret, Uri tokenEndpoint)
59+
public OAuth2ClientBuilder(string clientId, string clientSecret, Uri? tokenEndpoint = null, Uri? issuer = null)
5360
{
5461
_clientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
5562
_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;
5769
}
5870

5971
public OAuth2ClientBuilder SetScope(string scope)
@@ -70,30 +82,84 @@ public OAuth2ClientBuilder SetHttpClientHandler(HttpClientHandler handler)
7082

7183
public OAuth2ClientBuilder AddRequestParameter(string param, string paramValue)
7284
{
73-
if (param == null)
85+
if (param is null)
7486
{
75-
throw new ArgumentNullException("param is null");
87+
throw new ArgumentNullException(nameof(param));
7688
}
7789

78-
if (paramValue == null)
90+
if (paramValue is null)
7991
{
80-
throw new ArgumentNullException("paramValue is null");
92+
throw new ArgumentNullException(nameof(paramValue));
8193
}
8294

83-
if (_additionalRequestParameters == null)
84-
{
85-
_additionalRequestParameters = new Dictionary<string, string>();
86-
}
95+
_additionalRequestParameters ??= new Dictionary<string, string>();
8796
_additionalRequestParameters[param] = paramValue;
8897

8998
return this;
9099
}
91100

92101
public IOAuth2Client Build()
93102
{
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+
94110
return new OAuth2Client(_clientId, _clientSecret, _tokenEndpoint,
95111
_scope, _additionalRequestParameters, _httpClientHandler);
96112
}
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+
}
97163
}
98164

99165
/**
@@ -119,7 +185,7 @@ internal class OAuth2Client : IOAuth2Client, IDisposable
119185

120186
public static readonly IDictionary<string, string> EMPTY = new Dictionary<string, string>();
121187

122-
private HttpClient _httpClient;
188+
private readonly HttpClient _httpClient;
123189

124190
public OAuth2Client(string clientId, string clientSecret, Uri tokenEndpoint,
125191
string? scope,
@@ -150,12 +216,12 @@ public async Task<IToken> RequestTokenAsync(CancellationToken cancellationToken
150216
using HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, _tokenEndpoint);
151217
req.Content = new FormUrlEncodedContent(BuildRequestParameters());
152218

153-
using HttpResponseMessage response = await _httpClient.SendAsync(req)
219+
using HttpResponseMessage response = await _httpClient.SendAsync(req, cancellationToken)
154220
.ConfigureAwait(false);
155221

156222
response.EnsureSuccessStatusCode();
157223

158-
JsonToken? token = await response.Content.ReadFromJsonAsync<JsonToken>()
224+
JsonToken? token = await response.Content.ReadFromJsonAsync<JsonToken>(cancellationToken: cancellationToken)
159225
.ConfigureAwait(false);
160226

161227
if (token is null)
@@ -172,22 +238,20 @@ public async Task<IToken> RequestTokenAsync(CancellationToken cancellationToken
172238
public async Task<IToken> RefreshTokenAsync(IToken token,
173239
CancellationToken cancellationToken = default)
174240
{
175-
if (token.RefreshToken == null)
241+
if (token.RefreshToken is null)
176242
{
177243
throw new InvalidOperationException("Token has no Refresh Token");
178244
}
179245

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));
184248

185-
using HttpResponseMessage response = await _httpClient.SendAsync(req)
249+
using HttpResponseMessage response = await _httpClient.SendAsync(req, cancellationToken)
186250
.ConfigureAwait(false);
187251

188252
response.EnsureSuccessStatusCode();
189253

190-
JsonToken? refreshedToken = await response.Content.ReadFromJsonAsync<JsonToken>()
254+
JsonToken? refreshedToken = await response.Content.ReadFromJsonAsync<JsonToken>(cancellationToken: cancellationToken)
191255
.ConfigureAwait(false);
192256

193257
if (refreshedToken is null)
@@ -214,9 +278,9 @@ private Dictionary<string, string> BuildRequestParameters()
214278
{ CLIENT_SECRET, _clientSecret }
215279
};
216280

217-
if (_scope != null && _scope.Length > 0)
281+
if (!string.IsNullOrEmpty(_scope))
218282
{
219-
dict.Add(SCOPE, _scope);
283+
dict.Add(SCOPE, _scope!);
220284
}
221285

222286
dict.Add(GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS);
@@ -284,4 +348,23 @@ public long ExpiresIn
284348
get; set;
285349
}
286350
}
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+
}
287370
}

projects/RabbitMQ.Client.OAuth2/PublicAPI.Shipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ RabbitMQ.Client.OAuth2.IToken.RefreshToken.get -> string
88
RabbitMQ.Client.OAuth2.OAuth2ClientBuilder
99
RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.AddRequestParameter(string param, string paramValue) -> RabbitMQ.Client.OAuth2.OAuth2ClientBuilder
1010
RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.Build() -> RabbitMQ.Client.OAuth2.IOAuth2Client
11-
RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.OAuth2ClientBuilder(string clientId, string clientSecret, System.Uri tokenEndpoint) -> void
11+
RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.OAuth2ClientBuilder(string! clientId, string! clientSecret, System.Uri? tokenEndpoint = null, System.Uri? issuer = null) -> void
1212
RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.SetScope(string scope) -> RabbitMQ.Client.OAuth2.OAuth2ClientBuilder
1313
RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider
1414
RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.Name.get -> string

projects/RabbitMQ.Client.OAuth2/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ RabbitMQ.Client.OAuth2.CredentialsRefresherEventSource.Stopped(string! name) ->
1010
RabbitMQ.Client.OAuth2.IOAuth2Client.RefreshTokenAsync(RabbitMQ.Client.OAuth2.IToken! token, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<RabbitMQ.Client.OAuth2.IToken!>!
1111
RabbitMQ.Client.OAuth2.IOAuth2Client.RequestTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<RabbitMQ.Client.OAuth2.IToken!>!
1212
RabbitMQ.Client.OAuth2.NotifyCredentialsRefreshedAsync
13+
RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.BuildAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<RabbitMQ.Client.OAuth2.IOAuth2Client!>
1314
RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.SetHttpClientHandler(System.Net.Http.HttpClientHandler! handler) -> RabbitMQ.Client.OAuth2.OAuth2ClientBuilder!
1415
RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.Dispose() -> void
1516
RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.GetCredentialsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<RabbitMQ.Client.Credentials!>!

0 commit comments

Comments
 (0)