Skip to content

Commit

Permalink
[ODS-4562] CallContext loses values for a previous context when the s…
Browse files Browse the repository at this point in the history
…ame value is set for another context (#183)
  • Loading branch information
gmcelhanon authored and Jim McKay committed Oct 2, 2020
1 parent 9d505d9 commit 4283313
Show file tree
Hide file tree
Showing 16 changed files with 429 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public void Install(IWindsorContainer container, IConfigurationStore store)
{
RegisterDomainModel(container);
RegisterAuthenticationProvider(container);
RegisterBearerTokenHeaderProcessor(container);
RegisterContextProviders(container);
RegisterContextStorage(container);
RegisterCacheProvider(container);
Expand Down Expand Up @@ -270,6 +271,13 @@ private void RegisterAuthenticationProvider(IWindsorContainer container)
Component.For<IAuthenticationProvider>()
.ImplementedBy<OAuthAuthenticationProvider>());
}

private void RegisterBearerTokenHeaderProcessor(IWindsorContainer container)
{
container.Register(
Component.For<IBearerTokenHeaderProcessor>()
.ImplementedBy<BearerTokenHeaderProcessor>());
}

private void RegisterTokenRequestHandlers(IWindsorContainer container)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Net.Http.Formatting;
using System.Web.Http;
using EdFi.Ods.Api.Architecture;
using EdFi.Ods.Api.Services.Authentication;
using EdFi.Ods.Api.Services.Filters;
using EdFi.Ods.Common;
using EdFi.Ods.Common.Metadata;
Expand All @@ -16,12 +17,16 @@ namespace EdFi.Ods.Api.Startup.HttpConfigurators
{
public class ProfilesHttpConfigurator : IHttpConfigurator
{
private readonly IBearerTokenHeaderProcessor _bearerTokenHeaderProcessor;
private readonly IApiKeyContextProvider _apiKeyContextProvider;
private readonly IProfileResourceNamesProvider _profileResourceNamesProvider;

public ProfilesHttpConfigurator(IApiKeyContextProvider apiKeyContextProvider,
IProfileResourceNamesProvider profileResourceNamesProvider)
public ProfilesHttpConfigurator(
IApiKeyContextProvider apiKeyContextProvider,
IProfileResourceNamesProvider profileResourceNamesProvider,
IBearerTokenHeaderProcessor bearerTokenHeaderProcessor)
{
_bearerTokenHeaderProcessor = Preconditions.ThrowIfNull(bearerTokenHeaderProcessor, nameof(bearerTokenHeaderProcessor));
_apiKeyContextProvider = Preconditions.ThrowIfNull(apiKeyContextProvider, nameof(apiKeyContextProvider));

_profileResourceNamesProvider = Preconditions.ThrowIfNull(
Expand Down Expand Up @@ -56,7 +61,7 @@ private void ConfigureProfilesJsonSerializer(HttpConfiguration config)

private void ConfigureProfilesAuthorizationFilter(HttpConfiguration config)
{
config.Filters.Add(new ProfilesAuthorizationFilter(_apiKeyContextProvider, _profileResourceNamesProvider));
config.Filters.Add(new ProfilesAuthorizationFilter(_apiKeyContextProvider, _profileResourceNamesProvider, _bearerTokenHeaderProcessor));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using EdFi.Ods.Api.Services.Authorization;
using EdFi.Ods.Api.Services.Filters;
using EdFi.Ods.Common.Configuration;
using EdFi.Ods.Common.Extensions;

namespace EdFi.Ods.Api.Services.Authentication
{
public class BearerTokenHeaderProcessor : IBearerTokenHeaderProcessor
{
private const string AuthenticationScheme = "Bearer";

private readonly Lazy<bool?> _expectedUseSandboxValue;

private readonly IOAuthTokenValidator _oAuthTokenValidator;

public BearerTokenHeaderProcessor(IOAuthTokenValidator oAuthTokenValidator, IConfigValueProvider configValueProvider)
{
_oAuthTokenValidator = oAuthTokenValidator;

const string ExpectedUseSandboxValue = "ExpectedUseSandboxValue";

_expectedUseSandboxValue = new Lazy<bool?>(
() => configValueProvider.GetValue(ExpectedUseSandboxValue) == null
? (bool?) null
: Convert.ToBoolean(configValueProvider.GetValue(ExpectedUseSandboxValue)));
}

public async Task<BearerTokenProcessingResult> ProcessAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
AuthenticationHeaderValue authorization = request.Headers.Authorization;

// 2. If there are no credentials, do nothing.
if (authorization == null)
{
return new BearerTokenProcessingResult(new AuthenticationFailureResult("Missing credentials", request));
}

// 3. If there are credentials but the filter does not recognize the
// authentication scheme, do nothing.
if (!authorization.Scheme.EqualsIgnoreCase(AuthenticationScheme))
{
return new BearerTokenProcessingResult();
}

// 4. If there are credentials that the filter understands, try to validate them.
// 5. If the credentials are bad, set the error result.
if (string.IsNullOrEmpty(authorization.Parameter))
{
return new BearerTokenProcessingResult(new AuthenticationFailureResult("Missing parameter", request));
}

// Validate the token and get the corresponding API key details
var apiClientDetails = await _oAuthTokenValidator.GetClientDetailsForTokenAsync(authorization.Parameter);

if (!apiClientDetails.IsTokenValid)
{
return new BearerTokenProcessingResult(new AuthenticationFailureResult("Invalid token", request));
}

if (_expectedUseSandboxValue.Value.HasValue &&
apiClientDetails.IsSandboxClient != _expectedUseSandboxValue.Value.Value)
{
var message = apiClientDetails.IsSandboxClient
? "Sandbox credentials used in call to Production API"
: "Production credentials used in call to Sandbox API";

return new BearerTokenProcessingResult(new AuthenticationFailureResult(message, request));
}

return new BearerTokenProcessingResult(apiClientDetails);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using EdFi.Ods.Api.Services.Authorization;

namespace EdFi.Ods.Api.Services.Authentication
{
public interface IBearerTokenHeaderProcessor
{
/// <summary>
/// Processes the supplied HTTP request message for the authorization header containing the bearer token,
/// returning the associated API client details or the recommended error response.
/// </summary>
/// <param name="request">The request to be processed.</param>
/// <param name="cancellationToken">The cancellation token associated with the request.</param>
/// <returns>The result of the processing.</returns>
Task<BearerTokenProcessingResult> ProcessAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}

/// <summary>
/// Contains the results of bearer token processing with either API client details (when a valid token is present),
/// or an error (if an error response should be given), or neither (if the processing could not be performed).
/// </summary>
public class BearerTokenProcessingResult
{
/// <summary>
/// Creates a result that indicates that the bearer token could not be processed, but
/// no error response is indicated.
/// </summary>
public BearerTokenProcessingResult() { }

/// <summary>
/// Creates a result that indicates that the bearer token was valid and resolved to
/// the specified API client details.
/// </summary>
/// <param name="apiClientDetails"></param>
public BearerTokenProcessingResult(ApiClientDetails apiClientDetails)
{
ApiClientDetails = apiClientDetails;
}

/// <summary>
/// Creates a result that indicates an error occurred while processing the bearer token,
/// which should be returned as the result of explicit authentication processing.
/// </summary>
/// <param name="error"></param>
public BearerTokenProcessingResult(IHttpActionResult error)
{
Error = error;
}

/// <summary>
/// The API client details associated with the bearer token on the request.
/// </summary>
public ApiClientDetails ApiClientDetails { get; private set; }

/// <summary>
/// The error result for the response (to be used by client if relevant).
/// </summary>
public IHttpActionResult Error { get; private set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
using System.Web.Http.Results;
using EdFi.Ods.Api.Services.Authorization;
using EdFi.Ods.Api.Services.Filters;
using EdFi.Ods.Common.Configuration;
using EdFi.Ods.Common.Extensions;
using EdFi.Ods.Common.Security;
using EdFi.Ods.Common.Security.Claims;
Expand All @@ -25,25 +22,21 @@ namespace EdFi.Ods.Api.Services.Authentication
public class OAuthAuthenticationProvider : IAuthenticationProvider
{
private const string AuthenticationScheme = "Bearer";
private const string ExpectedUseSandboxValue = "ExpectedUseSandboxValue";
private readonly Lazy<bool?> _expectedUseSandboxValue;

public OAuthAuthenticationProvider(
IOAuthTokenValidator oauthTokenValidator,
IApiKeyContextProvider apiKeyContextProvider,
IClaimsIdentityProvider claimsIdentityProvider,
IConfigValueProvider configValueProvider)
IBearerTokenHeaderProcessor bearerTokenHeaderProcessor)
{
BearerTokenHeaderProcessor = bearerTokenHeaderProcessor;
OAuthTokenValidator = oauthTokenValidator;
ApiKeyContextProvider = apiKeyContextProvider;
ClaimsIdentityProvider = claimsIdentityProvider;

_expectedUseSandboxValue = new Lazy<bool?>(
() => configValueProvider.GetValue(ExpectedUseSandboxValue) == null
? (bool?) null
: Convert.ToBoolean(configValueProvider.GetValue(ExpectedUseSandboxValue)));
}

public IBearerTokenHeaderProcessor BearerTokenHeaderProcessor { get; set; }

public IOAuthTokenValidator OAuthTokenValidator { get; set; }

public IApiKeyContextProvider ApiKeyContextProvider { get; set; }
Expand All @@ -52,71 +45,43 @@ public OAuthAuthenticationProvider(

public async Task Authenticate(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;

// 2. If there are no credentials, do nothing.
if (authorization == null)
{
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return;
}

// 3. If there are credentials but the filter does not recognize the
// authentication scheme, do nothing.
if (!authorization.Scheme.EqualsIgnoreCase(AuthenticationScheme))
{
return;
}

// 4. If there are credentials that the filter understands, try to validate them.
// 5. If the credentials are bad, set the error result.
if (string.IsNullOrEmpty(authorization.Parameter))
// Don't reprocess bearer token, if API key context has already been set
if (!ApiKeyContextProvider.GetApiKeyContext().IsNullOrEmpty())
{
context.ErrorResult = new AuthenticationFailureResult("Missing parameter", request);
return;
}

var bearerTokenResult = await BearerTokenHeaderProcessor.ProcessAsync(context.Request, cancellationToken);

// Validate the token and get the corresponding API key details
var apiClientDetails = await OAuthTokenValidator.GetClientDetailsForTokenAsync(authorization.Parameter);

if (!apiClientDetails.IsTokenValid)
if (bearerTokenResult.Error != null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid token", request);
return;
context.ErrorResult = bearerTokenResult.Error;
}

if (_expectedUseSandboxValue.Value.HasValue &&
apiClientDetails.IsSandboxClient != _expectedUseSandboxValue.Value.Value)

if (bearerTokenResult.ApiClientDetails != null)
{
var message = apiClientDetails.IsSandboxClient
? "Sandbox credentials used in call to Production API"
: "Production credentials used in call to Sandbox API";

context.ErrorResult = new AuthenticationFailureResult(message, request);
return;
}

// Store API key details into context
ApiKeyContextProvider.SetApiKeyContext(
new ApiKeyContext(
apiClientDetails.ApiKey,
apiClientDetails.ClaimSetName,
var apiClientDetails = bearerTokenResult.ApiClientDetails;

// Store API key details into context
ApiKeyContextProvider.SetApiKeyContext(
new ApiKeyContext(
apiClientDetails.ApiKey,
apiClientDetails.ClaimSetName,
apiClientDetails.EducationOrganizationIds,
apiClientDetails.NamespacePrefixes,
apiClientDetails.Profiles,
apiClientDetails.StudentIdentificationSystemDescriptor,
apiClientDetails.CreatorOwnershipTokenId,
apiClientDetails.OwnershipTokenIds));

var claimsIdentity = ClaimsIdentityProvider.GetClaimsIdentity(
apiClientDetails.EducationOrganizationIds,
apiClientDetails.ClaimSetName,
apiClientDetails.NamespacePrefixes,
apiClientDetails.Profiles,
apiClientDetails.StudentIdentificationSystemDescriptor,
apiClientDetails.CreatorOwnershipTokenId,
apiClientDetails.OwnershipTokenIds));
apiClientDetails.Profiles.ToList());

var claimsIdentity = ClaimsIdentityProvider.GetClaimsIdentity(
apiClientDetails.EducationOrganizationIds,
apiClientDetails.ClaimSetName,
apiClientDetails.NamespacePrefixes,
apiClientDetails.Profiles.ToList());

context.Principal = new ClaimsPrincipal(claimsIdentity);
context.Principal = new ClaimsPrincipal(claimsIdentity);
}
}

public async Task Challenge(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
Expand Down
Loading

0 comments on commit 4283313

Please sign in to comment.