Skip to content
This repository was archived by the owner on Dec 24, 2020. It is now read-only.

Commit 22eaf3a

Browse files
committed
Introduce a new DecryptToken event in the validation handler
1 parent b372084 commit 22eaf3a

File tree

8 files changed

+347
-2
lines changed

8 files changed

+347
-2
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
4+
* concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using JetBrains.Annotations;
8+
using Microsoft.AspNetCore.Authentication;
9+
using Microsoft.AspNetCore.Http;
10+
11+
namespace AspNet.Security.OAuth.Validation
12+
{
13+
/// <summary>
14+
/// Allows custom decryption of access tokens.
15+
/// </summary>
16+
public class DecryptTokenContext : BaseControlContext
17+
{
18+
public DecryptTokenContext(
19+
[NotNull] HttpContext context,
20+
[NotNull] OAuthValidationOptions options,
21+
[NotNull] string token)
22+
: base(context)
23+
{
24+
Options = options;
25+
Token = token;
26+
}
27+
28+
/// <summary>
29+
/// Gets the options used by the validation middleware.
30+
/// </summary>
31+
public OAuthValidationOptions Options { get; }
32+
33+
/// <summary>
34+
/// Gets the access token.
35+
/// </summary>
36+
public string Token { get; }
37+
38+
/// <summary>
39+
/// Gets or sets the data format used to deserialize the authentication ticket.
40+
/// </summary>
41+
public ISecureDataFormat<AuthenticationTicket> DataFormat { get; set; }
42+
}
43+
}

src/AspNet.Security.OAuth.Validation/OAuthValidationEvents.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ public class OAuthValidationEvents
2424
/// </summary>
2525
public Func<CreateTicketContext, Task> OnCreateTicket { get; set; } = context => Task.FromResult(0);
2626

27+
/// <summary>
28+
/// Invoked when a token is to be decrypted.
29+
/// </summary>
30+
public Func<DecryptTokenContext, Task> OnDecryptToken { get; set; } = context => Task.FromResult(0);
31+
2732
/// <summary>
2833
/// Invoked when a token is to be parsed from a newly-received request.
2934
/// </summary>
@@ -44,6 +49,11 @@ public class OAuthValidationEvents
4449
/// </summary>
4550
public virtual Task CreateTicket(CreateTicketContext context) => OnCreateTicket(context);
4651

52+
/// <summary>
53+
/// Invoked when a token is to be decrypted.
54+
/// </summary>
55+
public virtual Task DecryptToken(DecryptTokenContext context) => OnDecryptToken(context);
56+
4757
/// <summary>
4858
/// Invoked when a token is to be parsed from a newly-received request.
4959
/// </summary>

src/AspNet.Security.OAuth.Validation/OAuthValidationHandler.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,9 +370,43 @@ private bool ValidateAudience(AuthenticationTicket ticket)
370370
return false;
371371
}
372372

