Skip to content

Commit 5914434

Browse files
MarcialRosaleslukebakken
authored andcommitted
Support OAuth2 authentication
Change test assertion Restore original test The new test did work in Unix but failed on Windows Fix whitespace, remove use of #nullable, do not remove UserName / Password from connection factory Do not make unnecessary API changes Add tls-gen as a submodule, start getting bash scripts to pass shellcheck Bash script refactoring Use full docker options Clean up OAuth2 test Program Remove unused code in start-rabbitmq.sh Start to move OAuth2 code to its own project Finish moving OAuth2 code to its own project Remove OAuth2 from APIApproval Add OAuth2 API approval test and move verified output into each directory Version the RabbitMQ.Client.OAuth2 project independently of the RabbitMQ.Client project Fix lack of whitespaces, use TimeSpan to represent a duration rather than a long. Fix APIApproval Fix project refs, use forward slashes
1 parent b6f9437 commit 5914434

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)