diff --git a/projects/Directory.Packages.props b/projects/Directory.Packages.props index 649439c40e..34cca5f24a 100644 --- a/projects/Directory.Packages.props +++ b/projects/Directory.Packages.props @@ -5,21 +5,21 @@ - - + + - - + + - - + + - + @@ -46,4 +46,4 @@ - + \ No newline at end of file diff --git a/projects/RabbitMQ.Client.OAuth2/CredentialsRefresher.cs b/projects/RabbitMQ.Client.OAuth2/CredentialsRefresher.cs new file mode 100644 index 0000000000..14b639b712 --- /dev/null +++ b/projects/RabbitMQ.Client.OAuth2/CredentialsRefresher.cs @@ -0,0 +1,144 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License 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. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace RabbitMQ.Client.OAuth2 +{ + public delegate Task NotifyCredentialsRefreshedAsync(Credentials? credentials, + Exception? exception = null, + CancellationToken cancellationToken = default); + + public class CredentialsRefresher : IDisposable + { + private readonly ICredentialsProvider _credentialsProvider; + private readonly NotifyCredentialsRefreshedAsync _onRefreshed; + + private readonly CancellationTokenSource _internalCts = new CancellationTokenSource(); + private readonly CancellationTokenSource _linkedCts; + + private readonly Task _refreshTask; + + private Credentials? _credentials; + private bool _disposedValue = false; + + public CredentialsRefresher(ICredentialsProvider credentialsProvider, + NotifyCredentialsRefreshedAsync onRefreshed, + CancellationToken cancellationToken) + { + if (credentialsProvider is null) + { + throw new ArgumentNullException(nameof(credentialsProvider)); + } + + if (onRefreshed is null) + { + throw new ArgumentNullException(nameof(onRefreshed)); + } + + _linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_internalCts.Token, cancellationToken); + + _credentialsProvider = credentialsProvider; + _onRefreshed = onRefreshed; + + _refreshTask = Task.Run(RefreshLoopAsync, _linkedCts.Token); + + CredentialsRefresherEventSource.Log.Started(_credentialsProvider.Name); + } + + public Credentials? Credentials => _credentials; + + private async Task RefreshLoopAsync() + { + while (false == _linkedCts.IsCancellationRequested) + { + try + { + _credentials = await _credentialsProvider.GetCredentialsAsync(_linkedCts.Token) + .ConfigureAwait(false); + + if (_linkedCts.IsCancellationRequested) + { + break; + } + + CredentialsRefresherEventSource.Log.RefreshedCredentials(_credentialsProvider.Name); + + await _onRefreshed(_credentials, null, _linkedCts.Token) + .ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) + { + await _onRefreshed(null, ex, _linkedCts.Token) + .ConfigureAwait(false); + } + + TimeSpan delaySpan = TimeSpan.FromSeconds(30); + if (_credentials != null && _credentials.ValidUntil.HasValue) + { + delaySpan = TimeSpan.FromMilliseconds(_credentials.ValidUntil.Value.TotalMilliseconds * (1.0 - (1 / 3.0))); + } + + await Task.Delay(delaySpan, _linkedCts.Token) + .ConfigureAwait(false); + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _internalCts.Cancel(); + _refreshTask.Wait(TimeSpan.FromSeconds(5)); + _internalCts.Dispose(); + _linkedCts.Dispose(); + CredentialsRefresherEventSource.Log.Stopped(_credentialsProvider.Name); + } + + _disposedValue = true; + } + } + } +} diff --git a/projects/RabbitMQ.Client.OAuth2/CredentialsRefresherEventSource.cs b/projects/RabbitMQ.Client.OAuth2/CredentialsRefresherEventSource.cs new file mode 100644 index 0000000000..b9d6707941 --- /dev/null +++ b/projects/RabbitMQ.Client.OAuth2/CredentialsRefresherEventSource.cs @@ -0,0 +1,54 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License 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. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +//--------------------------------------------------------------------------- + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; + +namespace RabbitMQ.Client.OAuth2 +{ + [EventSource(Name = "CredentialRefresher")] + public class CredentialsRefresherEventSource : EventSource + { + public static CredentialsRefresherEventSource Log { get; } = new CredentialsRefresherEventSource(); + + [Event(1)] + public void Started(string name) => WriteEvent(1, "Started", name); + + [Event(2)] + public void Stopped(string name) => WriteEvent(2, "Stopped", name); + + [Event(3)] +#if NET6_0_OR_GREATER + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe")] +#endif + public void RefreshedCredentials(string name) => WriteEvent(3, "RefreshedCredentials", name); + } +} diff --git a/projects/RabbitMQ.Client.OAuth2/IOAuth2Client.cs b/projects/RabbitMQ.Client.OAuth2/IOAuth2Client.cs new file mode 100644 index 0000000000..ee1f025a92 --- /dev/null +++ b/projects/RabbitMQ.Client.OAuth2/IOAuth2Client.cs @@ -0,0 +1,42 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License 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. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +//--------------------------------------------------------------------------- + +using System.Threading; +using System.Threading.Tasks; + +namespace RabbitMQ.Client.OAuth2 +{ + public interface IOAuth2Client + { + Task RequestTokenAsync(CancellationToken cancellationToken = default); + Task RefreshTokenAsync(IToken token, CancellationToken cancellationToken = default); + } +} diff --git a/projects/RabbitMQ.Client.OAuth2/OAuth2Client.cs b/projects/RabbitMQ.Client.OAuth2/OAuth2Client.cs index 2598d3a4e7..d14c0f725e 100644 --- a/projects/RabbitMQ.Client.OAuth2/OAuth2Client.cs +++ b/projects/RabbitMQ.Client.OAuth2/OAuth2Client.cs @@ -34,84 +34,26 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; namespace RabbitMQ.Client.OAuth2 { - public interface IOAuth2Client - { - IToken RequestToken(); - IToken RefreshToken(IToken token); - } - - public interface IToken - { - string AccessToken { get; } - string RefreshToken { get; } - TimeSpan ExpiresIn { get; } - bool hasExpired { get; } - } - - public class Token : IToken - { - private readonly JsonToken _source; - private readonly DateTime _lastTokenRenewal; - - public Token(JsonToken json) - { - this._source = json; - this._lastTokenRenewal = DateTime.Now; - } - - public string AccessToken - { - get - { - return _source.access_token; - } - } - - public string RefreshToken - { - get - { - return _source.refresh_token; - } - } - - public TimeSpan ExpiresIn - { - get - { - return TimeSpan.FromSeconds(_source.expires_in); - } - } - - bool IToken.hasExpired - { - get - { - TimeSpan age = DateTime.Now - _lastTokenRenewal; - return age > ExpiresIn; - } - } - } - public class OAuth2ClientBuilder { private readonly string _clientId; private readonly string _clientSecret; private readonly Uri _tokenEndpoint; - private string _scope; - private IDictionary _additionalRequestParameters; - private HttpClientHandler _httpClientHandler; + private string? _scope; + private IDictionary? _additionalRequestParameters; + private HttpClientHandler? _httpClientHandler; public OAuth2ClientBuilder(string clientId, string clientSecret, Uri tokenEndpoint) { _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); _clientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret)); _tokenEndpoint = tokenEndpoint ?? throw new ArgumentNullException(nameof(tokenEndpoint)); - } public OAuth2ClientBuilder SetScope(string scope) @@ -132,15 +74,18 @@ public OAuth2ClientBuilder AddRequestParameter(string param, string paramValue) { throw new ArgumentNullException("param is null"); } + if (paramValue == null) { throw new ArgumentNullException("paramValue is null"); } + if (_additionalRequestParameters == null) { _additionalRequestParameters = new Dictionary(); } _additionalRequestParameters[param] = paramValue; + return this; } @@ -169,60 +114,91 @@ internal class OAuth2Client : IOAuth2Client, IDisposable private readonly string _clientId; private readonly string _clientSecret; private readonly Uri _tokenEndpoint; - private readonly string _scope; + private readonly string? _scope; private readonly IDictionary _additionalRequestParameters; public static readonly IDictionary EMPTY = new Dictionary(); private HttpClient _httpClient; - public OAuth2Client(string clientId, string clientSecret, Uri tokenEndpoint, string scope, - IDictionary additionalRequestParameters, - HttpClientHandler httpClientHandler) + public OAuth2Client(string clientId, string clientSecret, Uri tokenEndpoint, + string? scope, + IDictionary? additionalRequestParameters, + HttpClientHandler? httpClientHandler) { - this._clientId = clientId; - this._clientSecret = clientSecret; - this._scope = scope; - this._additionalRequestParameters = additionalRequestParameters == null ? EMPTY : additionalRequestParameters; - this._tokenEndpoint = tokenEndpoint; - - _httpClient = httpClientHandler == null ? new HttpClient() : - new HttpClient(httpClientHandler); + _clientId = clientId; + _clientSecret = clientSecret; + _scope = scope; + _additionalRequestParameters = additionalRequestParameters ?? EMPTY; + _tokenEndpoint = tokenEndpoint; + + if (httpClientHandler is null) + { + _httpClient = new HttpClient(); + } + else + { + _httpClient = new HttpClient(httpClientHandler, false); + } + _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } - public IToken RequestToken() + public async Task RequestTokenAsync(CancellationToken cancellationToken = default) { - var req = new HttpRequestMessage(HttpMethod.Post, _tokenEndpoint); - req.Content = new FormUrlEncodedContent(buildRequestParameters()); - - Task response = _httpClient.SendAsync(req); - response.Wait(); - response.Result.EnsureSuccessStatusCode(); - Task token = response.Result.Content.ReadFromJsonAsync(); - token.Wait(); - return new Token(token.Result); + using HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, _tokenEndpoint); + req.Content = new FormUrlEncodedContent(BuildRequestParameters()); + + using HttpResponseMessage response = await _httpClient.SendAsync(req) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + JsonToken? token = await response.Content.ReadFromJsonAsync() + .ConfigureAwait(false); + + if (token is null) + { + // TODO specific exception? + throw new InvalidOperationException("token is null"); + } + else + { + return new Token(token); + } } - public IToken RefreshToken(IToken token) + public async Task RefreshTokenAsync(IToken token, + CancellationToken cancellationToken = default) { if (token.RefreshToken == null) { throw new InvalidOperationException("Token has no Refresh Token"); } - var req = new HttpRequestMessage(HttpMethod.Post, _tokenEndpoint) + using HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, _tokenEndpoint) { - Content = new FormUrlEncodedContent(buildRefreshParameters(token)) + Content = new FormUrlEncodedContent(BuildRefreshParameters(token)) }; - Task response = _httpClient.SendAsync(req); - response.Wait(); - response.Result.EnsureSuccessStatusCode(); - Task refreshedToken = response.Result.Content.ReadFromJsonAsync(); - refreshedToken.Wait(); - return new Token(refreshedToken.Result); + using HttpResponseMessage response = await _httpClient.SendAsync(req) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + JsonToken? refreshedToken = await response.Content.ReadFromJsonAsync() + .ConfigureAwait(false); + + if (refreshedToken is null) + { + // TODO specific exception? + throw new InvalidOperationException("refreshed token is null"); + } + else + { + return new Token(refreshedToken); + } } public void Dispose() @@ -230,66 +206,80 @@ public void Dispose() _httpClient.Dispose(); } - private Dictionary buildRequestParameters() + private Dictionary BuildRequestParameters() { var dict = new Dictionary(_additionalRequestParameters) { { CLIENT_ID, _clientId }, { CLIENT_SECRET, _clientSecret } }; + if (_scope != null && _scope.Length > 0) { dict.Add(SCOPE, _scope); } + dict.Add(GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS); + return dict; } - private Dictionary buildRefreshParameters(IToken token) + private Dictionary BuildRefreshParameters(IToken token) { - var dict = buildRequestParameters(); + Dictionary dict = BuildRequestParameters(); dict.Remove(GRANT_TYPE); dict.Add(GRANT_TYPE, REFRESH_TOKEN); + if (_scope != null) { dict.Add(SCOPE, _scope); } - dict.Add(REFRESH_TOKEN, token.RefreshToken); + + if (token.RefreshToken != null) + { + dict.Add(REFRESH_TOKEN, token.RefreshToken); + } + return dict; } } - public class JsonToken + internal class JsonToken { public JsonToken() { + AccessToken = string.Empty; + RefreshToken = string.Empty; } public JsonToken(string access_token, string refresh_token, TimeSpan expires_in_span) { - this.access_token = access_token; - this.refresh_token = refresh_token; - this.expires_in = (long)expires_in_span.TotalSeconds; + AccessToken = access_token; + RefreshToken = refresh_token; + ExpiresIn = (long)expires_in_span.TotalSeconds; } public JsonToken(string access_token, string refresh_token, long expires_in) { - this.access_token = access_token; - this.refresh_token = refresh_token; - this.expires_in = expires_in; + AccessToken = access_token; + RefreshToken = refresh_token; + ExpiresIn = expires_in; } - public string access_token + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } - public string refresh_token + [JsonPropertyName("refresh_token")] + public string? RefreshToken { get; set; } - public long expires_in + [JsonPropertyName("expires_in")] + public long ExpiresIn { get; set; } diff --git a/projects/RabbitMQ.Client.OAuth2/OAuth2CredentialsProvider.cs b/projects/RabbitMQ.Client.OAuth2/OAuth2CredentialsProvider.cs index f59b577a9b..3033a2a954 100644 --- a/projects/RabbitMQ.Client.OAuth2/OAuth2CredentialsProvider.cs +++ b/projects/RabbitMQ.Client.OAuth2/OAuth2CredentialsProvider.cs @@ -31,17 +31,17 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace RabbitMQ.Client.OAuth2 { - public class OAuth2ClientCredentialsProvider : ICredentialsProvider + public class OAuth2ClientCredentialsProvider : ICredentialsProvider, IDisposable { - const int TOKEN_RETRIEVAL_TIMEOUT = 5000; - private ReaderWriterLock _lock = new ReaderWriterLock(); + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); private readonly string _name; private readonly IOAuth2Client _oAuth2Client; - private IToken _token; + private IToken? _token; public OAuth2ClientCredentialsProvider(string name, IOAuth2Client oAuth2Client) { @@ -49,94 +49,44 @@ public OAuth2ClientCredentialsProvider(string name, IOAuth2Client oAuth2Client) _oAuth2Client = oAuth2Client ?? throw new ArgumentNullException(nameof(oAuth2Client)); } - public string Name - { - get - { - return _name; - } - } - - public string UserName - { - get - { - checkState(); - return string.Empty; - } - } + public string Name => _name; - public string Password + public async Task GetCredentialsAsync(CancellationToken cancellationToken = default) { - get - { - return checkState().AccessToken; - } - } - - public Nullable ValidUntil - { - get + await _semaphore.WaitAsync(cancellationToken) + .ConfigureAwait(false); + try { - IToken t = checkState(); - if (t is null) + if (_token == null || string.IsNullOrEmpty(_token.RefreshToken)) { - return null; + _token = await _oAuth2Client.RequestTokenAsync(cancellationToken) + .ConfigureAwait(false); } else { - return t.ExpiresIn; - } - } - } - - public void Refresh() - { - retrieveToken(); - } - - private IToken checkState() - { - _lock.AcquireReaderLock(TOKEN_RETRIEVAL_TIMEOUT); - try - { - if (_token != null) - { - return _token; + _token = await _oAuth2Client.RefreshTokenAsync(_token, cancellationToken) + .ConfigureAwait(false); } } finally { - _lock.ReleaseReaderLock(); + _semaphore.Release(); } - return retrieveToken(); - } - - private IToken retrieveToken() - { - _lock.AcquireWriterLock(TOKEN_RETRIEVAL_TIMEOUT); - try + if (_token is null) { - return requestOrRenewToken(); + throw new InvalidOperationException("_token should not be null here"); } - finally + else { - _lock.ReleaseReaderLock(); + return new Credentials(_name, string.Empty, + _token.AccessToken, _token.ExpiresIn); } } - private IToken requestOrRenewToken() + public void Dispose() { - if (_token == null || _token.RefreshToken == null) - { - _token = _oAuth2Client.RequestToken(); - } - else - { - _token = _oAuth2Client.RefreshToken(_token); - } - return _token; + _semaphore.Dispose(); } } } diff --git a/projects/RabbitMQ.Client.OAuth2/PublicAPI.Shipped.txt b/projects/RabbitMQ.Client.OAuth2/PublicAPI.Shipped.txt index e6e770d5bd..ddd490940c 100644 --- a/projects/RabbitMQ.Client.OAuth2/PublicAPI.Shipped.txt +++ b/projects/RabbitMQ.Client.OAuth2/PublicAPI.Shipped.txt @@ -1,37 +1,19 @@ #nullable enable RabbitMQ.Client.OAuth2.IOAuth2Client -RabbitMQ.Client.OAuth2.IOAuth2Client.RefreshToken(RabbitMQ.Client.OAuth2.IToken token) -> RabbitMQ.Client.OAuth2.IToken -RabbitMQ.Client.OAuth2.IOAuth2Client.RequestToken() -> RabbitMQ.Client.OAuth2.IToken RabbitMQ.Client.OAuth2.IToken RabbitMQ.Client.OAuth2.IToken.AccessToken.get -> string RabbitMQ.Client.OAuth2.IToken.ExpiresIn.get -> System.TimeSpan -RabbitMQ.Client.OAuth2.IToken.hasExpired.get -> bool +RabbitMQ.Client.OAuth2.IToken.HasExpired.get -> bool RabbitMQ.Client.OAuth2.IToken.RefreshToken.get -> string -RabbitMQ.Client.OAuth2.JsonToken -RabbitMQ.Client.OAuth2.JsonToken.access_token.get -> string -RabbitMQ.Client.OAuth2.JsonToken.access_token.set -> void -RabbitMQ.Client.OAuth2.JsonToken.expires_in.get -> long -RabbitMQ.Client.OAuth2.JsonToken.expires_in.set -> void -RabbitMQ.Client.OAuth2.JsonToken.JsonToken() -> void -RabbitMQ.Client.OAuth2.JsonToken.JsonToken(string access_token, string refresh_token, long expires_in) -> void -RabbitMQ.Client.OAuth2.JsonToken.JsonToken(string access_token, string refresh_token, System.TimeSpan expires_in_span) -> void -RabbitMQ.Client.OAuth2.JsonToken.refresh_token.get -> string -RabbitMQ.Client.OAuth2.JsonToken.refresh_token.set -> void RabbitMQ.Client.OAuth2.OAuth2ClientBuilder RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.AddRequestParameter(string param, string paramValue) -> RabbitMQ.Client.OAuth2.OAuth2ClientBuilder RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.Build() -> RabbitMQ.Client.OAuth2.IOAuth2Client RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.OAuth2ClientBuilder(string clientId, string clientSecret, System.Uri tokenEndpoint) -> void -RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.SetHttpClientHandler(System.Net.Http.HttpClientHandler handler) -> RabbitMQ.Client.OAuth2.OAuth2ClientBuilder RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.SetScope(string scope) -> RabbitMQ.Client.OAuth2.OAuth2ClientBuilder RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.Name.get -> string RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.OAuth2ClientCredentialsProvider(string name, RabbitMQ.Client.OAuth2.IOAuth2Client oAuth2Client) -> void -RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.Password.get -> string -RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.Refresh() -> void -RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.UserName.get -> string -RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.ValidUntil.get -> System.TimeSpan? RabbitMQ.Client.OAuth2.Token RabbitMQ.Client.OAuth2.Token.AccessToken.get -> string RabbitMQ.Client.OAuth2.Token.ExpiresIn.get -> System.TimeSpan RabbitMQ.Client.OAuth2.Token.RefreshToken.get -> string -RabbitMQ.Client.OAuth2.Token.Token(RabbitMQ.Client.OAuth2.JsonToken json) -> void diff --git a/projects/RabbitMQ.Client.OAuth2/PublicAPI.Unshipped.txt b/projects/RabbitMQ.Client.OAuth2/PublicAPI.Unshipped.txt index e69de29bb2..f93749f14d 100644 --- a/projects/RabbitMQ.Client.OAuth2/PublicAPI.Unshipped.txt +++ b/projects/RabbitMQ.Client.OAuth2/PublicAPI.Unshipped.txt @@ -0,0 +1,17 @@ +RabbitMQ.Client.OAuth2.CredentialsRefresher +RabbitMQ.Client.OAuth2.CredentialsRefresher.Credentials.get -> RabbitMQ.Client.Credentials? +RabbitMQ.Client.OAuth2.CredentialsRefresher.CredentialsRefresher(RabbitMQ.Client.ICredentialsProvider! credentialsProvider, RabbitMQ.Client.OAuth2.NotifyCredentialsRefreshedAsync! onRefreshed, System.Threading.CancellationToken cancellationToken) -> void +RabbitMQ.Client.OAuth2.CredentialsRefresher.Dispose() -> void +RabbitMQ.Client.OAuth2.CredentialsRefresherEventSource +RabbitMQ.Client.OAuth2.CredentialsRefresherEventSource.CredentialsRefresherEventSource() -> void +RabbitMQ.Client.OAuth2.CredentialsRefresherEventSource.RefreshedCredentials(string! name) -> void +RabbitMQ.Client.OAuth2.CredentialsRefresherEventSource.Started(string! name) -> void +RabbitMQ.Client.OAuth2.CredentialsRefresherEventSource.Stopped(string! name) -> void +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.IOAuth2Client.RequestTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +RabbitMQ.Client.OAuth2.NotifyCredentialsRefreshedAsync +RabbitMQ.Client.OAuth2.OAuth2ClientBuilder.SetHttpClientHandler(System.Net.Http.HttpClientHandler! handler) -> RabbitMQ.Client.OAuth2.OAuth2ClientBuilder! +RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.Dispose() -> void +RabbitMQ.Client.OAuth2.OAuth2ClientCredentialsProvider.GetCredentialsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static RabbitMQ.Client.OAuth2.CredentialsRefresherEventSource.Log.get -> RabbitMQ.Client.OAuth2.CredentialsRefresherEventSource! +virtual RabbitMQ.Client.OAuth2.CredentialsRefresher.Dispose(bool disposing) -> void diff --git a/projects/RabbitMQ.Client.OAuth2/RabbitMQ.Client.OAuth2.csproj b/projects/RabbitMQ.Client.OAuth2/RabbitMQ.Client.OAuth2.csproj index 30f8a1db87..33a6a67ff0 100644 --- a/projects/RabbitMQ.Client.OAuth2/RabbitMQ.Client.OAuth2.csproj +++ b/projects/RabbitMQ.Client.OAuth2/RabbitMQ.Client.OAuth2.csproj @@ -1,4 +1,4 @@ - + net6.0;netstandard2.0 @@ -27,7 +27,12 @@ true ../../packages README.md - 7.3 + + 8.0 + enable @@ -44,8 +49,8 @@ - + diff --git a/projects/RabbitMQ.Client.OAuth2/Token.cs b/projects/RabbitMQ.Client.OAuth2/Token.cs new file mode 100644 index 0000000000..4c7e4941d2 --- /dev/null +++ b/projects/RabbitMQ.Client.OAuth2/Token.cs @@ -0,0 +1,88 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License 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. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +//--------------------------------------------------------------------------- + +using System; + +namespace RabbitMQ.Client.OAuth2 +{ + public interface IToken + { + string AccessToken { get; } + string? RefreshToken { get; } + TimeSpan ExpiresIn { get; } + bool HasExpired { get; } + } + + public class Token : IToken + { + private readonly JsonToken _source; + private readonly DateTime _lastTokenRenewal; + + internal Token(JsonToken json) + { + _source = json; + _lastTokenRenewal = DateTime.Now; + } + + public string AccessToken + { + get + { + return _source.AccessToken; + } + } + + public string? RefreshToken + { + get + { + return _source.RefreshToken; + } + } + + public TimeSpan ExpiresIn + { + get + { + return TimeSpan.FromSeconds(_source.ExpiresIn); + } + } + + bool IToken.HasExpired + { + get + { + TimeSpan age = DateTime.Now - _lastTokenRenewal; + return age > ExpiresIn; + } + } + } +} diff --git a/projects/RabbitMQ.Client/PublicAPI.Shipped.txt b/projects/RabbitMQ.Client/PublicAPI.Shipped.txt index 7283f7a413..d2b1c0882a 100644 --- a/projects/RabbitMQ.Client/PublicAPI.Shipped.txt +++ b/projects/RabbitMQ.Client/PublicAPI.Shipped.txt @@ -105,13 +105,6 @@ RabbitMQ.Client.AsyncDefaultBasicConsumer.IsRunning.get -> bool RabbitMQ.Client.AsyncDefaultBasicConsumer.IsRunning.set -> void RabbitMQ.Client.AsyncDefaultBasicConsumer.ShutdownReason.get -> RabbitMQ.Client.ShutdownEventArgs RabbitMQ.Client.AsyncDefaultBasicConsumer.ShutdownReason.set -> void -RabbitMQ.Client.BasicCredentialsProvider -RabbitMQ.Client.BasicCredentialsProvider.BasicCredentialsProvider(string name, string userName, string password) -> void -RabbitMQ.Client.BasicCredentialsProvider.Name.get -> string -RabbitMQ.Client.BasicCredentialsProvider.Password.get -> string -RabbitMQ.Client.BasicCredentialsProvider.Refresh() -> void -RabbitMQ.Client.BasicCredentialsProvider.UserName.get -> string -RabbitMQ.Client.BasicCredentialsProvider.ValidUntil.get -> System.TimeSpan? RabbitMQ.Client.BasicGetResult RabbitMQ.Client.BasicGetResult.BasicGetResult(ulong deliveryTag, bool redelivered, string exchange, string routingKey, uint messageCount, RabbitMQ.Client.IReadOnlyBasicProperties basicProperties, System.ReadOnlyMemory body) -> void RabbitMQ.Client.BasicProperties @@ -187,8 +180,6 @@ RabbitMQ.Client.CachedString.CachedString(string value) -> void RabbitMQ.Client.CachedString.CachedString(string value, System.ReadOnlyMemory bytes) -> void RabbitMQ.Client.CachedString.CachedString(System.ReadOnlyMemory bytes) -> void RabbitMQ.Client.ConnectionConfig -RabbitMQ.Client.ConnectionConfig.CredentialsProvider -> RabbitMQ.Client.ICredentialsProvider -RabbitMQ.Client.ConnectionConfig.CredentialsRefresher -> RabbitMQ.Client.ICredentialsRefresher RabbitMQ.Client.ConnectionFactory RabbitMQ.Client.ConnectionFactory.AmqpUriSslProtocols.get -> System.Security.Authentication.SslProtocols RabbitMQ.Client.ConnectionFactory.AmqpUriSslProtocols.set -> void @@ -206,10 +197,6 @@ RabbitMQ.Client.ConnectionFactory.ConsumerDispatchConcurrency.get -> int RabbitMQ.Client.ConnectionFactory.ConsumerDispatchConcurrency.set -> void RabbitMQ.Client.ConnectionFactory.ContinuationTimeout.get -> System.TimeSpan RabbitMQ.Client.ConnectionFactory.ContinuationTimeout.set -> void -RabbitMQ.Client.ConnectionFactory.CredentialsProvider.get -> RabbitMQ.Client.ICredentialsProvider -RabbitMQ.Client.ConnectionFactory.CredentialsProvider.set -> void -RabbitMQ.Client.ConnectionFactory.CredentialsRefresher.get -> RabbitMQ.Client.ICredentialsRefresher -RabbitMQ.Client.ConnectionFactory.CredentialsRefresher.set -> void RabbitMQ.Client.ConnectionFactory.Endpoint.get -> RabbitMQ.Client.AmqpTcpEndpoint RabbitMQ.Client.ConnectionFactory.Endpoint.set -> void RabbitMQ.Client.ConnectionFactory.EndpointResolverFactory.get -> System.Func, RabbitMQ.Client.IEndpointResolver> @@ -373,7 +360,6 @@ RabbitMQ.Client.Exceptions.WireFormattingException.WireFormattingException(strin RabbitMQ.Client.ExchangeType RabbitMQ.Client.ExternalMechanism RabbitMQ.Client.ExternalMechanism.ExternalMechanism() -> void -RabbitMQ.Client.ExternalMechanism.handleChallenge(byte[] challenge, RabbitMQ.Client.ConnectionConfig config) -> byte[] RabbitMQ.Client.ExternalMechanismFactory RabbitMQ.Client.ExternalMechanismFactory.ExternalMechanismFactory() -> void RabbitMQ.Client.ExternalMechanismFactory.GetInstance() -> RabbitMQ.Client.IAuthMechanism @@ -392,7 +378,6 @@ RabbitMQ.Client.IAsyncBasicConsumer.HandleBasicConsumeOkAsync(string consumerTag RabbitMQ.Client.IAsyncBasicConsumer.HandleBasicDeliverAsync(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, RabbitMQ.Client.IReadOnlyBasicProperties properties, System.ReadOnlyMemory body) -> System.Threading.Tasks.Task RabbitMQ.Client.IAsyncBasicConsumer.HandleChannelShutdownAsync(object channel, RabbitMQ.Client.ShutdownEventArgs reason) -> System.Threading.Tasks.Task RabbitMQ.Client.IAuthMechanism -RabbitMQ.Client.IAuthMechanism.handleChallenge(byte[] challenge, RabbitMQ.Client.ConnectionConfig config) -> byte[] RabbitMQ.Client.IAuthMechanismFactory RabbitMQ.Client.IAuthMechanismFactory.GetInstance() -> RabbitMQ.Client.IAuthMechanism RabbitMQ.Client.IAuthMechanismFactory.Name.get -> string @@ -497,10 +482,6 @@ RabbitMQ.Client.IConnectionFactory.ConsumerDispatchConcurrency.get -> int RabbitMQ.Client.IConnectionFactory.ConsumerDispatchConcurrency.set -> void RabbitMQ.Client.IConnectionFactory.ContinuationTimeout.get -> System.TimeSpan RabbitMQ.Client.IConnectionFactory.ContinuationTimeout.set -> void -RabbitMQ.Client.IConnectionFactory.CredentialsProvider.get -> RabbitMQ.Client.ICredentialsProvider -RabbitMQ.Client.IConnectionFactory.CredentialsProvider.set -> void -RabbitMQ.Client.IConnectionFactory.CredentialsRefresher.get -> RabbitMQ.Client.ICredentialsRefresher -RabbitMQ.Client.IConnectionFactory.CredentialsRefresher.set -> void RabbitMQ.Client.IConnectionFactory.HandshakeContinuationTimeout.get -> System.TimeSpan RabbitMQ.Client.IConnectionFactory.HandshakeContinuationTimeout.set -> void RabbitMQ.Client.IConnectionFactory.Password.get -> string @@ -517,15 +498,6 @@ RabbitMQ.Client.IConnectionFactory.UserName.get -> string RabbitMQ.Client.IConnectionFactory.UserName.set -> void RabbitMQ.Client.IConnectionFactory.VirtualHost.get -> string RabbitMQ.Client.IConnectionFactory.VirtualHost.set -> void -RabbitMQ.Client.ICredentialsProvider -RabbitMQ.Client.ICredentialsProvider.Name.get -> string -RabbitMQ.Client.ICredentialsProvider.Password.get -> string -RabbitMQ.Client.ICredentialsProvider.Refresh() -> void -RabbitMQ.Client.ICredentialsProvider.UserName.get -> string -RabbitMQ.Client.ICredentialsProvider.ValidUntil.get -> System.TimeSpan? -RabbitMQ.Client.ICredentialsRefresher -RabbitMQ.Client.ICredentialsRefresher.NotifyCredentialRefreshedAsync -RabbitMQ.Client.ICredentialsRefresher.Unregister(RabbitMQ.Client.ICredentialsProvider provider) -> bool RabbitMQ.Client.IEndpointResolver RabbitMQ.Client.IEndpointResolver.All() -> System.Collections.Generic.IEnumerable RabbitMQ.Client.INetworkConnection @@ -610,7 +582,6 @@ RabbitMQ.Client.Logging.RabbitMqExceptionDetail.RabbitMqExceptionDetail(System.E RabbitMQ.Client.Logging.RabbitMqExceptionDetail.StackTrace.get -> string RabbitMQ.Client.Logging.RabbitMqExceptionDetail.Type.get -> string RabbitMQ.Client.PlainMechanism -RabbitMQ.Client.PlainMechanism.handleChallenge(byte[] challenge, RabbitMQ.Client.ConnectionConfig config) -> byte[] RabbitMQ.Client.PlainMechanism.PlainMechanism() -> void RabbitMQ.Client.PlainMechanismFactory RabbitMQ.Client.PlainMechanismFactory.GetInstance() -> RabbitMQ.Client.IAuthMechanism @@ -701,17 +672,6 @@ RabbitMQ.Client.SslOption.Version.set -> void RabbitMQ.Client.TcpClientAdapter RabbitMQ.Client.TcpClientAdapter.Dispose() -> void RabbitMQ.Client.TcpClientAdapter.TcpClientAdapter(System.Net.Sockets.Socket socket) -> void -RabbitMQ.Client.TimerBasedCredentialRefresher -RabbitMQ.Client.TimerBasedCredentialRefresher.TimerBasedCredentialRefresher() -> void -RabbitMQ.Client.TimerBasedCredentialRefresher.Unregister(RabbitMQ.Client.ICredentialsProvider provider) -> bool -RabbitMQ.Client.TimerBasedCredentialRefresherEventSource -RabbitMQ.Client.TimerBasedCredentialRefresherEventSource.AlreadyRegistered(string name) -> void -RabbitMQ.Client.TimerBasedCredentialRefresherEventSource.RefreshedCredentials(string name, bool succesfully) -> void -RabbitMQ.Client.TimerBasedCredentialRefresherEventSource.Registered(string name) -> void -RabbitMQ.Client.TimerBasedCredentialRefresherEventSource.ScheduledTimer(string name, double interval) -> void -RabbitMQ.Client.TimerBasedCredentialRefresherEventSource.TimerBasedCredentialRefresherEventSource() -> void -RabbitMQ.Client.TimerBasedCredentialRefresherEventSource.TriggeredTimer(string name) -> void -RabbitMQ.Client.TimerBasedCredentialRefresherEventSource.Unregistered(string name) -> void RabbitMQ.Client.TopologyRecoveryExceptionHandler RabbitMQ.Client.TopologyRecoveryExceptionHandler.BindingRecoveryExceptionCondition.get -> System.Func RabbitMQ.Client.TopologyRecoveryExceptionHandler.BindingRecoveryExceptionCondition.set -> void @@ -814,11 +774,9 @@ static RabbitMQ.Client.QueueDeclareOk.implicit operator string(RabbitMQ.Client.Q static RabbitMQ.Client.RabbitMQActivitySource.UseRoutingKeyAsOperationName.get -> bool static RabbitMQ.Client.RabbitMQActivitySource.UseRoutingKeyAsOperationName.set -> void static RabbitMQ.Client.TcpClientAdapter.GetMatchingHost(System.Collections.Generic.IReadOnlyCollection addresses, System.Net.Sockets.AddressFamily addressFamily) -> System.Net.IPAddress -static RabbitMQ.Client.TimerBasedCredentialRefresherEventSource.Log.get -> RabbitMQ.Client.TimerBasedCredentialRefresherEventSource static readonly RabbitMQ.Client.CachedString.Empty -> RabbitMQ.Client.CachedString static readonly RabbitMQ.Client.ConnectionFactory.DefaultAuthMechanisms -> System.Collections.Generic.IEnumerable static readonly RabbitMQ.Client.ConnectionFactory.DefaultConnectionTimeout -> System.TimeSpan -static readonly RabbitMQ.Client.ConnectionFactory.DefaultCredentialsRefresher -> RabbitMQ.Client.ICredentialsRefresher static readonly RabbitMQ.Client.ConnectionFactory.DefaultHeartbeat -> System.TimeSpan static readonly RabbitMQ.Client.Protocols.AMQP_0_9_1 -> RabbitMQ.Client.IProtocol static readonly RabbitMQ.Client.Protocols.DefaultProtocol -> RabbitMQ.Client.IProtocol @@ -883,8 +841,6 @@ virtual RabbitMQ.Client.TcpClientAdapter.ReceiveTimeout.set -> void ~RabbitMQ.Client.IConnectionFactory.CreateConnectionAsync(System.Collections.Generic.IEnumerable hostnames, string clientProvidedName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task ~RabbitMQ.Client.IConnectionFactory.CreateConnectionAsync(System.Collections.Generic.IEnumerable hostnames, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task ~RabbitMQ.Client.IConnectionFactory.CreateConnectionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task -~RabbitMQ.Client.ICredentialsRefresher.Register(RabbitMQ.Client.ICredentialsProvider provider, RabbitMQ.Client.ICredentialsRefresher.NotifyCredentialRefreshedAsync callback) -> RabbitMQ.Client.ICredentialsProvider -~RabbitMQ.Client.TimerBasedCredentialRefresher.Register(RabbitMQ.Client.ICredentialsProvider provider, RabbitMQ.Client.ICredentialsRefresher.NotifyCredentialRefreshedAsync callback) -> RabbitMQ.Client.ICredentialsProvider ~RabbitMQ.Client.TopologyRecoveryExceptionHandler.BindingRecoveryExceptionHandlerAsync.get -> System.Func ~RabbitMQ.Client.TopologyRecoveryExceptionHandler.BindingRecoveryExceptionHandlerAsync.set -> void ~RabbitMQ.Client.TopologyRecoveryExceptionHandler.ConsumerRecoveryExceptionHandlerAsync.get -> System.Func diff --git a/projects/RabbitMQ.Client/PublicAPI.Unshipped.txt b/projects/RabbitMQ.Client/PublicAPI.Unshipped.txt index bbd247d625..e2c631ea30 100644 --- a/projects/RabbitMQ.Client/PublicAPI.Unshipped.txt +++ b/projects/RabbitMQ.Client/PublicAPI.Unshipped.txt @@ -1 +1,22 @@ +RabbitMQ.Client.BasicCredentialsProvider +RabbitMQ.Client.BasicCredentialsProvider.BasicCredentialsProvider(string? name, string! userName, string! password) -> void +RabbitMQ.Client.BasicCredentialsProvider.GetCredentialsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +RabbitMQ.Client.BasicCredentialsProvider.Name.get -> string! RabbitMQ.Client.BasicProperties.BasicProperties(RabbitMQ.Client.IReadOnlyBasicProperties! input) -> void +RabbitMQ.Client.ConnectionFactory.CredentialsProvider.get -> RabbitMQ.Client.ICredentialsProvider? +RabbitMQ.Client.ConnectionFactory.CredentialsProvider.set -> void +RabbitMQ.Client.Credentials +RabbitMQ.Client.Credentials.Credentials(string! name, string! userName, string! password, System.TimeSpan? validUntil) -> void +RabbitMQ.Client.Credentials.Name.get -> string! +RabbitMQ.Client.Credentials.Password.get -> string! +RabbitMQ.Client.Credentials.UserName.get -> string! +RabbitMQ.Client.Credentials.ValidUntil.get -> System.TimeSpan? +RabbitMQ.Client.ExternalMechanism.HandleChallengeAsync(byte[]? challenge, RabbitMQ.Client.ConnectionConfig! config, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +RabbitMQ.Client.IAuthMechanism.HandleChallengeAsync(byte[]? challenge, RabbitMQ.Client.ConnectionConfig! config, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +RabbitMQ.Client.IConnectionFactory.CredentialsProvider.get -> RabbitMQ.Client.ICredentialsProvider? +RabbitMQ.Client.IConnectionFactory.CredentialsProvider.set -> void +RabbitMQ.Client.ICredentialsProvider +RabbitMQ.Client.ICredentialsProvider.GetCredentialsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +RabbitMQ.Client.ICredentialsProvider.Name.get -> string! +RabbitMQ.Client.PlainMechanism.HandleChallengeAsync(byte[]? challenge, RabbitMQ.Client.ConnectionConfig! config, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +readonly RabbitMQ.Client.ConnectionConfig.CredentialsProvider -> RabbitMQ.Client.ICredentialsProvider! diff --git a/projects/RabbitMQ.Client/client/api/BasicCredentialsProvider.cs b/projects/RabbitMQ.Client/client/api/BasicCredentialsProvider.cs index 97faa08d03..850def2523 100644 --- a/projects/RabbitMQ.Client/client/api/BasicCredentialsProvider.cs +++ b/projects/RabbitMQ.Client/client/api/BasicCredentialsProvider.cs @@ -30,6 +30,8 @@ //--------------------------------------------------------------------------- using System; +using System.Threading; +using System.Threading.Tasks; namespace RabbitMQ.Client { @@ -38,39 +40,21 @@ public class BasicCredentialsProvider : ICredentialsProvider private readonly string _name; private readonly string _userName; private readonly string _password; + private readonly Credentials _credentials; public BasicCredentialsProvider(string? name, string userName, string password) { - _name = name ?? string.Empty; + _name = name ?? nameof(BasicCredentialsProvider); _userName = userName ?? throw new ArgumentNullException(nameof(userName)); _password = password ?? throw new ArgumentNullException(nameof(password)); + _credentials = new Credentials(_name, _userName, _password, null); } - public string Name - { - get { return _name; } - } - - public string UserName - { - get { return _userName; } - } - - public string Password - { - get { return _password; } - } - - public Nullable ValidUntil - { - get - { - return null; - } - } + public string Name => _name; - public void Refresh() + public Task GetCredentialsAsync(CancellationToken cancellationToken = default) { + return Task.FromResult(_credentials); } } } diff --git a/projects/RabbitMQ.Client/client/api/ConnectionConfig.cs b/projects/RabbitMQ.Client/client/api/ConnectionConfig.cs index 0dce4e8dd3..ab0a0d7665 100644 --- a/projects/RabbitMQ.Client/client/api/ConnectionConfig.cs +++ b/projects/RabbitMQ.Client/client/api/ConnectionConfig.cs @@ -58,11 +58,10 @@ public sealed class ConnectionConfig public readonly string Password; /// - /// Default CredentialsProvider implementation. If set, this + /// Default ICredentialsProvider implementation. If set, this /// overrides UserName / Password /// - public ICredentialsProvider CredentialsProvider; - public ICredentialsRefresher CredentialsRefresher; + public readonly ICredentialsProvider CredentialsProvider; /// /// SASL auth mechanisms to use. @@ -145,7 +144,7 @@ public sealed class ConnectionConfig internal readonly Func> FrameHandlerFactoryAsync; internal ConnectionConfig(string virtualHost, string userName, string password, - ICredentialsProvider? credentialsProvider, ICredentialsRefresher credentialsRefresher, + ICredentialsProvider? credentialsProvider, IEnumerable authMechanisms, IDictionary clientProperties, string? clientProvidedName, ushort maxChannelCount, uint maxFrameSize, uint maxInboundMessageBodySize, bool topologyRecoveryEnabled, @@ -157,7 +156,6 @@ internal ConnectionConfig(string virtualHost, string userName, string password, UserName = userName; Password = password; CredentialsProvider = credentialsProvider ?? new BasicCredentialsProvider(clientProvidedName, userName, password); - CredentialsRefresher = credentialsRefresher; AuthMechanisms = authMechanisms; ClientProperties = clientProperties; ClientProvidedName = clientProvidedName; diff --git a/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs b/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs index 4858395357..cbcc5760d8 100644 --- a/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs +++ b/projects/RabbitMQ.Client/client/api/ConnectionFactory.cs @@ -161,8 +161,6 @@ public sealed class ConnectionFactory : ConnectionFactoryBase, IConnectionFactor /// public static System.Net.Sockets.AddressFamily DefaultAddressFamily { get; set; } - public static readonly ICredentialsRefresher DefaultCredentialsRefresher = new NoOpCredentialsRefresher(); - /// /// Set to false to disable automatic connection recovery. /// Defaults to true. @@ -315,15 +313,10 @@ public AmqpTcpEndpoint Endpoint public string Password { get; set; } = DefaultPass; /// - /// CredentialsProvider used to obtain username and password. + /// ICredentialsProvider used to obtain username and password. /// public ICredentialsProvider? CredentialsProvider { get; set; } - /// - /// Used to refresh credentials. - /// - public ICredentialsRefresher CredentialsRefresher { get; set; } = DefaultCredentialsRefresher; - /// /// Maximum channel number to ask for. /// @@ -591,7 +584,6 @@ private ConnectionConfig CreateConfig(string? clientProvidedName) UserName, Password, CredentialsProvider, - CredentialsRefresher, AuthMechanisms, ClientProperties, EnsureClientProvidedNameLength(clientProvidedName), diff --git a/projects/RabbitMQ.Client/client/api/ExternalMechanism.cs b/projects/RabbitMQ.Client/client/api/ExternalMechanism.cs index 364881f66f..ef54d96878 100644 --- a/projects/RabbitMQ.Client/client/api/ExternalMechanism.cs +++ b/projects/RabbitMQ.Client/client/api/ExternalMechanism.cs @@ -30,6 +30,8 @@ //--------------------------------------------------------------------------- using System; +using System.Threading; +using System.Threading.Tasks; namespace RabbitMQ.Client { @@ -38,9 +40,10 @@ public class ExternalMechanism : IAuthMechanism /// /// Handle one round of challenge-response. /// - public byte[] handleChallenge(byte[]? challenge, ConnectionConfig config) + public Task HandleChallengeAsync(byte[]? challenge, ConnectionConfig config, + CancellationToken cancellationToken = default) { - return Array.Empty(); + return Task.FromResult(Array.Empty()); } } } diff --git a/projects/RabbitMQ.Client/client/api/IAuthMechanism.cs b/projects/RabbitMQ.Client/client/api/IAuthMechanism.cs index 08c44e4af5..3f33195aa7 100644 --- a/projects/RabbitMQ.Client/client/api/IAuthMechanism.cs +++ b/projects/RabbitMQ.Client/client/api/IAuthMechanism.cs @@ -29,6 +29,9 @@ // Copyright (c) 2007-2024 Broadcom. All Rights Reserved. //--------------------------------------------------------------------------- +using System.Threading; +using System.Threading.Tasks; + namespace RabbitMQ.Client { /// @@ -39,6 +42,7 @@ public interface IAuthMechanism /// /// Handle one round of challenge-response. /// - byte[] handleChallenge(byte[]? challenge, ConnectionConfig config); + Task HandleChallengeAsync(byte[]? challenge, ConnectionConfig config, + CancellationToken cancellationToken = default); } } diff --git a/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs b/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs index 28d471c9e2..c6b37ec3ed 100644 --- a/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs +++ b/projects/RabbitMQ.Client/client/api/IConnectionFactory.cs @@ -75,12 +75,9 @@ public interface IConnectionFactory string VirtualHost { get; set; } /// - /// Credentials provider. It is optional. When set, username and password - /// are obtained thru this provider. + /// ICredentialsProvider used to obtain username and password. /// - ICredentialsProvider? CredentialsProvider { get; set; } - - ICredentialsRefresher CredentialsRefresher { get; set; } + public ICredentialsProvider? CredentialsProvider { get; set; } /// /// Sets or gets the AMQP Uri to be used for connections. diff --git a/projects/RabbitMQ.Client/client/api/ICredentialsProvider.cs b/projects/RabbitMQ.Client/client/api/ICredentialsProvider.cs index 9cd57a3991..1518331e90 100644 --- a/projects/RabbitMQ.Client/client/api/ICredentialsProvider.cs +++ b/projects/RabbitMQ.Client/client/api/ICredentialsProvider.cs @@ -29,25 +29,41 @@ // Copyright (c) 2007-2024 Broadcom. All Rights Reserved. //--------------------------------------------------------------------------- +using System; +using System.Threading; +using System.Threading.Tasks; + namespace RabbitMQ.Client { public interface ICredentialsProvider { string Name { get; } - string UserName { get; } - string Password { get; } + Task GetCredentialsAsync(CancellationToken cancellationToken = default); + } + + public class Credentials + { + private readonly string _name; + private readonly string _userName; + private readonly string _password; + private readonly TimeSpan? _validUntil; + + public Credentials(string name, string userName, string password, TimeSpan? validUntil) + { + _name = name; + _userName = userName; + _password = password; + _validUntil = validUntil; + } + + public string Name => _name; + public string UserName => _userName; + public string Password => _password; /// /// If credentials have an expiry time this property returns the interval. /// Otherwise, it returns null. /// - System.TimeSpan? ValidUntil { get; } - - /// - /// Before the credentials are available, be it Username, Password or ValidUntil, - /// the credentials must be obtained by calling this method. - /// And to keep it up to date, this method must be called before the ValidUntil interval. - /// - void Refresh(); + public TimeSpan? ValidUntil => _validUntil; } } diff --git a/projects/RabbitMQ.Client/client/api/ICredentialsRefresher.cs b/projects/RabbitMQ.Client/client/api/ICredentialsRefresher.cs deleted file mode 100644 index ea16d26b1f..0000000000 --- a/projects/RabbitMQ.Client/client/api/ICredentialsRefresher.cs +++ /dev/null @@ -1,156 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License 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. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. -//--------------------------------------------------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Tracing; -using System.Threading.Tasks; -namespace RabbitMQ.Client -{ - public interface ICredentialsRefresher - { - ICredentialsProvider Register(ICredentialsProvider provider, NotifyCredentialRefreshedAsync callback); - bool Unregister(ICredentialsProvider provider); - - delegate Task NotifyCredentialRefreshedAsync(bool successfully); - } - - [EventSource(Name = "TimerBasedCredentialRefresher")] - public class TimerBasedCredentialRefresherEventSource : EventSource - { - public static TimerBasedCredentialRefresherEventSource Log { get; } = new TimerBasedCredentialRefresherEventSource(); - - [Event(1)] - public void Registered(string name) => WriteEvent(1, "Registered", name); - [Event(2)] - public void Unregistered(string name) => WriteEvent(2, "UnRegistered", name); - [Event(3)] -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe")] -#endif - public void ScheduledTimer(string name, double interval) => WriteEvent(3, "ScheduledTimer", name, interval); - [Event(4)] - public void TriggeredTimer(string name) => WriteEvent(4, "TriggeredTimer", name); - [Event(5)] -#if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe")] -#endif - public void RefreshedCredentials(string name, bool succesfully) => WriteEvent(5, "RefreshedCredentials", name, succesfully); - [Event(6)] - public void AlreadyRegistered(string name) => WriteEvent(6, "AlreadyRegistered", name); - } - - public class TimerBasedCredentialRefresher : ICredentialsRefresher - { - private readonly ConcurrentDictionary _registrations = new ConcurrentDictionary(); - - public ICredentialsProvider Register(ICredentialsProvider provider, ICredentialsRefresher.NotifyCredentialRefreshedAsync callback) - { - if (!provider.ValidUntil.HasValue || provider.ValidUntil.Value.Equals(TimeSpan.Zero)) - { - return provider; - } - - if (_registrations.TryAdd(provider, scheduleTimer(provider, callback))) - { - TimerBasedCredentialRefresherEventSource.Log.Registered(provider.Name); - } - else - { - TimerBasedCredentialRefresherEventSource.Log.AlreadyRegistered(provider.Name); - } - - return provider; - } - - public bool Unregister(ICredentialsProvider provider) - { - if (_registrations.TryRemove(provider, out System.Timers.Timer? timer)) - { - try - { - TimerBasedCredentialRefresherEventSource.Log.Unregistered(provider.Name); - timer.Stop(); - } - finally - { - timer.Dispose(); - } - return true; - } - else - { - return false; - } - } - - private System.Timers.Timer scheduleTimer(ICredentialsProvider provider, ICredentialsRefresher.NotifyCredentialRefreshedAsync callback) - { - System.Timers.Timer timer = new System.Timers.Timer(); - timer.Interval = provider.ValidUntil!.Value.TotalMilliseconds * (1.0 - (1 / 3.0)); - timer.Elapsed += (o, e) => - { - TimerBasedCredentialRefresherEventSource.Log.TriggeredTimer(provider.Name); - try - { - provider.Refresh(); - scheduleTimer(provider, callback); - callback.Invoke(provider.Password != null); - TimerBasedCredentialRefresherEventSource.Log.RefreshedCredentials(provider.Name, true); - } - catch (Exception) - { - callback.Invoke(false); - TimerBasedCredentialRefresherEventSource.Log.RefreshedCredentials(provider.Name, false); - } - - }; - timer.Enabled = true; - timer.AutoReset = false; - TimerBasedCredentialRefresherEventSource.Log.ScheduledTimer(provider.Name, timer.Interval); - return timer; - } - } - - class NoOpCredentialsRefresher : ICredentialsRefresher - { - public ICredentialsProvider Register(ICredentialsProvider provider, ICredentialsRefresher.NotifyCredentialRefreshedAsync callback) - { - return provider; - } - - public bool Unregister(ICredentialsProvider provider) - { - return false; - } - } -} diff --git a/projects/RabbitMQ.Client/client/api/PlainMechanism.cs b/projects/RabbitMQ.Client/client/api/PlainMechanism.cs index ca2218f5d4..30f45b045c 100644 --- a/projects/RabbitMQ.Client/client/api/PlainMechanism.cs +++ b/projects/RabbitMQ.Client/client/api/PlainMechanism.cs @@ -30,14 +30,20 @@ //--------------------------------------------------------------------------- using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace RabbitMQ.Client { public class PlainMechanism : IAuthMechanism { - public byte[] handleChallenge(byte[]? challenge, ConnectionConfig config) + public async Task HandleChallengeAsync(byte[]? challenge, ConnectionConfig config, + CancellationToken cancellationToken = default) { - return Encoding.UTF8.GetBytes($"\0{config.CredentialsProvider.UserName}\0{config.CredentialsProvider.Password}"); + ICredentialsProvider credentialsProvider = config.CredentialsProvider; + Credentials credentials = await credentialsProvider.GetCredentialsAsync(cancellationToken) + .ConfigureAwait(false); + return Encoding.UTF8.GetBytes($"\0{credentials.UserName}\0{credentials.Password}"); } } } diff --git a/projects/RabbitMQ.Client/client/impl/Connection.Commands.cs b/projects/RabbitMQ.Client/client/impl/Connection.Commands.cs index adb1ee8cc9..2cdc180df2 100644 --- a/projects/RabbitMQ.Client/client/impl/Connection.Commands.cs +++ b/projects/RabbitMQ.Client/client/impl/Connection.Commands.cs @@ -133,7 +133,8 @@ await FinishCloseAsync(cancellationToken) byte[]? challenge = null; do { - byte[] response = mechanism.handleChallenge(challenge, _config); + byte[] response = await mechanism.HandleChallengeAsync(challenge, _config) + .ConfigureAwait(false); ConnectionSecureOrTune res; if (challenge is null) { @@ -183,31 +184,12 @@ await _channel0.ConnectionTuneOkAsync(channelMax, frameMax, (ushort)Heartbeat.To .ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - MaybeStartCredentialRefresher(); // now we can start heartbeat timers cancellationToken.ThrowIfCancellationRequested(); MaybeStartHeartbeatTimers(); } - private void MaybeStartCredentialRefresher() - { - if (_config.CredentialsProvider.ValidUntil != null) - { - _config.CredentialsRefresher.Register(_config.CredentialsProvider, NotifyCredentialRefreshedAsync); - } - } - - private async Task NotifyCredentialRefreshedAsync(bool succesfully) - { - if (succesfully) - { - using var cts = new CancellationTokenSource(InternalConstants.DefaultConnectionCloseTimeout); - await UpdateSecretAsync(_config.CredentialsProvider.Password, "Token refresh", cts.Token) - .ConfigureAwait(false); - } - } - private IAuthMechanismFactory GetAuthMechanismFactory(string supportedMechanismNames) { // Our list is in order of preference, the server one is not. diff --git a/projects/Test/Common/TestConnectionRecoveryBase.cs b/projects/Test/Common/TestConnectionRecoveryBase.cs index cf27d06d13..59ad8f11a5 100644 --- a/projects/Test/Common/TestConnectionRecoveryBase.cs +++ b/projects/Test/Common/TestConnectionRecoveryBase.cs @@ -40,8 +40,9 @@ namespace Test { - public class TestConnectionRecoveryBase : IntegrationFixture + public class TestConnectionRecoveryBase : IntegrationFixture, IDisposable { + private readonly Util _util; protected readonly byte[] _messageBody; protected const ushort TotalMessageCount = 16384; protected const ushort CloseAtCount = 16; @@ -49,9 +50,12 @@ public class TestConnectionRecoveryBase : IntegrationFixture public TestConnectionRecoveryBase(ITestOutputHelper output) : base(output) { + _util = new Util(output); _messageBody = GetRandomBody(4096); } + public void Dispose() => _util.Dispose(); + protected Task AssertConsumerCountAsync(string q, int count) { return WithTemporaryChannelAsync(async ch => @@ -166,7 +170,7 @@ internal async Task CreateAutorecoveringConnectionWith protected Task CloseConnectionAsync(IConnection conn) { - return Util.CloseConnectionAsync(conn); + return _util.CloseConnectionAsync(conn.ClientProvidedName); } protected Task CloseAndWaitForRecoveryAsync() diff --git a/projects/Test/Common/Util.cs b/projects/Test/Common/Util.cs index e9afa4edfd..db3382edde 100644 --- a/projects/Test/Common/Util.cs +++ b/projects/Test/Common/Util.cs @@ -4,45 +4,48 @@ using System.Linq; using System.Threading.Tasks; using EasyNetQ.Management.Client; -using RabbitMQ.Client; +using Xunit.Abstractions; namespace Test { - public static class Util + public class Util : IDisposable { - private static readonly ManagementClient s_managementClient; + private readonly ITestOutputHelper _output; + private readonly ManagementClient _managementClient; private static readonly bool s_isWindows = false; static Util() { - var managementUri = new Uri("http://localhost:15672"); - s_managementClient = new ManagementClient(managementUri, "guest", "guest"); s_isWindows = InitIsWindows(); } - public static string Now => DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture); - - public static bool IsWindows => s_isWindows; + public Util(ITestOutputHelper output) : this(output, "guest", "guest") + { + } - private static bool InitIsWindows() + public Util(ITestOutputHelper output, string managementUsername, string managementPassword) { - PlatformID platform = Environment.OSVersion.Platform; - if (platform == PlatformID.Win32NT) + _output = output; + + if (string.IsNullOrEmpty(managementUsername)) { - return true; + managementUsername = "guest"; } - string os = Environment.GetEnvironmentVariable("OS"); - if (os != null) + if (string.IsNullOrEmpty(managementPassword)) { - os = os.Trim(); - return os == "Windows_NT"; + throw new ArgumentNullException(nameof(managementPassword)); } - return false; + var managementUri = new Uri("http://localhost:15672"); + _managementClient = new ManagementClient(managementUri, managementUsername, managementPassword); } - public static async Task CloseConnectionAsync(IConnection conn) + public static string Now => DateTime.UtcNow.ToString("s", CultureInfo.InvariantCulture); + + public static bool IsWindows => s_isWindows; + + public async Task CloseConnectionAsync(string connectionClientProvidedName) { ushort tries = 1; EasyNetQ.Management.Client.Model.Connection connectionToClose = null; @@ -61,11 +64,11 @@ public static async Task CloseConnectionAsync(IConnection conn) await Task.Delay(TimeSpan.FromSeconds(delaySeconds)); - connections = await s_managementClient.GetConnectionsAsync(); + connections = await _managementClient.GetConnectionsAsync(); } while (connections.Count == 0); connectionToClose = connections.Where(c0 => - string.Equals((string)c0.ClientProperties["connection_name"], conn.ClientProvidedName, + string.Equals((string)c0.ClientProperties["connection_name"], connectionClientProvidedName, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); } catch (ArgumentNullException) @@ -79,7 +82,7 @@ public static async Task CloseConnectionAsync(IConnection conn) { try { - await s_managementClient.CloseConnectionAsync(connectionToClose); + await _managementClient.CloseConnectionAsync(connectionToClose); return; } catch (UnexpectedHttpStatusCodeException) @@ -91,8 +94,29 @@ public static async Task CloseConnectionAsync(IConnection conn) if (connectionToClose == null) { - throw new InvalidOperationException($"Could not delete connection: '{conn.ClientProvidedName}'"); + _output.WriteLine("{0} [WARNING] could not find/delete connection: '{1}'", + Now, connectionClientProvidedName); } } + + public void Dispose() => _managementClient.Dispose(); + + private static bool InitIsWindows() + { + PlatformID platform = Environment.OSVersion.Platform; + if (platform == PlatformID.Win32NT) + { + return true; + } + + string os = Environment.GetEnvironmentVariable("OS"); + if (os != null) + { + os = os.Trim(); + return os == "Windows_NT"; + } + + return false; + } } } diff --git a/projects/Test/Integration/TestQueueDeclare.cs b/projects/Test/Integration/TestQueueDeclare.cs index 5b4f8ae4d3..8ba26841b7 100644 --- a/projects/Test/Integration/TestQueueDeclare.cs +++ b/projects/Test/Integration/TestQueueDeclare.cs @@ -46,7 +46,7 @@ public TestQueueDeclare(ITestOutputHelper output) : base(output) } [Fact] - public async void TestQueueDeclareAsync() + public async Task TestQueueDeclareAsync() { string q = GenerateQueueName(); @@ -58,7 +58,7 @@ public async void TestQueueDeclareAsync() } [Fact] - public async void TestConcurrentQueueDeclareAndBindAsync() + public async Task TestConcurrentQueueDeclareAndBindAsync() { bool sawShutdown = false; diff --git a/projects/Test/OAuth2/MockOAuth2Client.cs b/projects/Test/OAuth2/MockOAuth2Client.cs new file mode 100644 index 0000000000..0daecca708 --- /dev/null +++ b/projects/Test/OAuth2/MockOAuth2Client.cs @@ -0,0 +1,103 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License 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. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client.OAuth2; +using Xunit.Abstractions; + +namespace OAuth2Test +{ + public class MockOAuth2Client : IOAuth2Client + { + private readonly ITestOutputHelper _testOutputHelper; + private IToken? _refreshToken; + private IToken? _requestToken; + + public MockOAuth2Client(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + public IToken? RefreshTokenValue + { + get { return _refreshToken; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _refreshToken = value; + } + } + + public IToken? RequestTokenValue + { + get { return _requestToken; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _requestToken = value; + } + } + + public Task RequestTokenAsync(CancellationToken cancellationToken = default) + { + if (_requestToken is null) + { + throw new NullReferenceException(); + } + + return Task.FromResult(_requestToken); + } + + public Task RefreshTokenAsync(IToken initialToken, CancellationToken cancellationToken = default) + { + Debug.Assert(Object.ReferenceEquals(_requestToken, initialToken)); + + if (_refreshToken is null) + { + throw new NullReferenceException(); + + } + return Task.FromResult(_refreshToken); + } + } + +} diff --git a/projects/Test/OAuth2/OAuth2.csproj b/projects/Test/OAuth2/OAuth2.csproj index d958da4c66..45699f5381 100644 --- a/projects/Test/OAuth2/OAuth2.csproj +++ b/projects/Test/OAuth2/OAuth2.csproj @@ -1,4 +1,4 @@ - + net6.0;net472 @@ -16,12 +16,14 @@ ../../rabbit.snk true true - 7.3 + 8.0 + enable + diff --git a/projects/Test/OAuth2/OAuth2Options.cs b/projects/Test/OAuth2/OAuth2Options.cs new file mode 100644 index 0000000000..10c173f14f --- /dev/null +++ b/projects/Test/OAuth2/OAuth2Options.cs @@ -0,0 +1,119 @@ +using System; + +namespace OAuth2Test +{ + public enum Mode + { + uaa, + keycloak + } + + public abstract class OAuth2OptionsBase + { + protected readonly Mode _mode; + + public OAuth2OptionsBase(Mode mode) + { + _mode = mode; + } + + public string Name + { + get + { + return _mode switch + { + Mode.uaa => "uaa", + Mode.keycloak => "keycloak", + _ => throw new InvalidOperationException(), + }; + } + } + + public string Scope + { + get + { + return _mode switch + { + Mode.uaa => string.Empty, + Mode.keycloak => "rabbitmq:configure:*/* rabbitmq:read:*/* rabbitmq:write:*/*", + _ => throw new InvalidOperationException(), + }; + } + } + + public string TokenEndpoint // => _mode switch + { + get + { + return _mode switch + { + Mode.uaa => "http://localhost:8080/oauth/token", + Mode.keycloak => "http://localhost:8080/realms/test/protocol/openid-connect/token", + _ => throw new InvalidOperationException(), + }; + } + } + + public abstract string ClientId { get; } + public abstract string ClientSecret { get; } + + public static int TokenExpiresInSeconds => 60; + } + + public class OAuth2ProducerOptions : OAuth2OptionsBase + { + public OAuth2ProducerOptions(Mode mode) : base(mode) + { + } + + public override string ClientId => "producer"; + + public override string ClientSecret + { + get + { + return _mode switch + { + Mode.uaa => "producer_secret", + Mode.keycloak => "kbOFBXI9tANgKUq8vXHLhT6YhbivgXxn", + _ => throw new InvalidOperationException(), + }; + } + } + } + + public class OAuth2HttpApiOptions : OAuth2OptionsBase + { + public OAuth2HttpApiOptions(Mode mode) : base(mode) + { + } + + public override string ClientId + { + get + { + return _mode switch + { + Mode.uaa => "mgt_api_client", + Mode.keycloak => "mgt_api_client", + _ => throw new InvalidOperationException(), + }; + } + } + + public override string ClientSecret + { + get + { + return _mode switch + { + Mode.uaa => "mgt_api_client", + Mode.keycloak => "LWOuYqJ8gjKg3D2U8CJZDuID3KiRZVDa", + _ => throw new InvalidOperationException(), + }; + } + } + } +} diff --git a/projects/Test/OAuth2/RequestFormMatcher.cs b/projects/Test/OAuth2/RequestFormMatcher.cs index 9d3e518069..ab6e47ba5c 100644 --- a/projects/Test/OAuth2/RequestFormMatcher.cs +++ b/projects/Test/OAuth2/RequestFormMatcher.cs @@ -31,13 +31,14 @@ using System; using System.Collections.Specialized; +using System.Web; using Xunit; namespace OAuth2Test { public class RequestFormMatcher { - private NameValueCollection _expected = new NameValueCollection(); + private readonly NameValueCollection _expected = new NameValueCollection(); public RequestFormMatcher WithParam(string key, string value) { @@ -45,12 +46,17 @@ public RequestFormMatcher WithParam(string key, string value) return this; } - public Func Matcher() + public Func Matcher() { return (body) => { - NameValueCollection actual = System.Web.HttpUtility.ParseQueryString(body); - Assert.Equal(actual.Count, _expected.Count); + if (body is null) + { + throw new ArgumentNullException(nameof(body)); + } + + NameValueCollection actual = HttpUtility.ParseQueryString(body); + Assert.Equal(_expected.Count, actual.Count); foreach (string k in actual.Keys) { Assert.NotNull(_expected[k]); diff --git a/projects/Test/OAuth2/TestCredentialsRefresher.cs b/projects/Test/OAuth2/TestCredentialsRefresher.cs new file mode 100644 index 0000000000..d5c46b8918 --- /dev/null +++ b/projects/Test/OAuth2/TestCredentialsRefresher.cs @@ -0,0 +1,162 @@ +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License 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. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +//--------------------------------------------------------------------------- + +using System; +using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; +using RabbitMQ.Client.OAuth2; +using Xunit; +using Xunit.Abstractions; + +namespace OAuth2Test +{ + public class MockCredentialsProvider : ICredentialsProvider + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly TimeSpan _validUntil; + private readonly Exception? _maybeGetCredentialsException; + + public MockCredentialsProvider(ITestOutputHelper testOutputHelper, TimeSpan validUntil, + Exception? maybeGetCredentialsException = null) + { + _testOutputHelper = testOutputHelper; + _validUntil = validUntil; + _maybeGetCredentialsException = maybeGetCredentialsException; + } + + public string Name => GetType().Name; + + public Task GetCredentialsAsync(CancellationToken cancellationToken = default) + { + if (_maybeGetCredentialsException is null) + { + var creds = new Credentials(this.GetType().Name, "guest", "guest", _validUntil); + return Task.FromResult(creds); + } + else + { + throw _maybeGetCredentialsException; + } + } + } + + public class TestCredentialsRefresher + { + private readonly ITestOutputHelper _testOutputHelper; + + public TestCredentialsRefresher(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + [Fact] + public async Task TestRefreshToken() + { + var expectedValidUntil = TimeSpan.FromSeconds(1); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var credentialsProvider = new MockCredentialsProvider(_testOutputHelper, expectedValidUntil); + + Task cb(Credentials? argCreds, Exception? ex, CancellationToken argToken) + { + if (argCreds is null) + { + tcs.SetException(new NullReferenceException("argCreds is null, huh?")); + } + else + { + tcs.SetResult(argCreds); + } + + return Task.CompletedTask; + } + + Credentials credentials; + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + { + using (CancellationTokenRegistration ctr = cts.Token.Register(() => tcs.TrySetCanceled())) + { + var credentialRefresher = new CredentialsRefresher(credentialsProvider, cb, cts.Token); + credentials = await tcs.Task; + } + } + + Assert.Equal(nameof(MockCredentialsProvider), credentials.Name); + Assert.Equal("guest", credentials.UserName); + Assert.Equal("guest", credentials.Password); + Assert.Equal(expectedValidUntil, credentials.ValidUntil); + } + + [Fact] + public async Task TestRefreshTokenFailed() + { + string exceptionMessage = nameof(TestCredentialsRefresher); + var expectedException = new Exception(exceptionMessage); + + var expectedValidUntil = TimeSpan.FromSeconds(1); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var credentialsProvider = new MockCredentialsProvider(_testOutputHelper, expectedValidUntil, expectedException); + + ushort callbackCount = 0; + Task cb(Credentials? argCreds, Exception? ex, CancellationToken argToken) + { + callbackCount++; + + if (ex != null) + { + tcs.SetException(ex); + } + + return Task.CompletedTask; + } + + Credentials? credentials = null; + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + { + using (CancellationTokenRegistration ctr = cts.Token.Register(() => tcs.TrySetCanceled())) + { + var credentialRefresher = new CredentialsRefresher(credentialsProvider, cb, cts.Token); + try + { + credentials = await tcs.Task; + } + catch (Exception ex) + { + Assert.Same(expectedException, ex); + } + } + } + + Assert.Null(credentials); + Assert.Equal(1, callbackCount); + } + } +} diff --git a/projects/Test/OAuth2/TestOAuth2.cs b/projects/Test/OAuth2/TestOAuth2.cs index f42abeb718..9f87f90b1b 100644 --- a/projects/Test/OAuth2/TestOAuth2.cs +++ b/projects/Test/OAuth2/TestOAuth2.cs @@ -1,99 +1,47 @@ -using System; +// This source code is dual-licensed under the Apache License, version +// 2.0, and the Mozilla Public License, version 2.0. +// +// The APL v2.0: +// +//--------------------------------------------------------------------------- +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License 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. +//--------------------------------------------------------------------------- +// +// The MPL v2.0: +// +//--------------------------------------------------------------------------- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. +//--------------------------------------------------------------------------- + +using System; using System.Text; using System.Threading; using System.Threading.Tasks; using RabbitMQ.Client; using RabbitMQ.Client.Events; using RabbitMQ.Client.OAuth2; +using Test; using Xunit; using Xunit.Abstractions; namespace OAuth2Test { - public enum Mode - { - uaa, - keycloak - } - - public class OAuth2Options - { - private readonly Mode _mode; - - public OAuth2Options(Mode mode) - { - _mode = mode; - } - - public string Name - { - get - { - switch (_mode) - { - case Mode.uaa: - return "uaa"; - case Mode.keycloak: - return "keycloak"; - default: - throw new InvalidOperationException(); - } - } - } - - public string ClientId => "producer"; - - public string ClientSecret - { - get - { - switch (_mode) - { - case Mode.uaa: - return "producer_secret"; - case Mode.keycloak: - return "kbOFBXI9tANgKUq8vXHLhT6YhbivgXxn"; - default: - throw new InvalidOperationException(); - } - } - } - - public string Scope - { - get - { - switch (_mode) - { - case Mode.uaa: - return string.Empty; - case Mode.keycloak: - return "rabbitmq:configure:*/* rabbitmq:read:*/* rabbitmq:write:*/*"; - default: - throw new InvalidOperationException(); - } - } - } - - public string TokenEndpoint // => _mode switch - { - get - { - switch (_mode) - { - case Mode.uaa: - return "http://localhost:8080/oauth/token"; - case Mode.keycloak: - return "http://localhost:8080/realms/test/protocol/openid-connect/token"; - default: - throw new InvalidOperationException(); - } - } - } - - public int TokenExpiresInSeconds => 60; - } - public class TestOAuth2 : IAsyncLifetime { private const string Exchange = "test_direct"; @@ -101,8 +49,13 @@ public class TestOAuth2 : IAsyncLifetime private readonly SemaphoreSlim _doneEvent = new SemaphoreSlim(0, 1); private readonly ITestOutputHelper _testOutputHelper; private readonly IConnectionFactory _connectionFactory; - private IConnection _connection; private readonly int _tokenExpiresInSeconds; + private readonly OAuth2ClientCredentialsProvider _producerCredentialsProvider; + private readonly OAuth2ClientCredentialsProvider _httpApiCredentialsProvider; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + private IConnection? _connection; + private CredentialsRefresher? _credentialsRefresher; public TestOAuth2(ITestOutputHelper testOutputHelper) { @@ -110,43 +63,100 @@ public TestOAuth2(ITestOutputHelper testOutputHelper) string modeStr = Environment.GetEnvironmentVariable("OAUTH2_MODE") ?? "uaa"; Mode mode = (Mode)Enum.Parse(typeof(Mode), modeStr.ToLowerInvariant()); - var options = new OAuth2Options(mode); + + var producerOptions = new OAuth2ProducerOptions(mode); + _producerCredentialsProvider = GetCredentialsProvider(producerOptions); + + var httpApiOptions = new OAuth2HttpApiOptions(mode); + _httpApiCredentialsProvider = GetCredentialsProvider(httpApiOptions); _connectionFactory = new ConnectionFactory { AutomaticRecoveryEnabled = true, - CredentialsProvider = GetCredentialsProvider(options), - CredentialsRefresher = GetCredentialsRefresher(), + CredentialsProvider = _producerCredentialsProvider, ClientProvidedName = nameof(TestOAuth2) }; - _tokenExpiresInSeconds = options.TokenExpiresInSeconds; + _tokenExpiresInSeconds = OAuth2OptionsBase.TokenExpiresInSeconds; } public async Task InitializeAsync() { - _connection = await _connectionFactory.CreateConnectionAsync(CancellationToken.None); + _connection = await _connectionFactory.CreateConnectionAsync(_cancellationTokenSource.Token); + + _connection.ConnectionShutdown += (sender, ea) => + { + _testOutputHelper.WriteLine("{0} [WARNING] connection shutdown!", DateTime.Now); + }; + + _connection.ConnectionRecoveryError += (sender, ea) => + { + _testOutputHelper.WriteLine("{0} [ERROR] connection recovery error: {1}", + DateTime.Now, ea.Exception); + }; + + _connection.RecoverySucceeded += (sender, ea) => + { + _testOutputHelper.WriteLine("{0} [INFO] connection recovery succeeded", DateTime.Now); + }; + + _credentialsRefresher = new CredentialsRefresher(_producerCredentialsProvider, + OnCredentialsRefreshedAsync, + _cancellationTokenSource.Token); } public async Task DisposeAsync() { try { - await _connection.CloseAsync(); + _cancellationTokenSource.Cancel(); + + if (_connection != null) + { + await _connection.CloseAsync(); + } } finally { _doneEvent.Dispose(); - _connection.Dispose(); + _producerCredentialsProvider.Dispose(); + _connection?.Dispose(); + } + } + + private Task OnCredentialsRefreshedAsync(Credentials? credentials, Exception? exception, + CancellationToken cancellationToken = default) + { + if (_connection is null) + { + _testOutputHelper.WriteLine("connection is unexpectedly null!"); + Assert.Fail("_connection is unexpectedly null!"); + } + + if (exception != null) + { + _testOutputHelper.WriteLine("exception is unexpectedly not-null: {0}", exception); + Assert.Fail($"exception is unexpectedly not-null: {exception}"); } + + if (credentials is null) + { + _testOutputHelper.WriteLine("credentials arg is unexpectedly null!"); + Assert.Fail("credentials arg is unexpectedly null!"); + } + + return _connection.UpdateSecretAsync(credentials.Password, "Token refresh", cancellationToken); } [Fact] - public async void IntegrationTest() + public async Task IntegrationTest() { - using (IChannel publishChannel = await DeclarePublisherAsync()) + Util? closeConnectionUtil = null; + Task? closeConnectionTask = null; + + using (IChannel publishChannel = await DeclarePublishChannelAsync()) { - using (IChannel consumeChannel = await DeclareConsumerAsync()) + using (IChannel consumeChannel = await DeclareConsumeChannelAsync()) { await PublishAsync(publishChannel); await ConsumeAsync(consumeChannel); @@ -155,10 +165,38 @@ public async void IntegrationTest() { for (int i = 0; i < 4; i++) { - _testOutputHelper.WriteLine("Wait until Token expires. Attempt #" + (i + 1)); - - await Task.Delay(TimeSpan.FromSeconds(_tokenExpiresInSeconds + 10)); - _testOutputHelper.WriteLine("Resuming .."); + var delaySpan = TimeSpan.FromSeconds(_tokenExpiresInSeconds + 10); + _testOutputHelper.WriteLine("{0} [INFO] wait '{1}' until Token expires. Attempt #{1}", + DateTime.Now, delaySpan, (i + 1)); + + if (i == 1) + { + async Task CloseConnection() + { + Assert.NotNull(_connection); + Credentials httpApiCredentials = await _httpApiCredentialsProvider.GetCredentialsAsync(); + closeConnectionUtil = new Util(_testOutputHelper, "mgt_api_client", httpApiCredentials.Password); + await closeConnectionUtil.CloseConnectionAsync(_connection.ClientProvidedName); + } + + closeConnectionTask = Task.Run(CloseConnection); + } + + await Task.Delay(delaySpan); + + if (closeConnectionTask != null) + { + await closeConnectionTask; + closeConnectionTask = null; + + if (closeConnectionUtil != null) + { + closeConnectionUtil.Dispose(); + closeConnectionUtil = null; + } + } + + _testOutputHelper.WriteLine("{0} [INFO] Resuming ...", DateTime.Now); await PublishAsync(publishChannel); await ConsumeAsync(consumeChannel); @@ -177,22 +215,23 @@ public async void IntegrationTest() } [Fact] - public async void SecondConnectionCrashes_GH1429() + public async Task SecondConnectionCrashes_GH1429() { // https://github.com/rabbitmq/rabbitmq-dotnet-client/issues/1429 IConnection secondConnection = await _connectionFactory.CreateConnectionAsync(CancellationToken.None); secondConnection.Dispose(); } - private async Task DeclarePublisherAsync() + private async Task DeclarePublishChannelAsync() { - IChannel publisher = await _connection.CreateChannelAsync(); - await publisher.ConfirmSelectAsync(); - await publisher.ExchangeDeclareAsync("test_direct", ExchangeType.Direct, true, false); - return publisher; + Assert.NotNull(_connection); + IChannel publishChannel = await _connection.CreateChannelAsync(); + await publishChannel.ConfirmSelectAsync(); + await publishChannel.ExchangeDeclareAsync("test_direct", ExchangeType.Direct, true, false); + return publishChannel; } - private async Task PublishAsync(IChannel publisher) + private async Task PublishAsync(IChannel publishChannel) { const string message = "Hello World!"; @@ -202,32 +241,33 @@ private async Task PublishAsync(IChannel publisher) AppId = "oauth2", }; - await publisher.BasicPublishAsync(exchange: Exchange, routingKey: "hello", basicProperties: properties, body: body); + await publishChannel.BasicPublishAsync(exchange: Exchange, routingKey: "hello", basicProperties: properties, body: body); _testOutputHelper.WriteLine("Sent message"); - await publisher.WaitForConfirmsOrDieAsync(); + await publishChannel.WaitForConfirmsOrDieAsync(); _testOutputHelper.WriteLine("Confirmed Sent message"); } - private async ValueTask DeclareConsumerAsync() + private async ValueTask DeclareConsumeChannelAsync() { - IChannel subscriber = await _connection.CreateChannelAsync(); - await subscriber.QueueDeclareAsync(queue: "testqueue", true, false, false); - await subscriber.QueueBindAsync("testqueue", Exchange, "hello"); - return subscriber; + Assert.NotNull(_connection); + IChannel consumeChannel = await _connection.CreateChannelAsync(); + await consumeChannel.QueueDeclareAsync(queue: "testqueue", true, false, false); + await consumeChannel.QueueBindAsync("testqueue", Exchange, "hello"); + return consumeChannel; } - private async Task ConsumeAsync(IChannel subscriber) + private async Task ConsumeAsync(IChannel consumeChannel) { - var asyncListener = new AsyncEventingBasicConsumer(subscriber); + var asyncListener = new AsyncEventingBasicConsumer(consumeChannel); asyncListener.Received += AsyncListener_Received; - string consumerTag = await subscriber.BasicConsumeAsync("testqueue", true, "testconsumer", asyncListener); - await _doneEvent.WaitAsync(TimeSpan.FromMilliseconds(500)); + string consumerTag = await consumeChannel.BasicConsumeAsync("testqueue", true, "testconsumer", asyncListener); + await _doneEvent.WaitAsync(TimeSpan.FromSeconds(5)); _testOutputHelper.WriteLine("Received message"); - await subscriber.BasicCancelAsync(consumerTag); + await consumeChannel.BasicCancelAsync(consumerTag); } - private OAuth2ClientCredentialsProvider GetCredentialsProvider(OAuth2Options opts) + private OAuth2ClientCredentialsProvider GetCredentialsProvider(OAuth2OptionsBase opts) { _testOutputHelper.WriteLine("OAuth2Client "); _testOutputHelper.WriteLine($"- ClientId: {opts.ClientId}"); @@ -245,10 +285,5 @@ private Task AsyncListener_Received(object sender, BasicDeliverEventArgs @event) _doneEvent.Release(); return Task.CompletedTask; } - - private static ICredentialsRefresher GetCredentialsRefresher() - { - return new TimerBasedCredentialRefresher(); - } } } diff --git a/projects/Test/OAuth2/TestOAuth2Client.cs b/projects/Test/OAuth2/TestOAuth2Client.cs index 3633b6fab1..6b6e50ced9 100644 --- a/projects/Test/OAuth2/TestOAuth2Client.cs +++ b/projects/Test/OAuth2/TestOAuth2Client.cs @@ -32,6 +32,7 @@ using System; using System.Net.Http; using System.Text.Json; +using System.Threading.Tasks; using RabbitMQ.Client.OAuth2; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; @@ -51,96 +52,63 @@ public class TestOAuth2Client public TestOAuth2Client() { _oauthServer = WireMockServer.Start(); - - _client = new OAuth2ClientBuilder(_client_id, _client_secret, new System.Uri(_oauthServer.Url + "/token")).Build(); - } - - private void expectTokenRequest(RequestFormMatcher expectedRequestBody, JsonToken expectedResponse) - { - _oauthServer - .Given( - Request.Create() - .WithPath("/token") - .WithBody(expectedRequestBody.Matcher()) - .UsingPost() - ) - .RespondWith( - Response.Create() - .WithStatusCode(200) - .WithHeader("Content-Type", "application/json;charset=UTF-8") - .WithBody(JsonSerializer.Serialize(expectedResponse)) - ); + var uri = new Uri(_oauthServer.Url + "/token"); + _client = new OAuth2ClientBuilder(_client_id, _client_secret, uri).Build(); } [Fact] - public void TestRequestToken() + public async Task TestRequestToken() { JsonToken expectedJsonToken = new JsonToken("the_access_token", "the_refresh_token", TimeSpan.FromSeconds(10)); - expectTokenRequest(new RequestFormMatcher() + ExpectTokenRequest(new RequestFormMatcher() .WithParam("client_id", _client_id) .WithParam("client_secret", _client_secret) .WithParam("grant_type", "client_credentials"), expectedJsonToken); - IToken token = _client.RequestToken(); + IToken token = await _client.RequestTokenAsync(); Assert.NotNull(token); - Assert.Equal(expectedJsonToken.access_token, token.AccessToken); - Assert.Equal(expectedJsonToken.refresh_token, token.RefreshToken); - Assert.Equal(TimeSpan.FromSeconds(expectedJsonToken.expires_in), token.ExpiresIn); - } - - private void expectTokenRefresh(JsonToken expectedResponse) - { - _oauthServer - .Given( - Request.Create() - .WithPath("/token") - .WithParam("client_id", _client_id) - .WithParam("client_secret", _client_secret) - .WithParam("grant_type", "refresh_token") - .WithParam("refresh_token", expectedResponse.refresh_token) - .WithHeader("content_type", "application/x-www-form-urlencoded") - .UsingPost() - ) - .RespondWith( - Response.Create() - .WithStatusCode(200) - .WithHeader("Content-Type", "application/json;charset=UTF-8") - .WithBody(JsonSerializer.Serialize(expectedResponse)) - ); + Assert.Equal(expectedJsonToken.AccessToken, token.AccessToken); + Assert.Equal(expectedJsonToken.RefreshToken, token.RefreshToken); + Assert.Equal(TimeSpan.FromSeconds(expectedJsonToken.ExpiresIn), token.ExpiresIn); } [Fact] - public void TestRefreshToken() + public async Task TestRefreshToken() { - JsonToken expectedJsonToken = new JsonToken("the_access_token", "the_refresh_token", TimeSpan.FromSeconds(10)); - expectTokenRequest(new RequestFormMatcher() + const string accessToken0 = "the_access_token"; + const string accessToken1 = "the_access_token_2"; + const string refreshToken = "the_refresh_token"; + + JsonToken expectedJsonToken = new JsonToken(accessToken0, refreshToken, TimeSpan.FromSeconds(10)); + + ExpectTokenRequest(new RequestFormMatcher() .WithParam("client_id", _client_id) .WithParam("client_secret", _client_secret) .WithParam("grant_type", "client_credentials"), expectedJsonToken); - IToken token = _client.RequestToken(); + IToken token = await _client.RequestTokenAsync(); _oauthServer.Reset(); - expectedJsonToken = new JsonToken("the_access_token2", "the_refresh_token", TimeSpan.FromSeconds(20)); - expectTokenRequest(new RequestFormMatcher() + JsonToken responseJsonToken = new JsonToken(accessToken1, refreshToken, TimeSpan.FromSeconds(20)); + ExpectTokenRefresh(new RequestFormMatcher() .WithParam("client_id", _client_id) .WithParam("client_secret", _client_secret) .WithParam("grant_type", "refresh_token") - .WithParam("refresh_token", "the_refresh_token"), - expectedJsonToken); + .WithParam("refresh_token", refreshToken), + responseJsonToken); - IToken refreshedToken = _client.RefreshToken(token); - Assert.False(refreshedToken == token); + IToken refreshedToken = await _client.RefreshTokenAsync(token); Assert.NotNull(refreshedToken); - Assert.Equal(expectedJsonToken.access_token, refreshedToken.AccessToken); - Assert.Equal(expectedJsonToken.refresh_token, refreshedToken.RefreshToken); - Assert.Equal(TimeSpan.FromSeconds(expectedJsonToken.expires_in), refreshedToken.ExpiresIn); + Assert.False(Object.ReferenceEquals(refreshedToken, token)); + Assert.Equal(responseJsonToken.AccessToken, refreshedToken.AccessToken); + Assert.Equal(responseJsonToken.RefreshToken, refreshedToken.RefreshToken); + Assert.Equal(TimeSpan.FromSeconds(responseJsonToken.ExpiresIn), refreshedToken.ExpiresIn); } [Fact] - public void TestInvalidCredentials() + public async Task TestInvalidCredentials() { _oauthServer .Given( @@ -159,10 +127,46 @@ public void TestInvalidCredentials() try { - IToken token = _client.RequestToken(); + IToken token = await _client.RequestTokenAsync(); Assert.Fail("Should have thrown Exception"); } - catch (HttpRequestException) { } + catch (HttpRequestException) + { + } + } + + private void ExpectTokenRequest(RequestFormMatcher expectedRequestBody, JsonToken response) + { + _oauthServer + .Given( + Request.Create() + .WithPath("/token") + .WithBody(expectedRequestBody.Matcher()) + .UsingPost() + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "application/json;charset=UTF-8") + .WithBody(JsonSerializer.Serialize(response)) + ); + } + + private void ExpectTokenRefresh(RequestFormMatcher expectedRequestBody, JsonToken expectedResponse) + { + _oauthServer + .Given( + Request.Create() + .WithPath("/token") + .WithBody(expectedRequestBody.Matcher()) + .UsingPost() + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "application/json;charset=UTF-8") + .WithBody(JsonSerializer.Serialize(expectedResponse)) + ); } } } diff --git a/projects/Test/OAuth2/TestOAuth2ClientCredentialsProvider.cs b/projects/Test/OAuth2/TestOAuth2ClientCredentialsProvider.cs index 6c2604f960..98ac2acf7f 100644 --- a/projects/Test/OAuth2/TestOAuth2ClientCredentialsProvider.cs +++ b/projects/Test/OAuth2/TestOAuth2ClientCredentialsProvider.cs @@ -30,65 +30,14 @@ //--------------------------------------------------------------------------- using System; -using System.Diagnostics; -using System.Threading; +using System.Threading.Tasks; +using RabbitMQ.Client; using RabbitMQ.Client.OAuth2; using Xunit; using Xunit.Abstractions; namespace OAuth2Test { - public class MockIOAuth2Client : IOAuth2Client - { - private readonly ITestOutputHelper _testOutputHelper; - private IToken _refreshToken; - private IToken _requestToken; - - public MockIOAuth2Client(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - - public IToken RefreshTokenValue - { - get { return _refreshToken; } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _refreshToken = value; - } - } - - public IToken RequestTokenValue - { - get { return _requestToken; } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _requestToken = value; - } - } - - public IToken RefreshToken(IToken initialToken) - { - Debug.Assert(Object.ReferenceEquals(_requestToken, initialToken)); - return _refreshToken; - } - - public IToken RequestToken() - { - return _requestToken; - } - } - public class TestOAuth2CredentialsProvider { private readonly ITestOutputHelper _testOutputHelper; @@ -102,42 +51,45 @@ public TestOAuth2CredentialsProvider(ITestOutputHelper testOutputHelper) public void ShouldHaveAName() { const string name = "aName"; - IOAuth2Client oAuth2Client = new MockIOAuth2Client(_testOutputHelper); + IOAuth2Client oAuth2Client = new MockOAuth2Client(_testOutputHelper); var provider = new OAuth2ClientCredentialsProvider(name, oAuth2Client); - Assert.Equal(name, provider.Name); } [Fact] - public void ShouldRequestTokenWhenAskToRefresh() + public async Task ShouldRequestTokenWhenAskToRefresh() { const string newTokenValue = "the_access_token"; IToken newToken = NewToken(newTokenValue, TimeSpan.FromSeconds(60)); - var oAuth2Client = new MockIOAuth2Client(_testOutputHelper); + var oAuth2Client = new MockOAuth2Client(_testOutputHelper); oAuth2Client.RequestTokenValue = newToken; var provider = new OAuth2ClientCredentialsProvider(nameof(ShouldRequestTokenWhenAskToRefresh), oAuth2Client); - provider.Refresh(); + Credentials credentials = await provider.GetCredentialsAsync(); - Assert.Equal(newTokenValue, provider.Password); + Assert.Equal(newTokenValue, credentials.Password); } [Fact] - public void ShouldRequestTokenWhenGettingPasswordOrValidUntilForFirstTimeAccess() + public async Task ShouldRequestTokenWhenGettingPasswordOrValidUntilForFirstTimeAccess() { const string accessToken = "the_access_token"; const string refreshToken = "the_refresh_token"; IToken firstToken = NewToken(accessToken, refreshToken, TimeSpan.FromSeconds(1)); - var oAuth2Client = new MockIOAuth2Client(_testOutputHelper); + + var oAuth2Client = new MockOAuth2Client(_testOutputHelper); oAuth2Client.RequestTokenValue = firstToken; var provider = new OAuth2ClientCredentialsProvider(nameof(ShouldRequestTokenWhenGettingPasswordOrValidUntilForFirstTimeAccess), oAuth2Client); - Assert.Equal(firstToken.AccessToken, provider.Password); - Assert.Equal(firstToken.ExpiresIn, provider.ValidUntil.Value); + Credentials credentials = await provider.GetCredentialsAsync(); + + Assert.Equal(firstToken.AccessToken, credentials.Password); + Assert.NotNull(credentials.ValidUntil); + Assert.Equal(firstToken.ExpiresIn, credentials.ValidUntil.Value); } [Fact] - public void ShouldRefreshTokenUsingRefreshTokenWhenAvailable() + public async Task ShouldRefreshTokenUsingRefreshTokenWhenAvailable() { const string accessToken = "the_access_token"; const string refreshToken = "the_refresh_token"; @@ -146,44 +98,62 @@ public void ShouldRefreshTokenUsingRefreshTokenWhenAvailable() IToken firstToken = NewToken(accessToken, refreshToken, TimeSpan.FromSeconds(1)); IToken refreshedToken = NewToken(accessToken2, refreshToken2, TimeSpan.FromSeconds(60)); - var oAuth2Client = new MockIOAuth2Client(_testOutputHelper); + + var oAuth2Client = new MockOAuth2Client(_testOutputHelper); oAuth2Client.RequestTokenValue = firstToken; - var provider = new OAuth2ClientCredentialsProvider(nameof(ShouldRequestTokenWhenGettingPasswordOrValidUntilForFirstTimeAccess), oAuth2Client); - provider.Refresh(); + var provider = new OAuth2ClientCredentialsProvider(nameof(ShouldRefreshTokenUsingRefreshTokenWhenAvailable), oAuth2Client); + + Credentials credentials = await provider.GetCredentialsAsync(); - Assert.Equal(firstToken.AccessToken, provider.Password); - Assert.Equal(firstToken.ExpiresIn, provider.ValidUntil.Value); + Assert.Equal(firstToken.AccessToken, credentials.Password); + Assert.NotNull(credentials.ValidUntil); + Assert.Equal(firstToken.ExpiresIn, credentials.ValidUntil.Value); oAuth2Client.RefreshTokenValue = refreshedToken; - provider.Refresh(); - Assert.Equal(refreshedToken.AccessToken, provider.Password); - Assert.Equal(refreshedToken.ExpiresIn, provider.ValidUntil.Value); + while (false == firstToken.HasExpired) + { + await Task.Delay(100); + } + + credentials = await provider.GetCredentialsAsync(); + + Assert.Equal(refreshedToken.AccessToken, credentials.Password); + Assert.NotNull(credentials.ValidUntil); + Assert.Equal(refreshedToken.ExpiresIn, credentials.ValidUntil.Value); } [Fact] - public void ShouldRequestTokenWhenRefreshTokenNotAvailable() + public async Task ShouldRequestTokenWhenRefreshTokenNotAvailable() { const string accessToken = "the_access_token"; const string accessToken2 = "the_access_token_2"; - IToken firstToken = NewToken(accessToken, null, TimeSpan.FromSeconds(1)); - IToken secondToken = NewToken(accessToken2, null, TimeSpan.FromSeconds(60)); + IToken firstToken = NewToken(accessToken, string.Empty, TimeSpan.FromSeconds(1)); + IToken secondToken = NewToken(accessToken2, string.Empty, TimeSpan.FromSeconds(60)); - var oAuth2Client = new MockIOAuth2Client(_testOutputHelper); + var oAuth2Client = new MockOAuth2Client(_testOutputHelper); oAuth2Client.RequestTokenValue = firstToken; var provider = new OAuth2ClientCredentialsProvider(nameof(ShouldRequestTokenWhenRefreshTokenNotAvailable), oAuth2Client); - provider.Refresh(); + Credentials credentials = await provider.GetCredentialsAsync(); - Assert.Equal(firstToken.AccessToken, provider.Password); - Assert.Equal(firstToken.ExpiresIn, provider.ValidUntil.Value); + Assert.Equal(firstToken.AccessToken, credentials.Password); + Assert.NotNull(credentials.ValidUntil); + Assert.Equal(firstToken.ExpiresIn, credentials.ValidUntil.Value); oAuth2Client.RequestTokenValue = secondToken; - provider.Refresh(); - Assert.Equal(secondToken.AccessToken, provider.Password); - Assert.Equal(secondToken.ExpiresIn, provider.ValidUntil.Value); + while (false == firstToken.HasExpired) + { + await Task.Delay(100); + } + + credentials = await provider.GetCredentialsAsync(); + + Assert.Equal(secondToken.AccessToken, credentials.Password); + Assert.NotNull(credentials.ValidUntil); + Assert.Equal(secondToken.ExpiresIn, credentials.ValidUntil.Value); } private static Token NewToken(string access_token, TimeSpan expiresIn) diff --git a/projects/Test/OAuth2/keycloak/import/test-realm.json b/projects/Test/OAuth2/keycloak/import/test-realm.json index df2b7594f8..d34986eaaa 100644 --- a/projects/Test/OAuth2/keycloak/import/test-realm.json +++ b/projects/Test/OAuth2/keycloak/import/test-realm.json @@ -508,7 +508,7 @@ "credentials" : [ ], "disableableCredentialTypes" : [ ], "requiredActions" : [ ], - "realmRoles" : [ "default-roles-test", "rabbitmq-management" ], + "realmRoles" : [ "default-roles-test", "rabbitmq-management", "rabbitmq.tag:administrator" ], "notBefore" : 0, "groups" : [ ] }, { diff --git a/projects/Test/OAuth2/keycloak/rabbitmq.conf b/projects/Test/OAuth2/keycloak/rabbitmq.conf index 207106a14f..ed2a692c3b 100644 --- a/projects/Test/OAuth2/keycloak/rabbitmq.conf +++ b/projects/Test/OAuth2/keycloak/rabbitmq.conf @@ -1,4 +1,7 @@ -auth_backends.1 = rabbit_auth_backend_oauth2 +log.console = true +log.console.level = debug + +auth_backends.1 = oauth2 auth_oauth2.resource_server_id = rabbitmq auth_oauth2.preferred_username_claims.1 = username diff --git a/projects/Test/OAuth2/uaa/rabbitmq.conf b/projects/Test/OAuth2/uaa/rabbitmq.conf index b0a6d5b0f4..5edca13cc7 100644 --- a/projects/Test/OAuth2/uaa/rabbitmq.conf +++ b/projects/Test/OAuth2/uaa/rabbitmq.conf @@ -1,4 +1,8 @@ -auth_backends.1 = rabbit_auth_backend_oauth2 +log.console = true +log.console.level = debug + +auth_backends.1 = oauth2 + management.oauth_enabled = true management.oauth_client_id = rabbit_client_code management.oauth_provider_url = http://localhost:8080/ diff --git a/projects/Test/OAuth2/uaa/uaa.yml b/projects/Test/OAuth2/uaa/uaa.yml index 8772706f74..c3efb7633c 100644 --- a/projects/Test/OAuth2/uaa/uaa.yml +++ b/projects/Test/OAuth2/uaa/uaa.yml @@ -125,7 +125,7 @@ oauth: id: mgt_api_client secret: mgt_api_client authorized-grant-types: client_credentials - authorities: rabbitmq.tag:monitoring + authorities: rabbitmq.tag:administrator rabbit_client_code: id: rabbit_client_code secret: rabbit_client_code diff --git a/projects/Test/Unit/TestTimerBasedCredentialRefresher.cs b/projects/Test/Unit/TestTimerBasedCredentialRefresher.cs deleted file mode 100644 index faed6f03f7..0000000000 --- a/projects/Test/Unit/TestTimerBasedCredentialRefresher.cs +++ /dev/null @@ -1,181 +0,0 @@ -// This source code is dual-licensed under the Apache License, version -// 2.0, and the Mozilla Public License, version 2.0. -// -// The APL v2.0: -// -//--------------------------------------------------------------------------- -// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License 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. -//--------------------------------------------------------------------------- -// -// The MPL v2.0: -// -//--------------------------------------------------------------------------- -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2007-2024 Broadcom. All Rights Reserved. -//--------------------------------------------------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using RabbitMQ.Client; -using Xunit; -using Xunit.Abstractions; - -namespace Test.Unit -{ - public class MockCredentialsProvider : ICredentialsProvider - { - private readonly ITestOutputHelper _testOutputHelper; - private readonly TimeSpan? _validUntil = TimeSpan.FromSeconds(1); - private Exception _ex = null; - private bool _refreshCalled = false; - - public MockCredentialsProvider(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - - public MockCredentialsProvider(ITestOutputHelper testOutputHelper, TimeSpan validUntil) - { - _testOutputHelper = testOutputHelper; - _validUntil = validUntil; - } - - public bool RefreshCalled - { - get - { - return _refreshCalled; - } - } - - public string Name => this.GetType().Name; - - public string UserName => "guest"; - - public string Password - { - get - { - if (_ex == null) - { - return "guest"; - } - else - { - throw _ex; - } - } - } - - public TimeSpan? ValidUntil => _validUntil; - - public void Refresh() - { - _refreshCalled = true; - } - - public void PasswordThrows(Exception ex) - { - _ex = ex; - } - } - - public class TestTimerBasedCredentialsRefresher - { - private readonly ITestOutputHelper _testOutputHelper; - private readonly TimerBasedCredentialRefresher _refresher = new TimerBasedCredentialRefresher(); - - public TestTimerBasedCredentialsRefresher(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - - [Fact] - public void TestRegister() - { - Task cb(bool unused) => Task.CompletedTask; - ICredentialsProvider credentialsProvider = new MockCredentialsProvider(_testOutputHelper); - - Assert.True(credentialsProvider == _refresher.Register(credentialsProvider, cb)); - Assert.True(_refresher.Unregister(credentialsProvider)); - } - - [Fact] - public void TestDoNotRegisterWhenHasNoExpiry() - { - ICredentialsProvider credentialsProvider = new MockCredentialsProvider(_testOutputHelper, TimeSpan.Zero); - Task cb(bool unused) => Task.CompletedTask; - - _refresher.Register(credentialsProvider, cb); - - Assert.False(_refresher.Unregister(credentialsProvider)); - } - - [Fact] - public async Task TestRefreshToken() - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) - { - using (CancellationTokenRegistration ctr = cts.Token.Register(() => tcs.TrySetCanceled())) - { - var credentialsProvider = new MockCredentialsProvider(_testOutputHelper, TimeSpan.FromSeconds(1)); - - Task cb(bool arg) - { - tcs.SetResult(arg); - return Task.CompletedTask; - } - - _refresher.Register(credentialsProvider, cb); - Assert.True(await tcs.Task); - Assert.True(credentialsProvider.RefreshCalled); - Assert.True(_refresher.Unregister(credentialsProvider)); - } - } - } - - [Fact] - public async Task TestRefreshTokenFailed() - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) - { - using (CancellationTokenRegistration ctr = cts.Token.Register(() => tcs.TrySetCanceled())) - { - var credentialsProvider = new MockCredentialsProvider(_testOutputHelper, TimeSpan.FromSeconds(1)); - - Task cb(bool arg) - { - tcs.SetResult(arg); - return Task.CompletedTask; - } - - var ex = new Exception(); - credentialsProvider.PasswordThrows(ex); - - _refresher.Register(credentialsProvider, cb); - Assert.False(await tcs.Task); - Assert.True(credentialsProvider.RefreshCalled); - Assert.True(_refresher.Unregister(credentialsProvider)); - } - } - } - } -}