373+
private async Task<AuthenticationTicket> DecryptTokenAsync(string token)
374+
{
375+
var notification = new DecryptTokenContext(Context, Options, token)
376+
{
377+
DataFormat = Options.AccessTokenFormat
378+
};
379+
380+
await Options.Events.DecryptToken(notification);
381+
382+
if (notification.HandledResponse)
383+
{
384+
// If no ticket has been provided, return a failed result to
385+
// indicate that authentication was rejected by application code.
386+
if (notification.Ticket == null)
387+
{
388+
return null;
389+
}
390+
391+
return notification.Ticket;
392+
}
393+
394+
else if (notification.Skipped)
395+
{
396+
return null;
397+
}
398+
399+
if (notification.DataFormat == null)
400+
{
401+
throw new InvalidOperationException("A data formatter must be provided.");
402+
}
403+
404+
return notification.DataFormat.Unprotect(token);
405+
}
406+
373407
private async Task<AuthenticationTicket> CreateTicketAsync(string token)
374408
{
375-
var ticket = Options.AccessTokenFormat.Unprotect(token);
409+
var ticket = await DecryptTokenAsync(token);
376410
if (ticket == null)
377411
{
378412
return null;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions for more information
4+
* concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using JetBrains.Annotations;
8+
using Microsoft.Owin;
9+
using Microsoft.Owin.Security;
10+
using Microsoft.Owin.Security.Notifications;
11+
12+
namespace Owin.Security.OAuth.Validation
13+
{
14+
/// <summary>
15+
/// Allows custom decryption of access tokens.
16+
/// </summary>
17+
public class DecryptTokenContext : BaseNotification<OAuthValidationOptions>
18+
{
19+
public DecryptTokenContext(
20+
[NotNull] IOwinContext context,
21+
[NotNull] OAuthValidationOptions options,
22+
[NotNull] string token)
23+
: base(context, options)
24+
{
25+
Token = token;
26+
}
27+
28+
/// <summary>
29+
/// Gets the access token.
30+
/// </summary>
31+
public string Token { get; }
32+
33+
/// <summary>
34+
/// Gets or sets the data format used to deserialize the authentication ticket.
35+
/// </summary>
36+
public ISecureDataFormat<AuthenticationTicket> DataFormat { get; set; }
37+
38+
/// <summary>
39+
/// Gets or sets the <see cref="AuthenticationTicket"/>
40+
/// extracted from the access token.
41+
/// </summary>
42+
public AuthenticationTicket Ticket { get; set; }
43+
}
44+
}

src/Owin.Security.OAuth.Validation/OAuthValidationEvents.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ public class OAuthValidationEvents
2424
/// </summary>
2525
public Func<CreateTicketContext, Task> OnCreateTicket { get; set; } = context => Task.FromResult(0);
2626

27+
/// <summary>
28+
/// Invoked when a token is to be decrypted.
29+
/// </summary>
30+
public Func<DecryptTokenContext, Task> OnDecryptToken { get; set; } = context => Task.FromResult(0);
31+
2732
/// <summary>
2833
/// Invoked when a token is to be parsed from a newly-received request.
2934
/// </summary>
@@ -44,6 +49,11 @@ public class OAuthValidationEvents
4449
/// </summary>
4550
public virtual Task CreateTicket(CreateTicketContext context) => OnCreateTicket(context);
4651

52+
/// <summary>
53+
/// Invoked when a token is to be decrypted.
54+
/// </summary>
55+
public virtual Task DecryptToken(DecryptTokenContext context) => OnDecryptToken(context);
56+
4757
/// <summary>
4858
/// Invoked when a token is to be parsed from a newly-received request.
4959
/// </summary>

src/Owin.Security.OAuth.Validation/OAuthValidationHandler.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,9 +359,43 @@ private bool ValidateAudience(AuthenticationTicket ticket)
359359
return false;
360360
}
361361

362+
private async Task<AuthenticationTicket> DecryptTokenAsync(string token)
363+
{
364+
var notification = new DecryptTokenContext(Context, Options, token)
365+
{
366+
DataFormat = Options.AccessTokenFormat
367+
};
368+
369+
await Options.Events.DecryptToken(notification);
370+
371+
if (notification.HandledResponse)
372+
{
373+
// If no ticket has been provided, return a failed result to
374+
// indicate that authentication was rejected by application code.
375+
if (notification.Ticket == null)
376+
{
377+
return null;
378+
}
379+
380+
return notification.Ticket;
381+
}
382+
383+
else if (notification.Skipped)
384+
{
385+
return null;
386+
}
387+
388+
if (notification.DataFormat == null)
389+
{
390+
throw new InvalidOperationException("A data formatter must be provided.");
391+
}
392+
393+
return notification.DataFormat.Unprotect(token);
394+
}
395+
362396
private async Task<AuthenticationTicket> CreateTicketAsync(string token)
363397
{
364-
var ticket = Options.AccessTokenFormat.Unprotect(token);
398+
var ticket = await DecryptTokenAsync(token);
365399
if (ticket == null)
366400
{
367401
return null;

test/AspNet.Security.OAuth.Validation.Tests/OAuthValidationHandlerTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,93 @@ public async Task HandleAuthenticateAsync_ReplacedTicketAndHandleResponseFromRec
400400
Assert.Equal("Fabrikam", await response.Content.ReadAsStringAsync());
401401
}
402402

403+
[Fact]
404+
public async Task HandleAuthenticateAsync_HandleResponseFromFromDecryptTokenCauseInvalidAuthentication()
405+
{
406+
// Arrange
407+
var server = CreateResourceServer(options =>
408+
{
409+
options.Events.OnDecryptToken = context =>
410+
{
411+
context.HandleResponse();
412+
413+
return Task.FromResult(0);
414+
};
415+
});
416+
417+
var client = server.CreateClient();
418+
419+
var request = new HttpRequestMessage(HttpMethod.Get, "/");
420+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
421+
422+
// Act
423+
var response = await client.SendAsync(request);
424+
425+
// Assert
426+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
427+
}
428+
429+
[Fact]
430+
public async Task HandleAuthenticateAsync_SkipToNextMiddlewareFromFromDecryptTokenCauseInvalidAuthentication()
431+
{
432+
// Arrange
433+
var server = CreateResourceServer(options =>
434+
{
435+
options.Events.OnDecryptToken = context =>
436+
{
437+
context.SkipToNextMiddleware();
438+
439+
return Task.FromResult(0);
440+
};
441+
});
442+
443+
var client = server.CreateClient();
444+
445+
var request = new HttpRequestMessage(HttpMethod.Get, "/");
446+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
447+
448+
// Act
449+
var response = await client.SendAsync(request);
450+
451+
// Assert
452+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
453+
}
454+
455+
[Fact]
456+
public async Task HandleAuthenticateAsync_TicketAndHandleResponseFromFromDecryptTokenCauseSuccessfulAuthentication()
457+
{
458+
// Arrange
459+
var server = CreateResourceServer(options =>
460+
{
461+
options.Events.OnDecryptToken = context =>
462+
{
463+
var identity = new ClaimsIdentity(OAuthValidationDefaults.AuthenticationScheme);
464+
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Contoso"));
465+
466+
context.Ticket = new AuthenticationTicket(
467+
new ClaimsPrincipal(identity),
468+
new AuthenticationProperties(),
469+
context.Options.AuthenticationScheme);
470+
471+
context.HandleResponse();
472+
473+
return Task.FromResult(0);
474+
};
475+
});
476+
477+
var client = server.CreateClient();
478+
479+
var request = new HttpRequestMessage(HttpMethod.Get, "/");
480+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
481+
482+
// Act
483+
var response = await client.SendAsync(request);
484+
485+
// Assert
486+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
487+
Assert.Equal("Contoso", await response.Content.ReadAsStringAsync());
488+
}
489+
403490
[Fact]
404491
public async Task HandleAuthenticateAsync_SkipToNextMiddlewareFromValidateTokenCausesInvalidAuthentication()
405492
{

0 commit comments

Comments
 (0)