This video is really nice and shows how to create Minimal APIs using .net 6:
https://www.youtube.com/watch?v=eRJFNGIsJEo
It is amazing how it uses dependency injection to get mostly everything that you need inside your endpoints. For example if I need the value of a custom header I would have this:
app.MapGet("/get-custom-header", ([FromHeader(Name = "User-Agent")] string data) =>
{
return $"User again is: {data}";
});
I can have another endpoint where I have access to the entire httpContext like this:
app.MapGet("/foo", (Microsoft.AspNetCore.Http.HttpContext c) =>
{
var path = c.Request.Path;
return path;
});
I can even register my own classes with this code: builder.Services.AddTransient<TheClassIWantToRegister>()
If I register my custom classes I will be able to create an instance of that class every time I need it on and endpoint (app.MapGet("...)
Anyways back to the question. When a user logs in I send him this:
{
"ApiKey": "1234",
"ExpirationDate": blabla bla
.....
}
The user must send the 1234 token to use the API. How can I avoid repeating my code like this:
app.MapGet("/getCustomers", ([FromHeader(Name = "API-KEY")] string apiToken) =>
{
// validate apiToken agains DB
if(validationPasses)
return Database.Customers.ToList();
else
// return unauthorized
});
I have tried creating a custom class RequiresApiTokenKey and registering that class as builder.Services.AddTransient<RequiresApiTokenKey>() so that my API knows how to create an instance of that class when needed but how can I access the current http context inside that class for example? How can I avoid having to repeat having to check if the header API-KEY header is valid in every method that requires it?
Gave this a test based on my comments.
This would call the method Invoke in the middleware on each request and you can do checks here.
Probably a better way would be to use the AuthenticationHandler. using this would mean you can attribute individual endpoints to have the API key check done instead of all incoming requests
But, I thought this was still useful, middleware can be used for anything you'd like to perform on every request
Using Middleware
Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
//our custom middleware extension to call UseMiddleware
app.UseAPIKeyCheckMiddleware();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/", () => "Hello World!");
app.Run();
APIKeyCheckMiddleware.cs
using Microsoft.Extensions.Primitives;
internal class APIKeyCheckMiddleware
{
private readonly RequestDelegate _next;
public APIKeyCheckMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
//we could inject here our database context to do checks against the db
if (httpContext.Request.Headers.TryGetValue("API-KEY", out StringValues value))
{
//do the checks on key
var apikey = value;
}
else
{
//return 403
httpContext.Response.StatusCode = 403;
}
await _next(httpContext);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class APIKeyCheckMiddlewareExtensions
{
public static IApplicationBuilder UseAPIKeyCheckMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<APIKeyCheckMiddleware>();
}
}
I used SmithMart's answer but had to change things in the Invoke method and used DI in the constructor.
Here's my version:
internal class ApiKeyCheckMiddleware
{
public static string ApiKeyHeaderName = "X-ApiKey";
private readonly RequestDelegate _next;
private readonly ILogger<ApiKeyCheckMiddleware> _logger;
private readonly IApiKeyService _apiKeyService;
public ApiKeyCheckMiddleware(RequestDelegate next, ILogger<ApiKeyCheckMiddleware> logger, IApiKeyService apiKeyService)
{
_next = next;
_logger = logger;
_apiKeyService = apiKeyService;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var request = httpContext.Request;
var hasApiKeyHeader = request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValue);
if (hasApiKeyHeader)
{
_logger.LogDebug("Found the header {ApiKeyHeader}. Starting API Key validation", ApiKeyHeaderName);
if (apiKeyValue.Count != 0 && !string.IsNullOrWhiteSpace(apiKeyValue))
{
if (Guid.TryParse(apiKeyValue, out Guid apiKey))
{
var allowed = await _apiKeyService.Validate(apiKey);
if (allowed)
{
_logger.LogDebug("Client successfully logged in with key {ApiKey}", apiKeyValue);
var apiKeyClaim = new Claim("ApiKey", apiKeyValue);
var allowedSiteIdsClaim = new Claim("SiteIds", string.Join(",", allowedSiteIds));
var principal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { apiKeyClaim, allowedSiteIdsClaim }, "ApiKey"));
httpContext.User = principal;
await _next(httpContext);
return;
}
}
_logger.LogWarning("Client with ApiKey {ApiKey} is not authorized", apiKeyValue);
}
else
{
_logger.LogWarning("{HeaderName} header found, but api key was null or empty", ApiKeyHeaderName);
}
}
else
{
_logger.LogWarning("No ApiKey header found.");
}
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
}
Related
I am implementing an API and as part of it I have setup a custom .Net Middleware service extension UseRequestLoggingModdlewareExtension() that it run between the following:
app.UseHttpsRedirection();
app.UseRequestLoggingModdlewareExtension();
app.UseRouting();
The code is simple, and just logs the output of the request into a custom table.
public async Task InvokeAsync(HttpContext httpContext)
{
var stopAccess = _keyManager.getKeyValue("stopImmediateAccess");
if (!Convert.ToBoolean(stopAccess))
{
await _next(httpContext);
var loggingLevel = _keyManager.getKeyValue("loggingLevel");
if (loggingLevel != null)
{
if (loggingLevel.ToLower() == "information")
{
var userIdClaim = httpContext.User.FindFirst("userid")?.Value;
int? userId = null;
if(userIdClaim != null)
{
userId = Int32.Parse(userIdClaim);
}
var logging = new ApiRequestLogging
{
userId = userId,
remoteIp = httpContext.Connection.RemoteIpAddress.ToString() == "::1" ? "localhost" : httpContext.Connection.RemoteIpAddress.ToString(),
userAgent = httpContext.Request.Headers["User-Agent"].ToString(),
requestMethod = httpContext.Request.Method,
requestUrl = httpContext.Request.Path,
queryString = httpContext.Request.QueryString.ToString(),
requestHeaders = String.Join(",", httpContext.Request.Headers),
responseCode = httpContext.Response.StatusCode,
responseHeaders = String.Join(",", httpContext.Response.Headers),
createdDt = DateTime.Now
};
_logging.LogApiRequest(logging);
}
}
}
}
Where I am struggling is with some errors regarding some issues with the DBContext.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
The error is appearing twice, at both lines where the _keyManager service is called. The keyManager service is simply doing the following:
public string getKeyValue(string keyName)
{
var value = _context.keyManagement.Where(k => k.keyName == keyName).Select(v => v.keyValue).FirstOrDefault();
return value;
}
I have a suspicion that it could be something to do with the 'await' and the aschronousness of the code, however I have tried multiple combinations and cannot seem to bypass this issue.
Do you implement the IMiddleware interface i.e. something like this RequestLoggingMiddleware: IMiddleware This will resolve your middleware through the IMiddlewareFactory like a scoped service and inject the other dependant services. Your middleware ctor should be something like RequestLoggingMiddleware(IKeyManagerService keyManager) This way the middleware will be activated per client request i.e. scoped and not in the normal way as a singleton. Providing scoped middleware instances per request will allow you to use short-lived ApplicationDbContext in the middleware or its dependent services:
public RequestLoggingMiddleware(ApplicationDbContext db)
{
_db = db;
}
or in your case more like
public class RequestLoggingMiddleware: IMiddleware
{
public RequestLoggingMiddleware(IKeyManagerService keyManager)
{
_keyManager = keyManager;
}
}
public KeyManagerService(ApplicationDbContext db)
{
_db = db;
}
services.AddScoped<IKeyManagerService, KeyManagerService>()
This way the ApplicationDbContext used by keyManager service will be created per request and disposed of after the request is completed. Of course, the IKeyManagerService should be registered as a scoped service as well.
Thats why i like to use IDisposable interface with DbContext.
public string getKeyValue(string keyName)
{
string value = null;
using(var _cnx = new DbContext())
{
value = _cnx.keyManagement.Where(k => k.keyName == keyName).Select(v => v.keyValue).FirstOrDefault();
}
return value;
}
I'm workin on a MVC Aps.Net Core project and I have this situation :
User A is loggedin device A, and user B is trying to login in device A. I allow to login the user B to device A without problem, but in that case I show pop-up message to user A that user A now is disconected from device A.
Evrithing works fine. I call WebApi where I have SQL function, and that function do all the job. The problem is that I have the same code(that call the WebApi) in each function on my project. So I was thinking to make custom middleware so in that way I don't need to replace that code in every function/method in my project.
This is what I tried :
public class Middleware
{
private readonly RequestDelegate _next;
string strBaseUrl = string.Empty;
string strMappaturaUrl = string.Empty;
private readonly IConfiguration config;
private readonly HttpClient client;
public Middleware(RequestDelegate next, IConfiguration _config, HttpClient _client)
{
_next = next;
client = _client;
config = _config;
strBaseUrl = config.GetValue<string>("AppSettings:BaseUrl");
strMappaturaUrl = config.GetValue<string>("AppSettings:MapUrl");
}
public async Task Invoke(HttpContext httpContext)
{
string returnErrore = string.Empty;
CheckUtenteDTO userRequest = new CheckUtenteDTO();
string strUrlApi = string.Empty;
string strContext = string.Empty;
try
{
userRequest.user = HttpContext.Session.GetString("nomeUten");
userRequest.macchina = HttpContext.Session.GetString("macchina");
//Here I call WebApi where I have SQL functio
strUrlApi = config.GetValue<string>("AppSettings:BaseUrl") + "/Users/CheckLoginUser";
string stringData = JsonConvert.SerializeObject(userRequest);
var contentData = new StringContent(stringData, System.Text.Encoding.UTF8, "application/json");
using (var responseMessage = await client.PostAsync(strUrlApi, contentData))
{
if (responseMessage.IsSuccessStatusCode)
{
strContext = await responseMessage.Content.ReadAsStringAsync();
var strReturn = JsonConvert.DeserializeObject<string>(strContext);
if (string.IsNullOrWhiteSpace(strReturn))
returnErrore = string.Empty;
else
throw new UtenteException(strReturn);
}
else
{
strContext = await responseMessage.Content.ReadAsStringAsync();
throw new Exception(strContext);
}
}
}
catch (UserException ex1)
{
returnErrore = ex1.Message.Trim();
HttpContext.Session.SetString("strErrore", ex1.Message);
HttpContext.Session.SetString("nextAction", "LogoutAfterCheck");
HttpContext.Session.SetString("nextController", "Login");
}
catch (Exception ex)
{
returnErrore = ex.Message.Trim();
HttpContext.Session.SetString("strErrore", ex.Message);
HttpContext.Session.SetString("nextAction", "Index");
HttpContext.Session.SetString("nextController", "HomeScreen");
}
return Json(returnErrore);
//return _next(httpContext);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<Middleware>();
}
}
}
And I get this errors .
Is it possible to do it in this way?
Any suggestions how to fix this?
Thanks in advance!
That looks like your middleware don't have access to HttpContext and thus the error. In such case, you can pass the context as a parameter to your middleware from your presentation layer where you have access to HttpContext and calling the Middleware function.
Essentially, you are using only the below two parameters from session .. then why not just extract them in your presentation layer and pass them as argument to the middleware function
userRequest.user = HttpContext.Session.GetString("nomeUten");
userRequest.macchina = HttpContext.Session.GetString("macchina");
Change your Invoke method signature to
Task Invoke(string user, string macchina) //assuming both are type string
I have REST API developed using asp.net web api2. I am migrating the REST API to GraphQL.net endpoints using asp.net core 2. In the existing REST API code I have a Delegating handler used to extend the result of REST API call with additional data which in this case is add localization data to the response.Since Delegating handler are no more supported in asp.net core 2. I am trying to migrate the existing Delegating handler to Middleware component.
For reference purpose I followed the details mentioned at : Extending WebApi response using OWIN Middleware and
https://www.devtrends.co.uk/blog/wrapping-asp.net-web-api-responses-for-consistency-and-to-provide-additional-information
Here I have couple of queries:
How to map the below code in case of Middlware ?
var response = await base.SendAsync(request, cancellationToken);
Where should I place the middleware in Startup.cs Configure method.
Middleware equivalent of the existing Delegating Handler
Code:
public class CommonResponserHandler : DelegatingHandler
{
ICommonService _commonService = new CommonService();
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string locale = string.Empty;
if (request.Headers.Contains("Accept-Language"))
{
locale = request.Headers.GetValues("Accept-Language").First();
}
bool initialAuthorizationStatus = GetInitialAuthorization(request);
var response = await base.SendAsync(request, cancellationToken);
APIResult commonResponse;
if (response.TryGetContentValue<APIResult>(out commonResponse))
{
//populate common response here;
UpdateCommonResponse(request, response, commonResponse);
//UpdateCommonResponse(basicResponse, commonResponse);
HttpResponseMessage newResponse;
bool authorizatinCheckResult = AssertAuthorization(initialAuthorizationStatus, request);
if (authorizatinCheckResult)
{
newResponse = request.CreateResponse(response.StatusCode, commonResponse);
}
else
{
var unAuthorisedResult = new APIResult{Authorized = false, UserMessage = Constants.Unauthorized, Locale = new Locale(_commonService.GetLanguageFromLocale(locale))};
newResponse = request.CreateResponse(HttpStatusCode.Unauthorized, unAuthorisedResult);
var jsonSerializerSettings = new JsonSerializerSettings{ContractResolver = new CamelCasePropertyNamesContractResolver()};
HttpContext.Current.Items["401message"] = JsonConvert.SerializeObject(unAuthorisedResult, Formatting.Indented, jsonSerializerSettings);
}
//Add headers from old response to new response
foreach (var header in response.Headers)
{
newResponse.Headers.Add(header.Key, header.Value);
}
return newResponse;
}
return response;
}
}
Can anyone help me to provide their guidance in resolving the issue?
Please read the ASP.NET Core Middleware documentation for a better understanding on how middlewares work.
The middleware takes in the next RequestDelegate in its constructor and supports an Invoke method .For example :
public class CommonResponserMiddleware
{
private readonly RequestDelegate _next;
public CommonResponserMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
//process context.Request
await _next.Invoke(context);
//process context.Response
}
}
public static class CommonResponserExtensions
{
public static IApplicationBuilder UseCommonResponser(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CommonResponserMiddleware>();
}
}
And use in Starup.cs:
public void Configure(IApplicationBuilder app) {
//...other configuration
app.UseCommonResponser();
//...other configuration
}
You can also refer to related SO question:
Registering a new DelegatingHandler in ASP.NET Core Web API
How can I wrap Web API responses(in .net core) for consistency?
I do not know if this is actually possible, but I think it' s worth a try to find out.
There are maybe other and better patterns (if you know one let me know, I will look them up) to do this, but I'm just curious to know if this is possible.
When you have to call an API you could do it directly from within the controller using the HttpClient like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: get some access token to use in api call
var accessToken = await HttpContext.GetTokenAsync("access_token");
//Example: do an API call direcly using a static HttpClient wrapt in a service
var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/api/some/endpoint");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _client.Client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var rederectUrl = "/account/login?returnUrl="+Request.Path;
return Redirect(rederectUrl);
}
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
return null;
}
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
When you would do this you'll have to reuse some code over and over again. You could just make a Repository but for some scenarios that would be overkill and you just want to make some quick and dirty API calls.
Now what I want to know is, when we move the logic of setting an Authorization header or handling the 401 and 403 responses outside the controller, how do you redirect or control the controller's action.
Lets say I create a Middleware for the HttpClient like this:
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
//Handle situation where user is not authenticated
var context = _contextAccessor.HttpContext;
var rederectUrl = "/account/login?returnUrl="+context.Request.Path;
context.Response.Redirect(rederectUrl); //not working
}
//When 403 user probably does not have authorization to use endpoint
if (response.StatusCode == HttpStatusCode.Forbidden)
{
//Handle situation where user is not authorized
}
return response;
}
}
We can just do the request like this:
[Authorize]
public async Task<IActionResult> Private()
{
//Example: do an API call direcly using a static HttpClient initiated with Middleware wrapt in a service
var response = await _client.Client.GetAsync("https://example.com/api/some/endpoint");
var text = await response.Content.ReadAsStringAsync();
Result result = JObject.Parse(text).ToObject<Result>();
return View(result);
}
The problem here is that context.Response.Redirect(rederectUrl); does not work. It does not break off the flow to redirect. Is it possible to implement this, and how would you solve this?
Ok since nobody answers my question I've thought about it thoroughly and I came up with the following:
Setup
We have a resource gateway (RG). The RG can return a 401 or 403 meaning that the session is expired (401) or the user does not have sufficient rights (403). We use an access token (AT) to authenticate and authorize our requests to the RG.
authentication
When we get a 401 and we have a refresh token (RT) we want to trigger something that will retrieve a new AT. When there is no RT or the RT is expired we want to reauthenticate the user.
authorization
When we get a 403 we want to show the user that he has no access or something similar like that.
Solution
To handle the above, without making it a hassle for the programmer that uses the API or API wrapper class we can use a Middleware that will specifically handle the Exception thrown by using the API or the API wrapper. The middleware can handle any of the above situations.
Create custom Exceptions
public class ApiAuthenticationException : Exception
{
public ApiAuthenticationException()
{
}
public ApiAuthenticationException(string message) : base(message)
{
}
}
public class ApiAuthorizationException : Exception
{
public ApiAuthorizationException()
{
}
public ApiAuthorizationException(string message) : base(message)
{
}
}
Throw exceptions
Create a wrapper or use the HttpClient middleware to manage the exception throwing.
public class ResourceGatewayMessageHandler : HttpClientHandler
{
private readonly IHttpContextAccessor _contextAccessor;
public ResourceGatewayMessageHandler(IHttpContextAccessor context)
{
_contextAccessor = context;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Retrieve acces token from token store
var accessToken = await _contextAccessor.HttpContext.GetTokenAsync("access_token");
//Add token to request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
//Execute request
var response = await base.SendAsync(request, cancellationToken);
//When 401 user is probably not logged in any more -> redirect to login screen
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new ApiAuthenticationException();
}
//When 403 user probably does not have authorization to use endpoint -> show error page
if (response.StatusCode == HttpStatusCode.Forbidden)
{
throw new ApiAuthorizationException();
}
return response;
}
}
Now you have to setup the HttpClient inside your Startup.cs. There are multiple ways to do this. I advise to use AddTransient to innitiate a wrapper class that uses a HttpClient as a static.
You could do it like this:
public class ResourceGatewayClient : IApiClient
{
private static HttpClient _client;
public HttpClient Client => _client;
public ResourceGatewayClient(IHttpContextAccessor contextAccessor)
{
if (_client == null)
{
_client = new HttpClient(new ResourceGatewayMessageHandler(contextAccessor));
//configurate default base address
_client.BaseAddress = "https://gateway.domain.com/api";
}
}
}
And in your Startup.cs inside the ConfigureServices(IServiceCollection services) you can do:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<ResourceGatewayClient>();
Now you can use the dependency injection in any controller you would like.
Handle the Exceptions
Create something like this middleware (with thanks to this answer):
public class ApiErrorMiddleWare
{
private readonly RequestDelegate next;
public ApiErrorMiddleWare(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
if (exception is ApiAuthenticationException)
{
context.Response.Redirect("/account/login");
}
if (exception is ApiAuthorizationException)
{
//handle not authorized
}
}
Register your middleware
Go to Startup.cs and go to the Configure(IApplicationBuilder app, IHostingEnvironment env) method and add app.UseMiddleware<ApiErrorMiddleWare>();.
This should do it. Currently, I'm creating an example when it is publicly available (after peer review) I'll add a github reference.
I would like to hear some feedback on this solution or an alternative approach.
I'm trying to implement OAuth Bearer Authentication with Owin. When an invalid or expired token is passed, the default implementation is to log this as a warning and just don't set an Identity. I however would like to reject the whole request with an error in this case. But how would I do this?
After digging through the code I found out that in OAuthBearerAuthenticationHandler it will parse the token using a fallback mechanism when the provided AuthenticationTokenProvider did not parse any ticket (like the default implementation). This handler will log a warning when the token could not be parsed to any ticket or when it expired.
But I can't find any place to plug in my own logic to what happens when the token is invalid or expired. I could theoretically check this on my own in the AuthenticationTokenProvider, but then I would have to reimplement the logic (= copy it over) for creating and reading the token. Also this seems just out of place, as this class seems to be only responsible for creating and parsing tokens. I also don't see a way to plug in my own implementation of the OAuthBearerAuthenticationHandler in the OAuthBearerAuthenticationMiddleware.
Apparently my best and cleanest shot would be to reimplement the whole middleware, but this also seems very overkill.
What do I overlook? How would I go on about this the best?
edit:
For clarification. I know by not setting an identity the request will be rejected with 401 Unauthorized later in the Web API. But I personally see this as really bad style, silently swallowing an erroneous access token without any notification. This way you don't get to know that your token is crap, you just get to know you're not authorized.
I had a similar issue, i think the answer is to late but someone will come here with a similar problem:
I used this nuget package for validate authentication, but i think any method can help: https://www.nuget.org/packages/WebApi.AuthenticationFilter. You can read its documentation in this site https://github.com/mbenford/WebApi-AuthenticationFilter
AuthenticationFilter.cs
public class AuthenticationFilter : AuthenticationFilterAttribute{
public override void OnAuthentication(HttpAuthenticationContext context)
{
System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
var ci = context.Principal.Identity as ClaimsIdentity;
//First of all we are going to check that the request has the required Authorization header. If not set the Error
var authHeader = context.Request.Headers.Authorization;
//Change "Bearer" for the needed schema
if (authHeader == null || authHeader.Scheme != "Bearer")
{
context.ErrorResult = context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
new { Error = new { Code = 401, Message = "Request require authorization" } });
}
//If the token has expired the property "IsAuthenticated" would be False, then set the error
else if (!ci.IsAuthenticated)
{
context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
new { Error = new { Code = 401, Message = "The Token has expired" } });
}
}}
AuthenticationFailureResult.cs
public class AuthenticationFailureResult : IHttpActionResult{
private object ResponseMessage;
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request, object responseMessage)
{
ReasonPhrase = reasonPhrase;
Request = request;
ResponseMessage = responseMessage;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
response.Content = new System.Net.Http.ObjectContent<object>(ResponseMessage, jsonFormatter);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}}
Response examples:
{"Error":{"Code":401,"Message":"Request require authorization"}}
{"Error":{"Code":401,"Message":"The Token has expired"}}
Fonts and inspiration documentation:
//github.com/mbenford/WebApi-AuthenticationFilter
//www.asp.net/web-api/overview/security/authentication-filters
Yeah, I did not find 'good' solution for this,
I also don't see a way to plug in my own implementation of the
OAuthBearerAuthenticationHandler in the
OAuthBearerAuthenticationMiddleware.
Apparently my best and cleanest shot would be to reimplement the whole
middleware, but this also seems very overkill.
Agreed, but that's what I did (before reading your post). I copy & pasted three owin classes, and made it so that it sets property in Owins context, which can be later checked by other handlers.
public static class OAuthBearerAuthenticationExtensions
{
public static IAppBuilder UseOAuthBearerAuthenticationExtended(this IAppBuilder app, OAuthBearerAuthenticationOptions options)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
app.Use(typeof(OAuthBearerAuthenticationMiddlewareExtended), app, options);
app.UseStageMarker(PipelineStage.Authenticate);
return app;
}
}
internal class OAuthBearerAuthenticationHandlerExtended : AuthenticationHandler<OAuthBearerAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly string _challenge;
public OAuthBearerAuthenticationHandlerExtended(ILogger logger, string challenge)
{
_logger = logger;
_challenge = challenge;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
try
{
// Find token in default location
string requestToken = null;
string authorization = Request.Headers.Get("Authorization");
if (!string.IsNullOrEmpty(authorization))
{
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
requestToken = authorization.Substring("Bearer ".Length).Trim();
}
}
// Give application opportunity to find from a different location, adjust, or reject token
var requestTokenContext = new OAuthRequestTokenContext(Context, requestToken);
await Options.Provider.RequestToken(requestTokenContext);
// If no token found, no further work possible
if (string.IsNullOrEmpty(requestTokenContext.Token))
{
return null;
}
// Call provider to process the token into data
var tokenReceiveContext = new AuthenticationTokenReceiveContext(
Context,
Options.AccessTokenFormat,
requestTokenContext.Token);
await Options.AccessTokenProvider.ReceiveAsync(tokenReceiveContext);
if (tokenReceiveContext.Ticket == null)
{
tokenReceiveContext.DeserializeTicket(tokenReceiveContext.Token);
}
AuthenticationTicket ticket = tokenReceiveContext.Ticket;
if (ticket == null)
{
_logger.WriteWarning("invalid bearer token received");
Context.Set("oauth.token_invalid", true);
return null;
}
// Validate expiration time if present
DateTimeOffset currentUtc = Options.SystemClock.UtcNow;
if (ticket.Properties.ExpiresUtc.HasValue &&
ticket.Properties.ExpiresUtc.Value < currentUtc)
{
_logger.WriteWarning("expired bearer token received");
Context.Set("oauth.token_expired", true);
return null;
}
// Give application final opportunity to override results
var context = new OAuthValidateIdentityContext(Context, Options, ticket);
if (ticket != null &&
ticket.Identity != null &&
ticket.Identity.IsAuthenticated)
{
// bearer token with identity starts validated
context.Validated();
}
if (Options.Provider != null)
{
await Options.Provider.ValidateIdentity(context);
}
if (!context.IsValidated)
{
return null;
}
// resulting identity values go back to caller
return context.Ticket;
}
catch (Exception ex)
{
_logger.WriteError("Authentication failed", ex);
return null;
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode != 401)
{
return Task.FromResult<object>(null);
}
AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
OAuthChallengeContext challengeContext = new OAuthChallengeContext(Context, _challenge);
Options.Provider.ApplyChallenge(challengeContext);
}
return Task.FromResult<object>(null);
}
}
public class OAuthBearerAuthenticationMiddlewareExtended : AuthenticationMiddleware<OAuthBearerAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly string _challenge;
/// <summary>
/// Bearer authentication component which is added to an OWIN pipeline. This constructor is not
/// called by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication
/// extension method.
///
/// </summary>
public OAuthBearerAuthenticationMiddlewareExtended(OwinMiddleware next, IAppBuilder app, OAuthBearerAuthenticationOptions options)
: base(next, options)
{
_logger = AppBuilderLoggerExtensions.CreateLogger<OAuthBearerAuthenticationMiddlewareExtended>(app);
_challenge = string.IsNullOrWhiteSpace(Options.Challenge) ? (!string.IsNullOrWhiteSpace(Options.Realm) ? "Bearer realm=\"" + this.Options.Realm + "\"" : "Bearer") : this.Options.Challenge;
if (Options.Provider == null)
Options.Provider = new OAuthBearerAuthenticationProvider();
if (Options.AccessTokenFormat == null)
Options.AccessTokenFormat = new TicketDataFormat(
Microsoft.Owin.Security.DataProtection.AppBuilderExtensions.CreateDataProtector(app, typeof(OAuthBearerAuthenticationMiddleware).Namespace, "Access_Token", "v1"));
if (Options.AccessTokenProvider != null)
return;
Options.AccessTokenProvider = new AuthenticationTokenProvider();
}
/// <summary>
/// Called by the AuthenticationMiddleware base class to create a per-request handler.
///
/// </summary>
///
/// <returns>
/// A new instance of the request handler
/// </returns>
protected override AuthenticationHandler<OAuthBearerAuthenticationOptions> CreateHandler()
{
return new OAuthBearerAuthenticationHandlerExtended(_logger, _challenge);
}
}
Then I wrote my own authorization filter, which will be applied globally:
public class AuthorizeAttributeExtended : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
var tokenHasExpired = false;
var owinContext = OwinHttpRequestMessageExtensions.GetOwinContext(actionContext.Request);
if (owinContext != null)
{
tokenHasExpired = owinContext.Environment.ContainsKey("oauth.token_expired");
}
if (tokenHasExpired)
{
actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
new
{
error = "invalid_token",
error_message = "The Token has expired"
});
}
else
{
actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
new
{
error = "invalid_request",
error_message = "The Token is invalid"
});
}
}
}
public class AuthenticationFailureMessage : HttpResponseMessage
{
public AuthenticationFailureMessage(string reasonPhrase, HttpRequestMessage request, object responseMessage)
: base(HttpStatusCode.Unauthorized)
{
MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
Content = new ObjectContent<object>(responseMessage, jsonFormatter);
RequestMessage = request;
ReasonPhrase = reasonPhrase;
}
}
my WebApiConfig:
config.Filters.Add(new AuthorizeAttributeExtended());
How my configureOAuth looks like:
public void ConfigureOAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
OAuthBearerOptions = new OAuthBearerAuthenticationOptions()
{
};
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider(),
AuthenticationMode = AuthenticationMode.Active
};
FacebookAuthOptions = new CustomFacebookAuthenticationOptions();
app.UseFacebookAuthentication(FacebookAuthOptions);
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthenticationExtended(OAuthBearerOptions);
}
I will try & get this to main branch of oAuth middleware, it seems like an obvious use case, unless I am missing something.
I came across this problem recently. We wanted to return a JSON message if the user's access token had expired, allowing the consumer web application to silently refresh the access token and re-issue the API request. We also didn't want to rely on the exceptions thrown for token lifetime validation.
Not wanting to re-implement any middleware, we specified the Provider option inside JwtBearerAuthenticationOptions and added a delegate to handle the OnRequestTokenMethod. The delegate checks to see if it can read the token passed to the middleware and sets a boolean inside the OWIN context if it's expired.
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = tokenValidationParameters,
Provider = new OAuthBearerAuthenticationProvider
{
OnRequestToken = (ctx) =>
{
if (!string.IsNullOrEmpty(ctx.Token))
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
if (handler.CanReadToken(ctx.Token))
{
JwtSecurityToken jwtToken = handler.ReadJwtToken(ctx.Token);
if (jwtToken.IsExpired())
ctx.OwinContext.Set<bool>("expiredToken", true);
}
}
return Task.CompletedTask;
}
}
});
For convenience I added a quick extension method to check if a JWT expired:
public static class JwtSecurityTokenExtensions
{
public static bool IsExpired (this JwtSecurityToken token)
{
if (DateTime.UtcNow > token.ValidTo.ToUniversalTime())
return true;
return false;
}
}
We ended up using a middleware to check on the state of that boolean:
app.Use((context, next) =>
{
bool expiredToken = context.Get<bool>("expiredToken");
if (expiredToken)
{
// do stuff
}
return next.Invoke();
});
app.UseStageMarker(PipelineStage.Authenticate);
Not exactly the most efficient code, since we're parsing the token again after the middleware already did and also introducing a new middleware to act on the result of the check, but it's a fresh perspective nonetheless.
If authentication fails (meaning the token is expired) then that layer doesn't set the user, as you said. It's up the the authorization layer (later on) to reject the call. So for your scenario your Web API would need to deny access to an anonymous caller. Use the [Authorize] authorization filter attribute.