Skip to content

Commit

Permalink
Refactor credential refresh (#1649)
Browse files Browse the repository at this point in the history
Fixes #1639

* Remove all traces of credential refresh from `RabbitMQ.Client`
* Add `CredentialsRefresher`, which does not use timers, and calls refresh based on `ValidUntil`
* Use `ICredentialsProvider` in the auth mechanisms, async
* Pass `CancellationToken` around and correctly dispose Http objects (thanks @Tornhoof)
* Use `SemaphoreSlim`
* Refresh token at 1/3 the interval value.
* Close connection mid-integration test for OAuth2
* Use OAuth2 for EasyNetQ.Management.Client in OAuth2 tests.
* Create separate credentials provider for HTTP API requests.
* Set RabbitMQ logging to debug for OAuth2
* Use `JsonPropertyName`
* Use OAuth2 for HTTP API access. Thanks @MarcialRosales
  • Loading branch information
lukebakken authored Aug 6, 2024
1 parent 5aca35a commit 8d10b43
Show file tree
Hide file tree
Showing 38 changed files with 1,304 additions and 974 deletions.
16 changes: 8 additions & 8 deletions projects/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="Ductus.FluentDocker" Version="2.10.59" />
<PackageVersion Include="EasyNetQ.Management.Client" Version="2.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="EasyNetQ.Management.Client" Version="3.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="Nullable" Version="1.3.1" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.7.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.8.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.9.0" />
<!--
Note: do NOT upgrade the System.IO.Pipelines dependency unless necessary
See https://github.com/rabbitmq/rabbitmq-dotnet-client/pull/1481#pullrequestreview-1847905299
-->
<PackageVersion Include="System.IO.Pipelines" Version="6.0.0" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="WireMock.Net" Version="1.5.49" />
<PackageVersion Include="xunit" Version="2.7.0" />
<PackageVersion Include="WireMock.Net" Version="1.5.62" />
<PackageVersion Include="xunit" Version="2.9.0" />
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework)=='netstandard2.0'">
Expand All @@ -46,4 +46,4 @@
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<GlobalPackageReference Include="MinVer" Version="5.0.0" />
</ItemGroup>
</Project>
</Project>
144 changes: 144 additions & 0 deletions projects/RabbitMQ.Client.OAuth2/CredentialsRefresher.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
54 changes: 54 additions & 0 deletions projects/RabbitMQ.Client.OAuth2/CredentialsRefresherEventSource.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
42 changes: 42 additions & 0 deletions projects/RabbitMQ.Client.OAuth2/IOAuth2Client.cs
Original file line number Diff line number Diff line change
@@ -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<IToken> RequestTokenAsync(CancellationToken cancellationToken = default);
Task<IToken> RefreshTokenAsync(IToken token, CancellationToken cancellationToken = default);
}
}
Loading

0 comments on commit 8d10b43

Please sign in to comment.