Browse Source

Backport authorization validation support to OpenIddict 1.x

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

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

@ -33,9 +33,6 @@ namespace OpenIddict.Validation
public override async Task DecryptToken([NotNull] DecryptTokenContext context) public override async Task DecryptToken([NotNull] DecryptTokenContext context)
{ {
var options = (OpenIddictValidationOptions) context.Options; var options = (OpenIddictValidationOptions) context.Options;
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictValidationProvider>>();
if (options.UseReferenceTokens) if (options.UseReferenceTokens)
{ {
// Note: the token manager is deliberately not injected using constructor injection // Note: the token manager is deliberately not injected using constructor injection
@ -49,6 +46,8 @@ namespace OpenIddict.Validation
.ToString()); .ToString());
} }
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictValidationProvider>>();
// Retrieve the token entry from the database. If it // Retrieve the token entry from the database. If it
// cannot be found, assume the token is not valid. // cannot be found, assume the token is not valid.
var token = await manager.FindByReferenceIdAsync(context.Token); var token = await manager.FindByReferenceIdAsync(context.Token);
@ -102,8 +101,41 @@ namespace OpenIddict.Validation
=> context.HttpContext.RequestServices.GetRequiredService<IOpenIddictValidationEventService>() => context.HttpContext.RequestServices.GetRequiredService<IOpenIddictValidationEventService>()
.PublishAsync(new OpenIddictValidationEvents.RetrieveToken(context)); .PublishAsync(new OpenIddictValidationEvents.RetrieveToken(context));
public override Task ValidateToken([NotNull] ValidateTokenContext context) public override async Task ValidateToken([NotNull] ValidateTokenContext context)
=> context.HttpContext.RequestServices.GetRequiredService<IOpenIddictValidationEventService>() {
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 logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictValidationProvider>>();
var identifier = context.Ticket.Properties.GetProperty(OpenIddictConstants.Properties.InternalAuthorizationId);
if (!string.IsNullOrEmpty(identifier))
{
var authorization = await manager.FindByIdAsync(identifier);
if (authorization == null || !await manager.IsValidAsync(authorization))
{
logger.LogError("Authentication failed because the authorization " +
"associated with the access token was not longer valid.");
context.Ticket = null;
return;
}
}
}
await context.HttpContext.RequestServices.GetRequiredService<IOpenIddictValidationEventService>()
.PublishAsync(new OpenIddictValidationEvents.ValidateToken(context)); .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)); 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> /// <summary>
/// Configures OpenIddict not to return the authentication error /// Configures OpenIddict not to return the authentication error
/// details as part of the standard WWW-Authenticate response header. /// details as part of the standard WWW-Authenticate response header.

6
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -21,6 +21,12 @@ namespace OpenIddict.Validation
Events = new OpenIddictValidationProvider(); Events = new 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> /// <summary>
/// Gets or sets a boolean indicating whether reference tokens are used. /// Gets or sets a boolean indicating whether reference tokens are used.
/// </summary> /// </summary>

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

@ -15,6 +15,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OAuth.Validation; using AspNet.Security.OAuth.Validation;
using AspNet.Security.OpenIdConnect.Extensions;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@ -247,6 +248,233 @@ namespace OpenIddict.Validation.Tests
format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once()); 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),
new AuthenticationProperties(),
OpenIddictValidationDefaults.AuthenticationScheme);
});
var server = CreateResourceServer(builder =>
{
foreach (var service in builder.Services.ToArray())
{
if (service.ServiceType == typeof(IOpenIddictAuthorizationManager))
{
builder.Services.Remove(service);
}
}
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),
new AuthenticationProperties(),
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),
new AuthenticationProperties(),
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),
new AuthenticationProperties(),
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) private static TestServer CreateResourceServer(Action<OpenIddictValidationBuilder> configuration = null)
{ {
var builder = new WebHostBuilder(); var builder = new WebHostBuilder();
@ -259,6 +487,7 @@ namespace OpenIddict.Validation.Tests
services.AddOpenIddict() services.AddOpenIddict()
.AddCore(options => .AddCore(options =>
{ {
options.SetDefaultAuthorizationEntity<OpenIddictAuthorization>();
options.SetDefaultTokenEntity<OpenIddictToken>(); options.SetDefaultTokenEntity<OpenIddictToken>();
options.Services.AddSingleton(CreateTokenManager()); options.Services.AddSingleton(CreateTokenManager());
}) })
@ -334,6 +563,19 @@ namespace OpenIddict.Validation.Tests
return new TestServer(builder); 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<IOptions<OpenIddictCoreOptions>>());
configuration?.Invoke(manager);
return manager.Object;
}
private static OpenIddictTokenManager<OpenIddictToken> CreateTokenManager( private static OpenIddictTokenManager<OpenIddictToken> CreateTokenManager(
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null) Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null)
{ {
@ -347,6 +589,8 @@ namespace OpenIddict.Validation.Tests
return manager.Object; return manager.Object;
} }
public class OpenIddictAuthorization { }
public class OpenIddictToken { } 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); 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] [Fact]
public void RemoveErrorDetails_IncludeErrorDetailsIsSetToFalse() public void RemoveErrorDetails_IncludeErrorDetailsIsSetToFalse()
{ {

Loading…
Cancel
Save