Skip to content

Commit 6677851

Browse files
authored
Merge pull request #1387 from rabbitmq/rabbitmq-dotnet-client-1346-6.x
Port OAuth2 support to 6.x
2 parents 68f9f1b + fd9a305 commit 6677851

Some content is hidden

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

45 files changed

+4679
-57
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
_site/
2+
Properties/
23

34
###################
45
## Generated files
@@ -28,6 +29,7 @@ test.sh
2829
test-output.log
2930
InternalTrace*
3031
nunit-agent*
32+
3133
#################
3234
## Visual Studio
3335
#################

RabbitMQDotNetClient.sln

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unit", "projects\Unit\Unit.
1414
EndProject
1515
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Apigen", "projects\Apigen\Apigen.csproj", "{B416DDB7-5E3E-4A20-B5A9-C6E518E203A2}"
1616
EndProject
17+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RabbitMQ.Client.OAuth2", "projects\RabbitMQ.Client.OAuth2\RabbitMQ.Client.OAuth2.csproj", "{E40AB526-336F-4002-8736-00DDECE4E9E9}"
18+
EndProject
19+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestApplications", "TestApplications", "{701568F5-222E-4010-8D0D-2AAE78FCE1F0}"
20+
EndProject
21+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OAuth2", "projects\TestApplications\OAuth2\OAuth2.csproj", "{07E203AC-9E4B-4BED-9445-E2B45E10E412}"
22+
EndProject
1723
Global
1824
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1925
Debug|Any CPU = Debug|Any CPU
@@ -32,10 +38,21 @@ Global
3238
{B416DDB7-5E3E-4A20-B5A9-C6E518E203A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
3339
{B416DDB7-5E3E-4A20-B5A9-C6E518E203A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
3440
{B416DDB7-5E3E-4A20-B5A9-C6E518E203A2}.Release|Any CPU.Build.0 = Release|Any CPU
41+
{E40AB526-336F-4002-8736-00DDECE4E9E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42+
{E40AB526-336F-4002-8736-00DDECE4E9E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
43+
{E40AB526-336F-4002-8736-00DDECE4E9E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
44+
{E40AB526-336F-4002-8736-00DDECE4E9E9}.Release|Any CPU.Build.0 = Release|Any CPU
45+
{07E203AC-9E4B-4BED-9445-E2B45E10E412}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46+
{07E203AC-9E4B-4BED-9445-E2B45E10E412}.Debug|Any CPU.Build.0 = Debug|Any CPU
47+
{07E203AC-9E4B-4BED-9445-E2B45E10E412}.Release|Any CPU.ActiveCfg = Release|Any CPU
48+
{07E203AC-9E4B-4BED-9445-E2B45E10E412}.Release|Any CPU.Build.0 = Release|Any CPU
3549
EndGlobalSection
3650
GlobalSection(SolutionProperties) = preSolution
3751
HideSolutionNode = FALSE
3852
EndGlobalSection
53+
GlobalSection(NestedProjects) = preSolution
54+
{07E203AC-9E4B-4BED-9445-E2B45E10E412} = {701568F5-222E-4010-8D0D-2AAE78FCE1F0}
55+
EndGlobalSection
3956
GlobalSection(ExtensibilityGlobals) = postSolution
4057
SolutionGuid = {3C6A0C44-FA63-4101-BBF9-2598641167D1}
4158
EndGlobalSection

projects/Apigen/Apigen.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<Compile Include="..\RabbitMQ.Client\client\api\AmqpTimestamp.cs;..\RabbitMQ.Client\client\api\IBasicConsumer.cs;..\RabbitMQ.Client\client\api\IBasicProperties.cs;..\RabbitMQ.Client\client\api\IContentHeader.cs;..\RabbitMQ.Client\client\api\IModel.cs;..\RabbitMQ.Client\client\api\PublicationAddress.cs;..\RabbitMQ.Client\client\api\IBasicPublishBatch.cs;..\RabbitMQ.Client\client\api\BasicGetResult.cs;..\RabbitMQ.Client\client\api\QueueDeclareOk.cs;..\RabbitMQ.Client\client\api\ShutdownEventArgs.cs;..\RabbitMQ.Client\client\api\ShutdownInitiator.cs;..\RabbitMQ.Client\client\events\BasicReturnEventArgs.cs;..\RabbitMQ.Client\client\events\BasicAckEventArgs.cs;..\RabbitMQ.Client\client\events\BasicNackEventArgs.cs;..\RabbitMQ.Client\client\events\CallbackExceptionEventArgs.cs;..\RabbitMQ.Client\client\events\ConsumerEventArgs.cs;..\RabbitMQ.Client\client\events\FlowControlEventArgs.cs;..\RabbitMQ.Client\client\impl\IFullModel.cs" />
9+
<Compile Include="../RabbitMQ.Client/client/api/AmqpTimestamp.cs;../RabbitMQ.Client/client/api/IBasicConsumer.cs;../RabbitMQ.Client/client/api/IBasicProperties.cs;../RabbitMQ.Client/client/api/IContentHeader.cs;../RabbitMQ.Client/client/api/IModel.cs;../RabbitMQ.Client/client/api/PublicationAddress.cs;../RabbitMQ.Client/client/api/IBasicPublishBatch.cs;../RabbitMQ.Client/client/api/BasicGetResult.cs;../RabbitMQ.Client/client/api/QueueDeclareOk.cs;../RabbitMQ.Client/client/api/ShutdownEventArgs.cs;../RabbitMQ.Client/client/api/ShutdownInitiator.cs;../RabbitMQ.Client/client/events/BasicReturnEventArgs.cs;../RabbitMQ.Client/client/events/BasicAckEventArgs.cs;../RabbitMQ.Client/client/events/BasicNackEventArgs.cs;../RabbitMQ.Client/client/events/CallbackExceptionEventArgs.cs;../RabbitMQ.Client/client/events/ConsumerEventArgs.cs;../RabbitMQ.Client/client/events/FlowControlEventArgs.cs;../RabbitMQ.Client/client/impl/IFullModel.cs" />
1010
</ItemGroup>
1111

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

0 commit comments

Comments
 (0)