-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathAuthorizationTests.cs
More file actions
372 lines (341 loc) · 17.4 KB
/
Copy pathAuthorizationTests.cs
File metadata and controls
372 lines (341 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using GraphQL.AspNetCore3.Errors;
using GraphQL.Execution;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Hosting;
#if NET48 || NETCOREAPP2_1
using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime;
#endif
namespace Tests.Middleware;
public class AuthorizationTests
{
private GraphQLHttpMiddlewareOptions _options = null!;
private bool _enableCustomErrorInfoProvider;
private TestServer _server;
public AuthorizationTests()
{
_server = CreateServer();
}
private TestServer CreateServer(Action<IServiceCollection>? configureServices = null)
{
var hostBuilder = new WebHostBuilder();
hostBuilder.ConfigureServices(services => {
services.AddSingleton<Chat.Services.ChatService>();
services.AddGraphQL(b => b
.AddAutoSchema<Chat.Schema.Query>()
.AddErrorInfoProvider(new CustomErrorInfoProvider(this))
.AddSystemTextJson());
services.AddRouting();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters.ValidateIssuerSigningKey = false;
options.TokenValidationParameters.ValidateLifetime = false;
options.TokenValidationParameters.ValidateAudience = false;
options.TokenValidationParameters.ValidIssuer = "test";
options.TokenValidationParameters.RequireSignedTokens = false;
});
services.AddAuthorization(config => {
config.AddPolicy("MyPolicy", policyConfig => {
policyConfig.RequireAuthenticatedUser();
});
config.AddPolicy("FailingPolicy", policyConfig => {
policyConfig.RequireRole("FailingRole");
});
});
configureServices?.Invoke(services);
});
hostBuilder.Configure(app => {
app.UseWebSockets();
app.UseAuthentication();
#if !NETCOREAPP2_1 && !NET48
app.UseAuthorization();
#endif
app.UseGraphQL("/graphql", opts => {
_options = opts;
});
});
return new TestServer(hostBuilder);
}
private string CreateJwtToken()
{
var tokenHandler = new JwtSecurityTokenHandler();
var token = new JwtSecurityToken(issuer: "test", claims: new Claim[] {
new Claim("role", "MyRole")
});
return tokenHandler.WriteToken(token);
}
private Task<HttpResponseMessage> PostQueryAsync(string json, bool authenticated)
{
var client = _server.CreateClient();
var content = new StringContent(json);
content.Headers.ContentType = new("application/graphql");
var request = new HttpRequestMessage(HttpMethod.Post, "/graphql") { Content = content };
if (authenticated)
request.Headers.Authorization = new("Bearer", CreateJwtToken());
return client.SendAsync(request);
}
[Fact]
public async Task NotAuthorized()
{
_options.AuthorizationRequired = true;
using var response = await PostQueryAsync("{ __typename }", false);
response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied for schema."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
[Fact]
public async Task NotAuthorized_Get()
{
_options.AuthorizationRequired = true;
var client = _server.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "/graphql?query={ __typename }");
request.Headers.Add("GraphQL-Require-Preflight", "true");
using var response = await client.SendAsync(request);
response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied for schema."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
[Fact]
public async Task WebSocket_IgnoreAuthenticationOnConnect()
{
_options.AuthorizationRequired = true;
var webSocketClient = _server.CreateWebSocketClient();
webSocketClient.ConfigureRequest = request => {
request.Headers["Sec-WebSocket-Protocol"] = "graphql-ws";
};
webSocketClient.SubProtocols.Add("graphql-ws");
using var webSocket = await webSocketClient.ConnectAsync(new Uri(_server.BaseAddress, "/graphql"), default);
}
[Fact]
public async Task WebSocket_NotAuthorized()
{
var hostBuilder = new WebHostBuilder();
hostBuilder.ConfigureServices(services => {
services.AddGraphQL(b => b
.AddAutoSchema<Chat.Schema.Query>()
.AddSystemTextJson());
});
hostBuilder.Configure(app => {
app.UseWebSockets();
app.UseGraphQL<MyMiddleware>("/graphql");
});
var server = new TestServer(hostBuilder);
var webSocketClient = server.CreateWebSocketClient();
webSocketClient.ConfigureRequest = request => {
request.Headers["Sec-WebSocket-Protocol"] = "graphql-ws";
};
webSocketClient.SubProtocols.Add("graphql-ws");
var error = await Should.ThrowAsync<InvalidOperationException>(() => webSocketClient.ConnectAsync(new Uri(_server.BaseAddress, "/graphql"), default));
error.Message.ShouldBe("Incomplete handshake, status code: 422");
}
private class MyMiddleware : GraphQLHttpMiddleware<ISchema>
{
#pragma warning disable CS0618 // Type or member is obsolete
public MyMiddleware(RequestDelegate next, IGraphQLTextSerializer serializer, IDocumentExecuter<ISchema> documentExecuter, IServiceScopeFactory serviceScopeFactory, IHostApplicationLifetime hostApplicationLifetime)
#pragma warning restore CS0618 // Type or member is obsolete
: base(next, serializer, documentExecuter, serviceScopeFactory, new(), hostApplicationLifetime)
{
}
protected override async ValueTask<bool> HandleAuthorizeWebSocketConnectionAsync(HttpContext context, RequestDelegate next)
{
await WriteErrorResponseAsync(context, (HttpStatusCode)422 /* HttpStatusCode.UnprocessableEntity */, "Access deined");
return true;
}
}
[Fact]
public async Task NotAuthorized_2()
{
_options.AuthorizationRequired = true;
_enableCustomErrorInfoProvider = true;
using var response = await PostQueryAsync("{ __typename }", false);
response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied; authorization required."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
[Fact]
public async Task Authorized()
{
_options.AuthorizationRequired = true;
using var response = await PostQueryAsync("{ __typename }", true);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""data"":{""__typename"":""Query""}}");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task NotAuthorized_Roles(bool authenticated)
{
_options.AuthorizedRoles.Add("AnotherRole");
_options.AuthorizedRoles.Add("FailingRole");
using var response = await PostQueryAsync("{ __typename }", authenticated);
response.StatusCode.ShouldBe(authenticated ? HttpStatusCode.Forbidden : HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied for schema."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task NotAuthorized_Roles_2(bool authenticated)
{
_options.AuthorizedRoles.Add("AnotherRole");
_options.AuthorizedRoles.Add("FailingRole");
_enableCustomErrorInfoProvider = true;
using var response = await PostQueryAsync("{ __typename }", authenticated);
response.StatusCode.ShouldBe(authenticated ? HttpStatusCode.Forbidden : HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
if (authenticated) {
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied; roles required \u0027AnotherRole\u0027/\u0027FailingRole\u0027."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
} else {
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied; authorization required."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
}
[Fact]
public async Task Authorized_Roles()
{
_options.AuthorizedRoles.Add("AnotherRole");
_options.AuthorizedRoles.Add("MyRole");
using var response = await PostQueryAsync("{ __typename }", true);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""data"":{""__typename"":""Query""}}");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task NotAuthorized_Policy(bool authenticated)
{
_options.AuthorizedPolicy = "FailingPolicy";
using var response = await PostQueryAsync("{ __typename }", authenticated);
response.StatusCode.ShouldBe(authenticated ? HttpStatusCode.Forbidden : HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied for schema."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task NotAuthorized_Policy_2(bool authenticated)
{
_options.AuthorizedPolicy = "FailingPolicy";
_enableCustomErrorInfoProvider = true;
using var response = await PostQueryAsync("{ __typename }", authenticated);
response.StatusCode.ShouldBe(authenticated ? HttpStatusCode.Forbidden : HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
if (authenticated) {
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied; policy required \u0027FailingPolicy\u0027."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
} else {
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied; authorization required."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
}
[Fact]
public async Task Authorized_Policy()
{
_options.AuthorizedPolicy = "MyPolicy";
using var response = await PostQueryAsync("{ __typename }", true);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""data"":{""__typename"":""Query""}}");
}
[Fact]
public async Task NotAuthorized_WrongScheme()
{
_server = CreateServer(services => {
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme); // change default scheme to Cookie authentication
});
_options.AuthorizationRequired = true;
using var response = await PostQueryAsync("{ __typename }", true); // send an authenticated request (with JWT bearer scheme)
response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied for schema."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
[Fact]
public async Task NotAuthorized_WrongScheme_2()
{
_server.Dispose();
_server = CreateServer(services => {
services.AddAuthentication().AddCookie(); // add Cookie authentication
});
_options.AuthorizationRequired = true;
_options.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme); // change authentication scheme for GraphQL requests to Cookie (which is not used by the test client)
using var response = await PostQueryAsync("{ __typename }", true); // send an authenticated request (with JWT bearer scheme)
response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""errors"":[{""message"":""Access denied for schema."",""extensions"":{""code"":""ACCESS_DENIED"",""codes"":[""ACCESS_DENIED""]}}]}");
}
[Fact]
public async Task NotAuthorized_WrongScheme_VerifyUser()
{
bool validatedUser = false;
_server = CreateServer(services => {
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme); // change default scheme to Cookie authentication
services.AddGraphQL(b => b
.ConfigureExecutionOptions(opts => {
opts.User.ShouldNotBeNull().Identity.ShouldNotBeNull().IsAuthenticated.ShouldBeFalse();
validatedUser = true;
}));
});
_options.AuthorizationRequired = false; // disable authorization requirements; we just want to verify that an anonymous user is passed to the execution options
using var response = await PostQueryAsync("{ __typename }", true); // send an authenticated request (with JWT bearer scheme)
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""data"":{""__typename"":""Query""}}");
validatedUser.ShouldBeTrue();
}
[Fact]
public async Task Authorized_DifferentScheme()
{
bool validatedUser = false;
_server = CreateServer(services => {
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme); // change default scheme to Cookie authentication
services.AddGraphQL(b => b.ConfigureExecutionOptions(opts => {
opts.User.ShouldNotBeNull().Identity.ShouldNotBeNull().IsAuthenticated.ShouldBeTrue();
validatedUser = true;
}));
});
_options.AuthorizationRequired = true;
_options.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
using var response = await PostQueryAsync("{ __typename }", true);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var actual = await response.Content.ReadAsStringAsync();
actual.ShouldBe(@"{""data"":{""__typename"":""Query""}}");
validatedUser.ShouldBeTrue();
}
[Fact]
public void SecurityHelperTests()
{
SecurityHelper.MergeUserPrincipal(null, null).ShouldNotBeNull().Identity.ShouldBeNull(); // Note that ASP.NET Core does not return null for anonymous user
var principal1 = new ClaimsPrincipal(new ClaimsIdentity()); // empty identity for primary identity (default for ASP.NET Core)
SecurityHelper.MergeUserPrincipal(null, principal1).ShouldBe(principal1);
var principal2 = new ClaimsPrincipal(new ClaimsIdentity("test1")); // non-empty identity for secondary identity
SecurityHelper.MergeUserPrincipal(principal1, principal2).Identities.ShouldHaveSingleItem().AuthenticationType.ShouldBe("test1");
var principal3 = new ClaimsPrincipal(new ClaimsIdentity("test2")); // merge two non-empty identities together
SecurityHelper.MergeUserPrincipal(principal2, principal3).Identities.Select(x => x.AuthenticationType).ShouldBe(new[] { "test2", "test1" }); // last one wins
}
private class CustomErrorInfoProvider : ErrorInfoProvider
{
private readonly AuthorizationTests _authorizationTests;
public CustomErrorInfoProvider(AuthorizationTests authorizationTests)
=> _authorizationTests = authorizationTests;
public override ErrorInfo GetInfo(ExecutionError executionError)
{
var info = base.GetInfo(executionError);
if (!_authorizationTests._enableCustomErrorInfoProvider)
return info;
if (executionError is AccessDeniedError accessDeniedError) {
if (accessDeniedError.RolesRequired != null) {
info.Message = $"Access denied; roles required {string.Join("/", accessDeniedError.RolesRequired.Select(x => $"'{x}'"))}.";
} else if (accessDeniedError.PolicyRequired != null) {
info.Message = $"Access denied; policy required '{accessDeniedError.PolicyRequired}'.";
accessDeniedError.PolicyAuthorizationResult.ShouldNotBeNull();
accessDeniedError.PolicyAuthorizationResult.Succeeded.ShouldBeFalse();
} else {
info.Message = $"Access denied; authorization required.";
}
}
return info;
}
}
}