Skip to content

Commit 8d10b43

Browse files
authored
Refactor credential refresh (#1649)
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
1 parent 5aca35a commit 8d10b43

38 files changed

+1304
-974
lines changed

projects/Directory.Packages.props

+8-8
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@
55
<ItemGroup>
66
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
77
<PackageVersion Include="Ductus.FluentDocker" Version="2.10.59" />
8-
<PackageVersion Include="EasyNetQ.Management.Client" Version="2.0.0" />
9-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
8+
<PackageVersion Include="EasyNetQ.Management.Client" Version="3.0.0" />
9+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
1010
<PackageVersion Include="Nullable" Version="1.3.1" />
11-
<PackageVersion Include="OpenTelemetry.Api" Version="1.7.0" />
12-
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.8.0" />
11+
<PackageVersion Include="OpenTelemetry.Api" Version="1.9.0" />
12+
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.9.0" />
1313
<!--
1414
Note: do NOT upgrade the System.IO.Pipelines dependency unless necessary
1515
See https://github.com/rabbitmq/rabbitmq-dotnet-client/pull/1481#pullrequestreview-1847905299
1616
-->
1717
<PackageVersion Include="System.IO.Pipelines" Version="6.0.0" />
1818
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
19-
<PackageVersion Include="WireMock.Net" Version="1.5.49" />
20-
<PackageVersion Include="xunit" Version="2.7.0" />
19+
<PackageVersion Include="WireMock.Net" Version="1.5.62" />
20+
<PackageVersion Include="xunit" Version="2.9.0" />
2121
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
22-
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" />
22+
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
2323
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
2424
</ItemGroup>
2525
<ItemGroup Condition="$(TargetFramework)=='netstandard2.0'">
@@ -46,4 +46,4 @@
4646
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
4747
<GlobalPackageReference Include="MinVer" Version="5.0.0" />
4848
</ItemGroup>
49-
</Project>
49+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// This source code is dual-licensed under the Apache License, version
2+
// 2.0, and the Mozilla Public License, version 2.0.
3+
//
4+
// The APL v2.0:
5+
//
6+
//---------------------------------------------------------------------------
7+
// Copyright (c) 2007-2024 Broadcom. All Rights Reserved.
8+
//
9+
// Licensed under the Apache License, Version 2.0 (the "License");
10+
// you may not use this file except in compliance with the License.
11+
// You may obtain a copy of the License at
12+
//
13+
// https://www.apache.org/licenses/LICENSE-2.0
14+
//
15+
// Unless required by applicable law or agreed to in writing, software
16+
// distributed under the License is distributed on an "AS IS" BASIS,
17+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
// See the License for the specific language governing permissions and
19+
// limitations under the License.
20+
//---------------------------------------------------------------------------
21+
//
22+
// The MPL v2.0:
23+
//
24+
//---------------------------------------------------------------------------
25+
// This Source Code Form is subject to the terms of the Mozilla Public
26+
// License, v. 2.0. If a copy of the MPL was not distributed with this
27+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
28+
//
29+
// Copyright (c) 2007-2024 Broadcom. All Rights Reserved.
30+
//---------------------------------------------------------------------------
31+
32+
using System;
33+
using System.Threading;
34+
using System.Threading.Tasks;
35+
36+
namespace RabbitMQ.Client.OAuth2
37+
{
38+
public delegate Task NotifyCredentialsRefreshedAsync(Credentials? credentials,
39+
Exception? exception = null,
40+
CancellationToken cancellationToken = default);
41+
42+
public class CredentialsRefresher : IDisposable
43+
{
44+
private readonly ICredentialsProvider _credentialsProvider;
45+
private readonly NotifyCredentialsRefreshedAsync _onRefreshed;
46+
47+
private readonly CancellationTokenSource _internalCts = new CancellationTokenSource();
48+
private readonly CancellationTokenSource _linkedCts;
49+
50+
private readonly Task _refreshTask;
51+
52+
private Credentials? _credentials;
53+
private bool _disposedValue = false;
54+
55+
public CredentialsRefresher(ICredentialsProvider credentialsProvider,
56+
NotifyCredentialsRefreshedAsync onRefreshed,
57+
CancellationToken cancellationToken)
58+
{
59+
if (credentialsProvider is null)
60+
{
61+
throw new ArgumentNullException(nameof(credentialsProvider));
62+
}
63+
64+
if (onRefreshed is null)
65+
{
66+
throw new ArgumentNullException(nameof(onRefreshed));
67+
}
68+
69+
_linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_internalCts.Token, cancellationToken);
70+
71+
_credentialsProvider = credentialsProvider;
72+
_onRefreshed = onRefreshed;
73+
74+
_refreshTask = Task.Run(RefreshLoopAsync, _linkedCts.Token);
75+
76+
CredentialsRefresherEventSource.Log.Started(_credentialsProvider.Name);
77+
}
78+
79+
public Credentials? Credentials => _credentials;
80+
81+
private async Task RefreshLoopAsync()
82+
{
83+
while (false == _linkedCts.IsCancellationRequested)
84+
{
85+
try
86+
{
87+
_credentials = await _credentialsProvider.GetCredentialsAsync(_linkedCts.Token)
88+
.ConfigureAwait(false);
89+
90+
if (_linkedCts.IsCancellationRequested)
91+
{
92+
break;
93+
}
94+
95+
CredentialsRefresherEventSource.Log.RefreshedCredentials(_credentialsProvider.Name);
96+
97+
await _onRefreshed(_credentials, null, _linkedCts.Token)
98+
.ConfigureAwait(false);
99+
}
100+
catch (OperationCanceledException)
101+
{
102+
return;
103+
}
104+
catch (Exception ex)
105+
{
106+
await _onRefreshed(null, ex, _linkedCts.Token)
107+
.ConfigureAwait(false);
108+
}
109+
110+
TimeSpan delaySpan = TimeSpan.FromSeconds(30);
111+
if (_credentials != null && _credentials.ValidUntil.HasValue)
112+
{
113+
delaySpan = TimeSpan.FromMilliseconds(_credentials.ValidUntil.Value.TotalMilliseconds * (1.0 - (1 / 3.0)));
114+
}
115+
116+
await Task.Delay(delaySpan, _linkedCts.Token)
117+
.ConfigureAwait(false);
118+
}
119+
}
120+
121+
public void Dispose()
122+
{
123+
Dispose(disposing: true);
124+
GC.SuppressFinalize(this);
125+
}
126+
127+
protected virtual void Dispose(bool disposing)
128+
{
129+
if (!_disposedValue)
130+
{
131+
if (disposing)
132+
{
133+
_internalCts.Cancel();
134+
_refreshTask.Wait(TimeSpan.FromSeconds(5));
135+
_internalCts.Dispose();
136+
_linkedCts.Dispose();
137+
CredentialsRefresherEventSource.Log.Stopped(_credentialsProvider.Name);
138+
}
139+
140+
_disposedValue = true;
141+
}
142+
}
143+
}
144+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// This source code is dual-licensed under the Apache License, version
2+
// 2.0, and the Mozilla Public License, version 2.0.
3+
//
4+
// The APL v2.0:
5+
//
6+
//---------------------------------------------------------------------------
7+
// Copyright (c) 2007-2024 Broadcom. All Rights Reserved.
8+
//
9+
// Licensed under the Apache License, Version 2.0 (the "License");
10+
// you may not use this file except in compliance with the License.
11+
// You may obtain a copy of the License at
12+
//
13+
// https://www.apache.org/licenses/LICENSE-2.0
14+
//
15+
// Unless required by applicable law or agreed to in writing, software
16+
// distributed under the License is distributed on an "AS IS" BASIS,
17+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
// See the License for the specific language governing permissions and
19+
// limitations under the License.
20+
//---------------------------------------------------------------------------
21+
//
22+
// The MPL v2.0:
23+
//
24+
//---------------------------------------------------------------------------
25+
// This Source Code Form is subject to the terms of the Mozilla Public
26+
// License, v. 2.0. If a copy of the MPL was not distributed with this
27+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
28+
//
29+
// Copyright (c) 2007-2024 Broadcom. All Rights Reserved.
30+
//---------------------------------------------------------------------------
31+
32+
using System.Diagnostics.CodeAnalysis;
33+
using System.Diagnostics.Tracing;
34+
35+
namespace RabbitMQ.Client.OAuth2
36+
{
37+
[EventSource(Name = "CredentialRefresher")]
38+
public class CredentialsRefresherEventSource : EventSource
39+
{
40+
public static CredentialsRefresherEventSource Log { get; } = new CredentialsRefresherEventSource();
41+
42+
[Event(1)]
43+
public void Started(string name) => WriteEvent(1, "Started", name);
44+
45+
[Event(2)]
46+
public void Stopped(string name) => WriteEvent(2, "Stopped", name);
47+
48+
[Event(3)]
49+
#if NET6_0_OR_GREATER
50+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Parameters to this method are primitive and are trimmer safe")]
51+
#endif
52+
public void RefreshedCredentials(string name) => WriteEvent(3, "RefreshedCredentials", name);
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// This source code is dual-licensed under the Apache License, version
2+
// 2.0, and the Mozilla Public License, version 2.0.
3+
//
4+
// The APL v2.0:
5+
//
6+
//---------------------------------------------------------------------------
7+
// Copyright (c) 2007-2024 Broadcom. All Rights Reserved.
8+
//
9+
// Licensed under the Apache License, Version 2.0 (the "License");
10+
// you may not use this file except in compliance with the License.
11+
// You may obtain a copy of the License at
12+
//
13+
// https://www.apache.org/licenses/LICENSE-2.0
14+
//
15+
// Unless required by applicable law or agreed to in writing, software
16+
// distributed under the License is distributed on an "AS IS" BASIS,
17+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
// See the License for the specific language governing permissions and
19+
// limitations under the License.
20+
//---------------------------------------------------------------------------
21+
//
22+
// The MPL v2.0:
23+
//
24+
//---------------------------------------------------------------------------
25+
// This Source Code Form is subject to the terms of the Mozilla Public
26+
// License, v. 2.0. If a copy of the MPL was not distributed with this
27+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
28+
//
29+
// Copyright (c) 2007-2024 Broadcom. All Rights Reserved.
30+
//---------------------------------------------------------------------------
31+
32+
using System.Threading;
33+
using System.Threading.Tasks;
34+
35+
namespace RabbitMQ.Client.OAuth2
36+
{
37+
public interface IOAuth2Client
38+
{
39+
Task<IToken> RequestTokenAsync(CancellationToken cancellationToken = default);
40+
Task<IToken> RefreshTokenAsync(IToken token, CancellationToken cancellationToken = default);
41+
}
42+
}

0 commit comments

Comments
 (0)