I have one DelegatingHandler to verify an ApiKey included in the Request Header:
public class ApiKeyHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (!ApiKey.VerifyRequest(request))
{
var response = request.CreateErrorResponse(HttpStatusCode.Forbidden, "Invalid Api Key");
var tsc = new TaskCompletionSource<HttpResponseMessage>();
tsc.SetResult(response);
return tsc.Task;
}
return base.SendAsync(request, cancellationToken);
}
}
Now I want to extend it to check the origin of the request, because this APIKey is only required for external requests (CORS has not be required yet).
External is defined by Domain and its exclusively Server to Server.
I found the Is_Local Property but this doesnt work for me.
I'm now going with the IP Address:
((dynamic)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
Since you seem to be needing an IP of the caller, WebApiContrib has a good extension method that works on both self host and web host:
public static class HttpRequestMessageExtensions
{
private const string HttpContext = "MS_HttpContext";
private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty";
public static string GetClientIpAddress(this HttpRequestMessage request)
{
if (request.Properties.ContainsKey(HttpContext))
{
dynamic ctx = request.Properties[HttpContext];
if (ctx != null)
{
return ctx.Request.UserHostAddress;
}
}
if (request.Properties.ContainsKey(RemoteEndpointMessage))
{
dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage];
if (remoteEndpoint != null)
{
return remoteEndpoint.Address;
}
}
return null;
}
}
Related
I am building a new .Net 6 WebAPI that will be consumed by many applications so I need to implement API Keys to limit access to only those applications. Only a very small amount of the individual users will require authorization (admins) so I would like to combine with JWT for the Admin endpoints. We do not want to require users to have to crate an account where not necessary (non-admins). Is this possible? Thank You.
Yes it is possible.
The solution I recommend is to setup multiple authentication methods in asp.net core 6 using two authentication schemes that you have to specify inside Authorize attribute.
Here a simple implementation of ApiKey authentication:
namespace MyAuthentication;
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private enum AuthenticationFailureReason
{
NONE = 0,
API_KEY_HEADER_NOT_PROVIDED,
API_KEY_HEADER_VALUE_NULL,
API_KEY_INVALID
}
private readonly Microsoft.Extensions.Logging.ILogger _logger;
private AuthenticationFailureReason _failureReason = AuthenticationFailureReason.NONE;
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory loggerFactory,
ILogger<ApiKeyAuthenticationHandler> logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, loggerFactory, encoder, clock)
{
_logger = logger;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//ApiKey header get
if (!TryGetApiKeyHeader(out string providedApiKey, out AuthenticateResult authenticateResult))
{
return authenticateResult;
}
//TODO: you apikey validity check
if (await ApiKeyCheckAsync(providedApiKey))
{
var principal = new ClaimsPrincipal(); //TODO: Create your Identity retreiving claims
var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationOptions.Scheme);
return AuthenticateResult.Success(ticket);
}
_failureReason = AuthenticationFailureReason.API_KEY_INVALID;
return AuthenticateResult.NoResult();
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
//Create response
Response.Headers.Append(HeaderNames.WWWAuthenticate, $#"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
Response.StatusCode = StatusCodes.Status401Unauthorized;
Response.ContentType = MediaTypeNames.Application.Json;
//TODO: setup a response to provide additional information if you want
var result = new
{
StatusCode = Response.StatusCode,
Message = _failureReason switch
{
AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED => "ApiKey not provided",
AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL => "ApiKey value is null",
AuthenticationFailureReason.NONE or AuthenticationFailureReason.API_KEY_INVALID or _ => "ApiKey is not valid"
}
};
using var responseStream = new MemoryStream();
await JsonSerializer.SerializeAsync(responseStream, result);
await Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
//Create response
Response.Headers.Append(HeaderNames.WWWAuthenticate, $#"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
Response.StatusCode = StatusCodes.Status403Forbidden;
Response.ContentType = MediaTypeNames.Application.Json;
var result = new
{
StatusCode = Response.StatusCode,
Message = "Forbidden"
};
using var responseStream = new MemoryStream();
await JsonSerializer.SerializeAsync(responseStream, result);
await Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
#region Privates
private bool TryGetApiKeyHeader(out string apiKeyHeaderValue, out AuthenticateResult result)
{
apiKeyHeaderValue = null;
if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKeyHeaderValues))
{
_logger.LogError("ApiKey header not provided");
_failureReason = AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED;
result = AuthenticateResult.Fail("ApiKey header not provided");
return false;
}
apiKeyHeaderValue = apiKeyHeaderValues.FirstOrDefault();
if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(apiKeyHeaderValue))
{
_logger.LogError("ApiKey header value null");
_failureReason = AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL;
result = AuthenticateResult.Fail("ApiKey header value null");
return false;
}
result = null;
return true;
}
private Task<bool> ApiKeyCheckAsync(string apiKey)
{
//TODO: setup your validation code...
return Task.FromResult<bool>(true);
}
#endregion
}
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";
public static string Scheme => DefaultScheme;
public static string AuthenticationType => DefaultScheme;
}
public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder, Action<ApiKeyAuthenticationOptions> options)
=> authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options);
}
Then register inside builder setup:
_ = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;
})
.AddApiKeySupport(options => { });
You have to also setup the standard JWT Bearer validation (I don't post it for the sake of brevity).
To protect your endpoint add the Authorize attribute like:
[Authorize(AuthenticationSchemes = ApiKeyAuthenticationOptions.DefaultScheme)] //ApiKey
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
//or..
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
//or..
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme},{ApiKeyAuthenticationOptions.DefaultScheme}" )] //ApiKey and Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
For me it is the best way so as to carry out the authorization check before the start of the application pipeline (fail fast) and to be able to create the user identity.
But if you don't need to put informations about the Api Key inside the ClaimsPrincipal and only check the validity of Api Key the simplest way to do that is:
Protect the "admin" actions with JWT auth (with Authorize attribute)
Setup and register a middleware to only check the Api Key in all actions
Here is an example:
public class SimpleApiKeyMiddleware
{
private static readonly string API_KEY_HEADER = "X-Api-Key";
private readonly RequestDelegate _next;
private readonly ILogger<SimpleApiKeyMiddleware> _logger;
public SimpleApiKeyMiddleware(RequestDelegate next, ILogger<SimpleApiKeyMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext httpContext)
{
//Get apikey header
if (!httpContext.Request.Headers.TryGetValue(API_KEY_HEADER, out var apiKey))
{
_logger.LogError("ApiKey not found inside request headers");
//Error and exit from asp.net core pipeline
await GenerateForbiddenResponse(httpContext, "ApiKey not found inside request headers");
}
else if (!await ApiKeyCheckAsync(apiKey))
{
_logger.LogError("ApiKey is not valid: {ApiKey}", apiKey);
//Error and exit from asp.net core pipeline
await GenerateForbiddenResponse(httpContext, "ApiKey not valid");
}
else
{
_logger.LogInformation("ApiKey validated: {ApiKey}", apiKey);
//Proceed with pipeline
await _next(httpContext);
}
}
private Task<bool> ApiKeyCheckAsync(string apiKey)
{
//TODO: setup your validation code...
return Task.FromResult<bool>(true);
}
private async Task GenerateForbiddenResponse(HttpContext context, string message)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = MediaTypeNames.Application.Json;
using var responseStream = new MemoryStream();
await System.Text.Json.JsonSerializer.SerializeAsync(responseStream, new
{
Status = StatusCodes.Status403Forbidden,
Message = message
});
await context.Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
}
Registration:
_ = app.UseMiddleware<ApiKeyMiddleware>(); //Register as first middleware to avoid other middleware execution before api key check
Usage:
//Admin: Jwt and Api Key check
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt and Api Key
[HttpGet]
public async Task<IActionResult> MyAdminApi()
{
//...omissis...
}
//Non Admin: Api Key check only
[HttpGet]
public async Task<IActionResult> MyNonAdminApi()
{
//...omissis...
}
Note: the middleware code above forces exit from pipeline returning an http result so as to stop next middleware execution. Also note that the asp.net core 6 pipeline executes Authorization first and then all the registered middlewares.
I added a consumer to observe routing slip events, but doesn't work as expected. RoutingSlipCompleted consumer is always triggered, RoutingSlipActivityCompleted and RoutingSlipActivityFaulted consumer are never triggered. This is my consumer code.
public abstract class RoutingSlipExecuteActivityResponseProxy<TRequest, TResponse, TFaultResponse> :
IConsumer<RoutingSlipActivityCompleted>,
IConsumer<RoutingSlipActivityFaulted>,
IConsumer<RoutingSlipCompleted>
where TRequest : class
where TResponse : class
where TFaultResponse : class
{
public abstract string ActivityName { get; }
public async Task Consume(ConsumeContext<RoutingSlipActivityCompleted> context)
{
if(context.Message.ActivityName!= ActivityName)
{
return;
}
var request = context.Message.GetVariable<TRequest>("Request");
var requestId = context.Message.GetVariable<Guid>("RequestId");
Uri responseAddress = null;
if (context.Message.Variables.ContainsKey("ResponseAddress"))
responseAddress = context.Message.GetVariable<Uri>("ResponseAddress");
if (responseAddress == null)
throw new ArgumentException($"The response address could not be found for the faulted routing slip: {context.Message.TrackingNumber}");
var endpoint = await context.GetResponseEndpoint<TResponse>(responseAddress, requestId).ConfigureAwait(false);
var response = await CreateResponseMessage(context, request);
await endpoint.Send(response).ConfigureAwait(false);
}
public async Task Consume(ConsumeContext<RoutingSlipActivityFaulted> context)
{
if (context.Message.ActivityName != ActivityName)
{
return;
}
var request = context.Message.GetVariable<TRequest>("Request");
var requestId = context.Message.GetVariable<Guid>("RequestId");
Uri faultAddress = null;
if (context.Message.Variables.ContainsKey("FaultAddress"))
faultAddress = context.Message.GetVariable<Uri>("FaultAddress");
if (faultAddress == null && context.Message.Variables.ContainsKey("ResponseAddress"))
faultAddress = context.Message.GetVariable<Uri>("ResponseAddress");
if (faultAddress == null)
throw new ArgumentException($"The fault/response address could not be found for the faulted routing slip: {context.Message.TrackingNumber}");
var endpoint = await context.GetFaultEndpoint<TResponse>(faultAddress, requestId).ConfigureAwait(false);
var response = await CreateFaultedResponseMessage(context, request, requestId);
await endpoint.Send(response).ConfigureAwait(false);
}
protected abstract Task<TResponse> CreateResponseMessage(ConsumeContext<RoutingSlipActivityCompleted> context, TRequest request);
protected abstract Task<TFaultResponse> CreateFaultedResponseMessage(ConsumeContext<RoutingSlipActivityFaulted> context, TRequest request, Guid requestId);
public Task Consume(ConsumeContext<RoutingSlipCompleted> context)
{
throw new NotImplementedException();
}
}
My activity has no additional configuration, basically it is written according to the documentation.
You might want to check out this sample, which uses the RequestResponseProxy to handle a request via routing slip, and then generates the response based upon the RoutingSlipCompleted/RoutingSlipFaulted events.
I've implemented OAuth authentication in my Web Api project based on these blog posts
It works well, including the refresh token logic.
I want to add an option for basic authentication as well for a couple of calls for scheduled jobs.
I've tried adding a Basic Auth solution as middleware but I'm still getting 401 asking for the Bearer token.
I can get it to work by removing the [Authorize] attribute from those api calls and checking manually in code if the user is authenticated but seems like the wrong way to solve it.
Is there a way to support both Basic Auth and OAuth authentication using OWin?
How about you attribute your actions or controller with you want to implement Basic authentication with the attribute [OverrideAuthentication] Then you create custom authentication filter attribute which inherits from Attribute, IAuthenticationFilter as the code below
public class BasicAuthenticationAttribute : Attribute, IAuthenticationFilter
{
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
var req = context.Request;
// Get credential from the Authorization header
//(if present) and authenticate
if (req.Headers.Authorization != null && "basic".Equals(req.Headers.Authorization.Scheme, StringComparison.OrdinalIgnoreCase))
{
var rawCreds = req.Headers.Authorization.Parameter;
var credArray = GetCredentials(rawCreds);
var clientId = credArray[0];
var secret = credArray[1];
if (ValidCredentials(clientId, secret))
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, clientId)
};
var identity = new ClaimsIdentity(claims, "Basic");
var principal = new ClaimsPrincipal(new[] { identity });
// The request message contains valid credential
context.Principal = principal;
}
else
{
context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request);
}
}
else
{
context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request);
}
return Task.FromResult(0);
}
private string[] GetCredentials(string rawCred)
{
var encoding = Encoding.GetEncoding("UTF-8");
var cred = encoding.GetString(Convert.FromBase64String(rawCred));
var credArray = cred.Split(':');
if (credArray.Length == 2)
{
return credArray;
}
else
{
return credArray = ":".Split(':');
}
}
private bool ValidCredentials(string clientId, string secret)
{
//compare the values from web.config
if (clientId == secret)
{
return true;
}
return false;
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context,CancellationToken cancellationToken)
{
context.Result = new ResultWithChallenge(context.Result);
return Task.FromResult(0);
}
public class ResultWithChallenge : IHttpActionResult
{
private readonly IHttpActionResult next;
public ResultWithChallenge(IHttpActionResult next)
{
this.next = next;
}
public async Task<HttpResponseMessage> ExecuteAsync( CancellationToken cancellationToken)
{
var response = await next.ExecuteAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Basic"));
}
return response;
}
}
public bool AllowMultiple
{
get { return false; }
}
}
Now you use it to attribute you controllers or actions as the code below:
[OverrideAuthentication]
[BasicAuthentication]
[Route("")]
public async Task<IHttpActionResult> Get()
{
}
Notice how we are creating claims identity and setting the Authentication scheme to Basic, you can put any claims you want here.
I have created the following action in my MVC Web API 2 controller:
[ResponseType(typeof(int))]
[RequireHttps]
public IHttpActionResult SaveLead(EcommerceLead lead)
{
}
But in my test app I am making a call to
http://localhost/api/savelead
And it is working. Is there any way to make the action to only work if it is called over https, ie return a 404 if it isn't or something?
If you are using RequireHttps from Mvc namespace, it will not work with Web API. You can write a simple filter for Web API yourself to enforce HTTPS. Since you are using Web API 2, create an authentication filter like this.
public class RequireHttpsAttribute : IAuthenticationFilter
{
public bool AllowMultiple
{
get { return true; }
}
public Task AuthenticateAsync(HttpAuthenticationContext context,
CancellationToken cancellationToken)
{
if (context.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
{
context.ActionContext.Response = new HttpResponseMessage(
System.Net.HttpStatusCode.Forbidden);
}
return Task.FromResult<object>(null);
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken)
{
return Task.FromResult<object>(null);
}
}
If you are using web api older version you can use Authoriztion filter.
public class RequireHttpsAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
{
ReasonPhrase = "HTTPS Required"
};
}
else
{
base.OnAuthorization(actionContext);
}
}
}
You can use Message Handler.
public class RequireHttpsHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
{
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
{
ReasonPhrase = "HTTPS Required"
});
}
return base.SendAsync(request, cancellationToken);
}
}
I'm having trouble figuring out how to implement an authorization filter in Web API using IAuthorizationFilter from System.Web.Http.Filters.
This is a simple filter I wrote to respond to all non-https requests with a 403 forbidden response:
public class HttpsFilter : IAuthorizationFilter {
public bool AllowMultiple {
get {
return false;
}
}
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync( HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation ) {
var request = actionContext.Request;
if ( request.RequestUri.Scheme != Uri.UriSchemeHttps ) {
HttpResponseMessage response = request.CreateResponse( HttpStatusCode.Forbidden );
response.Content = new StringContent( "<h1>HTTPS Required</h1>", Encoding.UTF8, "text/html" );
actionContext.Response = response;
return new Task<HttpResponseMessage>( delegate() {
return response;
} );
}
else
return continuation();
}
}
What I have written so far runs, but when I try to access the api over regular http, it just hangs and I never get a response.
For you scenario, you could simply derive from the "System.Web.Http.AuthorizeAttribute".
Example:
public class HttpsFilterAttribute : System.Web.Http.AuthorizeAttribute
{
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
//do something
}
}
You either need to save the task into a variable and call the task.Start() method before returning it, or use the Task<HttpResponseMessage>.Factory.StartNew(Action action) method to create the task.