Skip to content

Commit 6668107

Browse files
authored
Merge pull request #1346 from rabbitmq/support-credential-refresh
Support OAuth2 authentication
2 parents b6f9437 + 5914434 commit 6668107

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+4611
-75
lines changed

Build.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<ItemGroup>
99
<ProjectReference Include="projects/Benchmarks/Benchmarks.csproj" />
1010
<ProjectReference Include="projects/RabbitMQ.Client/RabbitMQ.Client.csproj" />
11+
<ProjectReference Include="projects/RabbitMQ.Client.OAuth2/RabbitMQ.Client.OAuth2.csproj" />
1112
<ProjectReference Include="projects/TestApplications/CreateChannel/CreateChannel.csproj" />
1213
<ProjectReference Include="projects/TestApplications/MassPublish/MassPublish.csproj" />
1314
<ProjectReference Include="projects/Unit/Unit.csproj" />

RabbitMQDotNetClient.sln

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Microsoft Visual Studio Solution File, Format Version 12.00
2-
# Visual Studio Version 16
3-
VisualStudioVersion = 16.0.29806.167
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.7.34003.232
44
MinimumVisualStudioVersion = 10.0.40219.1
55
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{34486CC0-D61E-46BA-9E5E-6E8EFA7C34B5}"
66
ProjectSection(SolutionItems) = preProject
@@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestApplications", "TestApp
1919
EndProject
2020
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateChannel", "projects\TestApplications\CreateChannel\CreateChannel.csproj", "{4A589408-F3A3-40E1-A6DF-F5E620F7CA31}"
2121
EndProject
22+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RabbitMQ.Client.OAuth2", "projects\RabbitMQ.Client.OAuth2\RabbitMQ.Client.OAuth2.csproj", "{794C7B31-0E9A-44A4-B285-0F3CAF6209F1}"
23+
EndProject
2224
Global
2325
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2426
Debug|Any CPU = Debug|Any CPU
@@ -45,6 +47,10 @@ Global
4547
{4A589408-F3A3-40E1-A6DF-F5E620F7CA31}.Debug|Any CPU.Build.0 = Debug|Any CPU
4648
{4A589408-F3A3-40E1-A6DF-F5E620F7CA31}.Release|Any CPU.ActiveCfg = Release|Any CPU
4749
{4A589408-F3A3-40E1-A6DF-F5E620F7CA31}.Release|Any CPU.Build.0 = Release|Any CPU
50+
{794C7B31-0E9A-44A4-B285-0F3CAF6209F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51+
{794C7B31-0E9A-44A4-B285-0F3CAF6209F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
52+
{794C7B31-0E9A-44A4-B285-0F3CAF6209F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
53+
{794C7B31-0E9A-44A4-B285-0F3CAF6209F1}.Release|Any CPU.Build.0 = Release|Any CPU
4854
EndGlobalSection
4955
GlobalSection(SolutionProperties) = preSolution
5056
HideSolutionNode = FALSE

projects/Benchmarks/Benchmarks.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</ItemGroup>
2121

2222
<ItemGroup>
23-
<ProjectReference Include="..\RabbitMQ.Client\RabbitMQ.Client.csproj" />
23+
<ProjectReference Include="../RabbitMQ.Client/RabbitMQ.Client.csproj" />
2424
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
2525
</ItemGroup>
2626

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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-2020 VMware, Inc.
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-2020 VMware, Inc. All rights reserved.
30+
//---------------------------------------------------------------------------
31+
32+
using System;
33+
using System.Collections.Generic;
34+
using System.Net.Http;
35+
using System.Net.Http.Headers;
36+
using System.Net.Http.Json;
37+
using System.Text.Json.Serialization;
38+
using System.Threading.Tasks;
39+
40+
namespace RabbitMQ.Client.OAuth2
41+
{
42+
public interface IOAuth2Client
43+
{
44+
public IToken RequestToken();
45+
public IToken RefreshToken(IToken token);
46+
}
47+
48+
public interface IToken
49+
{
50+
public string AccessToken { get; }
51+
public string RefreshToken { get; }
52+
public TimeSpan ExpiresIn { get; }
53+
public bool hasExpired { get; }
54+
}
55+
56+
public class Token : IToken
57+
{
58+
private readonly JsonToken _source;
59+
private readonly DateTime _lastTokenRenewal;
60+
61+
public Token(JsonToken json)
62+
{
63+
this._source = json;
64+
this._lastTokenRenewal = DateTime.Now;
65+
}
66+
67+
public string AccessToken
68+
{
69+
get
70+
{
71+
return _source.access_token;
72+
}
73+
}
74+
75+
public string RefreshToken
76+
{
77+
get
78+
{
79+
return _source.refresh_token;
80+
}
81+
}
82+
83+
public TimeSpan ExpiresIn
84+
{
85+
get
86+
{
87+
return TimeSpan.FromSeconds(_source.expires_in);
88+
}
89+
}
90+
91+
bool IToken.hasExpired
92+
{
93+
get
94+
{
95+
TimeSpan age = DateTime.Now - _lastTokenRenewal;
96+
return age > ExpiresIn;
97+
}
98+
}
99+
}
100+
101+
public class OAuth2ClientBuilder
102+
{
103+
private readonly string _clientId;
104+
private readonly string _clientSecret;
105+
private readonly Uri _tokenEndpoint;
106+
private string _scope;
107+
private IDictionary<string, string> _additionalRequestParameters;
108+
private HttpClientHandler _httpClientHandler;
109+
110+
public OAuth2ClientBuilder(string clientId, string clientSecret, Uri tokenEndpoint)
111+
{
112+
_clientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
113+
_clientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret));
114+
_tokenEndpoint = tokenEndpoint ?? throw new ArgumentNullException(nameof(tokenEndpoint));
115+
116+
}
117+
118+
public OAuth2ClientBuilder SetScope(string scope)
119+
{
120+
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
121+
return this;
122+
}
123+
124+
public OAuth2ClientBuilder SetHttpClientHandler(HttpClientHandler handler)
125+
{
126+
_httpClientHandler = handler ?? throw new ArgumentNullException(nameof(handler));
127+
return this;
128+
}
129+
130+
public OAuth2ClientBuilder AddRequestParameter(string param, string paramValue)
131+
{
132+
if (param == null)
133+
{
134+
throw new ArgumentNullException("param is null");
135+
}
136+
if (paramValue == null)
137+
{
138+
throw new ArgumentNullException("paramValue is null");
139+
}
140+
if (_additionalRequestParameters == null)
141+
{
142+
_additionalRequestParameters = new Dictionary<string, string>();
143+
}
144+
_additionalRequestParameters[param] = paramValue;
145+
return this;
146+
}
147+
148+
public IOAuth2Client Build()
149+
{
150+
return new OAuth2Client(_clientId, _clientSecret, _tokenEndpoint,
151+
_scope, _additionalRequestParameters, _httpClientHandler);
152+
}
153+
}
154+
155+
/**
156+
* Default implementation of IOAuth2Client. It uses Client_Credentials OAuth2 flow to request a
157+
* token. The basic constructor assumes no scopes are needed only the OAuth2 Client credentiuals.
158+
* The additional constructor accepts a Dictionary with all the request parameters passed onto the
159+
* OAuth2 request token.
160+
*/
161+
internal class OAuth2Client : IOAuth2Client, IDisposable
162+
{
163+
const string GRANT_TYPE = "grant_type";
164+
const string CLIENT_ID = "client_id";
165+
const string SCOPE = "scope";
166+
const string CLIENT_SECRET = "client_secret";
167+
const string REFRESH_TOKEN = "refresh_token";
168+
const string GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
169+
170+
private readonly string _clientId;
171+
private readonly string _clientSecret;
172+
private readonly Uri _tokenEndpoint;
173+
private readonly string _scope;
174+
private readonly IDictionary<string, string> _additionalRequestParameters;
175+
176+
public static readonly IDictionary<string, string> EMPTY = new Dictionary<string, string>();
177+
178+
private HttpClient _httpClient;
179+
180+
public OAuth2Client(string clientId, string clientSecret, Uri tokenEndpoint, string scope,
181+
IDictionary<string, string> additionalRequestParameters,
182+
HttpClientHandler httpClientHandler)
183+
{
184+
this._clientId = clientId;
185+
this._clientSecret = clientSecret;
186+
this._scope = scope;
187+
this._additionalRequestParameters = additionalRequestParameters == null ? EMPTY : additionalRequestParameters;
188+
this._tokenEndpoint = tokenEndpoint;
189+
190+
_httpClient = httpClientHandler == null ? new HttpClient() :
191+
new HttpClient(httpClientHandler);
192+
_httpClient.DefaultRequestHeaders.Accept.Clear();
193+
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
194+
}
195+
196+
public IToken RequestToken()
197+
{
198+
var req = new HttpRequestMessage(HttpMethod.Post, _tokenEndpoint);
199+
req.Content = new FormUrlEncodedContent(buildRequestParameters());
200+
201+
Task<HttpResponseMessage> response = _httpClient.SendAsync(req);
202+
response.Wait();
203+
response.Result.EnsureSuccessStatusCode();
204+
Task<JsonToken> token = response.Result.Content.ReadFromJsonAsync<JsonToken>();
205+
token.Wait();
206+
return new Token(token.Result);
207+
}
208+
209+
public IToken RefreshToken(IToken token)
210+
{
211+
if (token.RefreshToken == null)
212+
{
213+
throw new InvalidOperationException("Token has no Refresh Token");
214+
}
215+
216+
var req = new HttpRequestMessage(HttpMethod.Post, _tokenEndpoint)
217+
{
218+
Content = new FormUrlEncodedContent(buildRefreshParameters(token))
219+
};
220+
221+
Task<HttpResponseMessage> response = _httpClient.SendAsync(req);
222+
response.Wait();
223+
response.Result.EnsureSuccessStatusCode();
224+
Task<JsonToken> refreshedToken = response.Result.Content.ReadFromJsonAsync<JsonToken>();
225+
refreshedToken.Wait();
226+
return new Token(refreshedToken.Result);
227+
}
228+
229+
public void Dispose()
230+
{
231+
_httpClient.Dispose();
232+
}
233+
234+
private Dictionary<string, string> buildRequestParameters()
235+
{
236+
var dict = new Dictionary<string, string>(_additionalRequestParameters);
237+
dict.Add(CLIENT_ID, _clientId);
238+
dict.Add(CLIENT_SECRET, _clientSecret);
239+
if (_scope != null && _scope.Length > 0)
240+
{
241+
dict.Add(SCOPE, _scope);
242+
}
243+
dict.Add(GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS);
244+
return dict;
245+
}
246+
247+
private Dictionary<string, string> buildRefreshParameters(IToken token)
248+
{
249+
var dict = buildRequestParameters();
250+
dict.Remove(GRANT_TYPE);
251+
dict.Add(GRANT_TYPE, REFRESH_TOKEN);
252+
if (_scope != null)
253+
{
254+
dict.Add(SCOPE, _scope);
255+
}
256+
dict.Add(REFRESH_TOKEN, token.RefreshToken);
257+
return dict;
258+
}
259+
}
260+
261+
public class JsonToken
262+
{
263+
public JsonToken()
264+
{
265+
}
266+
267+
public JsonToken(string access_token, string refresh_token, TimeSpan expires_in_span)
268+
{
269+
this.access_token = access_token;
270+
this.refresh_token = refresh_token;
271+
this.expires_in = (long)expires_in_span.TotalSeconds;
272+
}
273+
274+
public JsonToken(string access_token, string refresh_token, long expires_in)
275+
{
276+
this.access_token = access_token;
277+
this.refresh_token = refresh_token;
278+
this.expires_in = expires_in;
279+
}
280+
281+
public string access_token
282+
{
283+
get; set;
284+
}
285+
286+
public string refresh_token
287+
{
288+
get; set;
289+
}
290+
291+
public long expires_in
292+
{
293+
get; set;
294+
}
295+
}
296+
}

0 commit comments

Comments
 (0)