Browse Source

Introduce a new option allowing to validate the authorization associated with an access token

pull/637/head
Kévin Chalet 8 years ago
parent
commit
8601156578
  1. 34
      src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs
  2. 9
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  3. 6
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  4. 234
      test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs
  5. 16
      test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs

34
src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs

@ -97,7 +97,37 @@ namespace OpenIddict.Validation
public override Task RetrieveToken([NotNull] RetrieveTokenContext context)
=> _eventService.PublishAsync(new OpenIddictValidationEvents.RetrieveToken(context));
public override Task ValidateToken([NotNull] ValidateTokenContext context)
=> _eventService.PublishAsync(new OpenIddictValidationEvents.ValidateToken(context));
public override async Task ValidateToken([NotNull] ValidateTokenContext context)
{
var options = (OpenIddictValidationOptions) context.Options;
if (options.EnableAuthorizationValidation)
{
// Note: the authorization manager is deliberately not injected using constructor injection
// to allow using the validation handler without having to register the OpenIddict core services.
var manager = context.HttpContext.RequestServices.GetService<IOpenIddictAuthorizationManager>();
if (manager == null)
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling authorization validation.")
.Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.")
.ToString());
}
var identifier = context.Properties.GetProperty(OpenIddictConstants.Properties.InternalAuthorizationId);
if (!string.IsNullOrEmpty(identifier))
{
var authorization = await manager.FindByIdAsync(identifier);
if (authorization == null || !await manager.IsValidAsync(authorization))
{
context.Fail("Authentication failed because the authorization " +
"associated with the access token was not longer valid.");
return;
}
}
}
await _eventService.PublishAsync(new OpenIddictValidationEvents.ValidateToken(context));
}
}
}

9
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -172,6 +172,15 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.Audiences.UnionWith(audiences));
}
/// <summary>
/// Enables authorization validation so that a database call is made for each API request
/// to ensure the authorization associated with the access token is still valid.
/// Note: enabling this option may have an impact on performance.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder EnableAuthorizationValidation()
=> Configure(options => options.EnableAuthorizationValidation = true);
/// <summary>
/// Configures OpenIddict not to return the authentication error
/// details as part of the standard WWW-Authenticate response header.

6
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -22,6 +22,12 @@ namespace OpenIddict.Validation
EventsType = typeof(OpenIddictValidationProvider);
}
/// <summary>
/// Gets or sets a boolean indicating whether a database call is made
/// to validate the authorization associated with the received tokens.
/// </summary>
public bool EnableAuthorizationValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether reference tokens are used.
/// </summary>

234
test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs

@ -15,6 +15,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AspNet.Security.OAuth.Validation;
using AspNet.Security.OpenIdConnect.Extensions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
@ -239,6 +240,223 @@ namespace OpenIddict.Validation.Tests
format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once());
}
[Fact]
public async Task ValidateToken_ThrowsAnExceptionWhenAuthorizationManagerIsNotRegistered()
{
// Arrange
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("valid-token"))
.Returns(delegate
{
var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam"));
return new AuthenticationTicket(
new ClaimsPrincipal(identity),
OpenIddictValidationDefaults.AuthenticationScheme);
});
var server = CreateResourceServer(builder =>
{
builder.Services.RemoveAll(typeof(IOpenIddictAuthorizationManager));
builder.EnableAuthorizationValidation();
builder.Configure(options =>
{
options.AccessTokenFormat = format.Object;
options.UseReferenceTokens = false;
});
});
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate
{
return client.SendAsync(request);
});
Assert.Equal(new StringBuilder()
.AppendLine("The core services must be registered when enabling authorization validation.")
.Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.")
.ToString(), exception.Message);
}
[Fact]
public async Task ValidateToken_ReturnsFailedResultForUnknownAuthorization()
{
// Arrange
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("valid-token"))
.Returns(delegate
{
var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam"));
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
OpenIddictValidationDefaults.AuthenticationScheme);
ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620");
return ticket;
});
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
var server = CreateResourceServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.EnableAuthorizationValidation();
builder.Configure(options =>
{
options.AccessTokenFormat = format.Object;
options.UseReferenceTokens = false;
});
});
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateToken_ReturnsFailedResultForInvalidAuthorization()
{
// Arrange
var authorization = new OpenIddictAuthorization();
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("valid-token"))
.Returns(delegate
{
var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam"));
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
OpenIddictValidationDefaults.AuthenticationScheme);
ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620");
return ticket;
});
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
var server = CreateResourceServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.EnableAuthorizationValidation();
builder.Configure(options =>
{
options.AccessTokenFormat = format.Object;
options.UseReferenceTokens = false;
});
});
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateToken_ReturnsValidResultForValidAuthorization()
{
// Arrange
var authorization = new OpenIddictAuthorization();
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("valid-token"))
.Returns(delegate
{
var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam"));
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
OpenIddictValidationDefaults.AuthenticationScheme);
ticket.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, "5230CBAD-89F9-4C3F-B48C-9253B6FB8620");
return ticket;
});
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
var server = CreateResourceServer(builder =>
{
builder.Services.AddSingleton(manager);
builder.EnableAuthorizationValidation();
builder.Configure(options =>
{
options.AccessTokenFormat = format.Object;
options.UseReferenceTokens = false;
});
});
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-token");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("5230CBAD-89F9-4C3F-B48C-9253B6FB8620", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()), Times.Once());
}
private static TestServer CreateResourceServer(Action<OpenIddictValidationBuilder> configuration = null)
{
var builder = new WebHostBuilder();
@ -251,6 +469,7 @@ namespace OpenIddict.Validation.Tests
services.AddOpenIddict()
.AddCore(options =>
{
options.SetDefaultAuthorizationEntity<OpenIddictAuthorization>();
options.SetDefaultTokenEntity<OpenIddictToken>();
options.Services.AddSingleton(CreateTokenManager());
})
@ -320,6 +539,19 @@ namespace OpenIddict.Validation.Tests
return new TestServer(builder);
}
private static OpenIddictAuthorizationManager<OpenIddictAuthorization> CreateAuthorizationManager(
Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>> configuration = null)
{
var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>(
Mock.Of<IOpenIddictAuthorizationStoreResolver>(),
Mock.Of<ILogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>(),
Mock.Of<IOptionsMonitor<OpenIddictCoreOptions>>());
configuration?.Invoke(manager);
return manager.Object;
}
private static OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null)
{
@ -333,6 +565,8 @@ namespace OpenIddict.Validation.Tests
return manager.Object;
}
public class OpenIddictAuthorization { }
public class OpenIddictToken { }
}
}

16
test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs

@ -100,6 +100,22 @@ namespace OpenIddict.Validation.Tests
Assert.Equal(new[] { "Fabrikam", "Contoso" }, options.Audiences);
}
[Fact]
public void EnableAuthorizationValidation_ValidationIsEnforced()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.EnableAuthorizationValidation();
var options = GetOptions(services);
// Assert
Assert.True(options.EnableAuthorizationValidation);
}
[Fact]
public void RemoveErrorDetails_IncludeErrorDetailsIsSetToFalse()
{

Loading…
Cancel
Save