I'm developing an ASP.net WebAPI application with OAUTH 2.0 authentication with separated STS (token service) and custom JSON formatter (ServiceStack.Text).
I'm trying to customize the access denied object/message to make it homogeneous with the rest of error messages but i haven't found a way to change it.
I'm also thinking that in this case is used the default formatter.
Example:
{
"Message": "Authorization has been denied for this request."
}
Example result:
{
"message": "... insert error message here ...",
"details": null
}
Thanks in advance.
You can return a custom response for the current HttpActionContext using a class for which you can define its members.
public override void OnActionExecuting(HttpActionContext actionContext)
{
bool isAuthenticated = IsAuthenticated(actionContext);
if (!isAuthenticated)
{
actionContext.Response = actionExecutedContext.Request.CreateResponse<CustomActionResult>(HttpStatusCode.Unauthorized, new CustomActionResult
{
Message = "... insert error message here ...",
Details = null
});
}
}
}
public class CustomActionResult
{
public string Message { get; set; }
public string Details { get; set; }
}
To change all 'Access Denied' messages for your entire ASP.NET web site, you can create a HttpModule for all not authorized status returned from your site. In the HttpModule you can handle the EndRequest event and you can check the response.StatusCode if it is 401 you can change the message to anything you want. Like so:
public class AuthorizeMsgModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.EndRequest += OnApplicationEndRequest;
}
// If the request was unauthorized, modify the response sent to the user
private static void OnApplicationEndRequest(object sender, EventArgs e)
{
var response = HttpContext.Current.Response;
if (response.StatusCode == 401)
{
response.ClearContent();
response.Write("{\"message\": \"... insert error message here ...\",\"details\": null}");
}
}
public void Dispose()
{
}
}
Register your module in your web.config for example:
<modules runAllManagedModulesForAllRequests="true">
<add name="AuthorizeMsgModule" type="mynamespace.AuthorizeMsgModule, myassembly" />
</modules>
This will give you the content you write any time you return a 401 status. As you can see here in fiddler.
Related
For my ASP.NET MVC application I'm using ADFS authentication. It is set up in the following manner
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
MetadataAddress = ConfigurationManager.AppSettings.Get("MetadataAddress"),
Wtrealm = ConfigurationManager.AppSettings.Get("Realm"),
});
Due to something beyond my control, occasionally the metadata at the MetadataAddress is unreachable. In situations like that, I would like to redirect users to a custom view rather than the default error view. How would one accomplish this?
What I've ended up doing is creating Owin Middleware that captures the error that is thrown by invalid metadata that looks like the following and redirects the user to a route describing the issue:
public class LoggingMiddleware : OwinMiddleware
{
public LoggingMiddleware(OwinMiddleware next)
: base(next)
{
}
public async override Task Invoke(IOwinContext context)
{
try
{
await Next.Invoke(context);
} catch(Exception e)
{
Logger.Error($"Encountered error in authenticating {e.ToString()}");
if(e.Source.Equals("Microsoft.IdentityModel.Protocols"))
{
context.Response.Redirect("/Home/OwinError");
} else
{
throw e;
}
}
}
}
The middleware can simply be added in the startup.cs file with the following line:
app.Use(typeof(LoggingMiddleware));
I followed the following tutorial and set up my azure backend .
https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter2/custom/
I then installed postman for the first time and set :
http://apptest.azurewebsites.net/.auth/login/custom
{"username" : "adrian" , "password" : "supersecret"}
Json (application/json)
However , i keep getting this error :
405 Method not Allowed
{
"Message": "The requested resource does not support http method 'GET'."
}
Backend Code :
using System;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Security.Claims;
using System.Web.Http;
using Newtonsoft.Json;
using AppTestService.Models;
using AppTestService.DataObjects;
using Microsoft.Azure.Mobile.Server.Login;
namespace AppTestService.Controllers
{
[Route(".auth/login/custom")]
public class CustomAuthController : ApiController
{
private AppTestContext db;
private string signingKey, audience, issuer;
public CustomAuthController()
{
db = new AppTestContext();
signingKey = Environment.GetEnvironmentVariable("WEBSITE_AUTH_SIGNING_KEY");
var website = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME");
audience = $"https://{website}/";
issuer = $"https://{website}/";
}
[System.Web.Http.HttpPost]
public IHttpActionResult Post([FromBody] User body)
{
if (body == null || body.Username == null || body.Password == null ||
body.Username.Length == 0 || body.Password.Length == 0)
{
return BadRequest();
}
if (!IsValidUser(body))
{
return Unauthorized();
}
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, body.Username)
};
JwtSecurityToken token = AppServiceLoginHandler.CreateToken(
claims, signingKey, audience, issuer, TimeSpan.FromDays(30));
return Ok(new LoginResult()
{
AuthenticationToken = token.RawData,
User = new LoginResultUser { UserId = body.Username }
});
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool IsValidUser(User user)
{
return db.Users.Count(u => u.Username.Equals(user.Username) && u.Password.Equals(user.Password)) > 0;
}
}
public class LoginResult
{
[JsonProperty(PropertyName = "authenticationToken")]
public string AuthenticationToken { get; set; }
[JsonProperty(PropertyName = "user")]
public LoginResultUser User { get; set; }
}
public class LoginResultUser
{
[JsonProperty(PropertyName = "userId")]
public string UserId { get; set; }
}
}
If i add [System.Web.Http.HttpGet] on top of the function , then i get a different error. :
415 Unsupported media type
{
"Message": "The request contains an entity body but no Content-Type header. The inferred media type 'application/octet-stream' is not supported for this resource."
}
These are the headers :
Allow →POST
Content-Length →72
Content-Type →application/json; charset=utf-8
Date →Sat, 28 Jan 2017 22:08:48 GMT
Server →Microsoft-IIS/8.0
X-Powered-By →ASP.NET
You probably do want to make a POST request from PostMan instead of a GET. Don't add [HttpGet] on the action, just set the method to POST in PostMan.
And make sure you set the header Content-Type: application/json in PostMan.
Make sure in the headers of your request you are setting ContentType = "application/json" in postman or even when creating a request from any client.
Change your controller definition to
[RoutePrefix("auth/login/custom")]
public class CustomAuthController : ApiController
{
}
And just for test, introduce a route on POST like
[System.Web.Http.HttpPost]
[Route("post")]
public IHttpActionResult Post([FromBody] User body)
and try making a request to
http://apptest.azurewebsites.net/auth/login/custom/post
In Azure service api make sure it is with [HttpPost].
If you are able to send body param that means you have selected Post in postman while calling API which is correct. Dont change either of postman call or azure api to get. it will be mismatch.
If you have access to azure logs check if http Post is redirected to the https url as Get, in this case try calling https directly.
Azure logs looks as follows in this case:
Received request: POST http://xxx.azurewebsites.net/api/Data/test
Information Redirecting: https://xxx.azurewebsites.net/api/Data/test
Received request: GET https://xxx.azurewebsites.net/api/Data/test
in this case call https://xxx.azurewebsites.net/api/Data/test
I'm developing a Web API 2 application and I'm currently trying to format error resposnes in a uniform way (so that the consumer will also know what data object/structure they can inspect to get more info about the errors). This is what I've got so far:
{
"Errors":
[
{
"ErrorType":5003,
"Message":"Error summary here",
"DeveloperAction":"Some more detail for API consumers (in some cases)",
"HelpUrl":"link to the docs etc."
}
]
}
This works fine for exceptions thrown by the application itself (i.e inside controllers). However, if the user requests a bad URI (and gets a 404) or uses the wrong verb (and gets a 405) etc, Web Api 2 spits out a default error message e.g.
{
Message: "No HTTP resource was found that matches the request URI 'http://localhost/abc'."
}
Is there any way of trapping these kinds of errors (404, 405 etc.) and formatting them out into the error response in the first example above?
So far I've tried:
Custom ExceptionAttribute inherting ExceptionFilterAttribute
Custom ControllerActionInvoker inherting ApiControllerActionInvoker
IExceptionHandler (new Global Error Handling feature from Web API 2.1)
However, none of these approaches are able to catch these kinds of errors (404, 405 etc). Any ideas on how/if this can be achieved?
...or, am I going about this the wrong way? Should I only format error responses in my particular style for application/user level errors and rely on the default error responses for things like 404?
You can override the DelegatingHandler abstract class and intercept the response to the client. This will give you the ability to return what you want.
Here's some info on it.
http://msdn.microsoft.com/en-us/library/system.net.http.delegatinghandler(v=vs.118).aspx
Here's a poster of the Web Api pipeline that shows what can be overriden.
http://www.asp.net/posters/web-api/asp.net-web-api-poster.pdf
Create a Handler class like this to override the response
public class MessageHandler1 : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
Debug.WriteLine("Process request");
// Call the inner handler.
var response = base.SendAsync(request, cancellationToken);
Debug.WriteLine("Process response");
if (response.Result.StatusCode == HttpStatusCode.NotFound)
{
//Create new HttpResponseMessage message
}
;
return response;
}
}
In your WebApiConfig.cs class add the handler.
config.MessageHandlers.Add(new MessageHandler1());
UPDATE
As Kiran mentions in the comments you can use the OwinMiddleware to intercept the response going back to the client. This would work for MVC and Web Api running on any host.
Here's an example of how to get the response and change it as it goes to the client.
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Use(typeof(MyMiddleware));
}
}
public class MyMiddleware : OwinMiddleware
{
public MyMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
await Next.Invoke(context);
if(context.Response.StatusCode== 404)
{
context.Response.StatusCode = 403;
context.Response.ReasonPhrase = "Blah";
}
}
}
I have done in same way as #Dan H mentioned
public class ApiGatewayHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
var objectContent = response.Content as ObjectContent;
return await Task.FromResult(new ApiResult(HttpStatusCode.NotFound, VmsStatusCodes.RouteNotFound, "", objectContent == null ? null : objectContent.Value).Response());
}
return response;
}
catch (System.Exception ex)
{
return await Task.FromResult(new ApiResult(HttpStatusCode.BadRequest, VmsStatusCodes.UnHandledError, ex.Message, "").Response());
}
}
}
Added routing like below and now it hits the try catch for invalid url
config.Routes.MapHttpRoute(name: "DefaultApi",routeTemplate: "api/{controller}/{id}",defaults: new { id = RouteParameter.Optional });
config.Routes.MapHttpRoute(name: "NotFound", routeTemplate: "api/{*paths}", defaults: new { controller = "ApiError", action = "NotFound" });
I am trying to do an authenticated web api request that does not reset the authentication cookie timeout. In the MVC world I would accomplish this by removing the FormsAuthenication cookie from the responce:
Response.Cookies.Remove(System.Web.Security.FormsAuthentication.FormsCookieName);
In Web API 2 I wrote a custom IHttpActionResult, and I am removing the Set-Cookie header from the response. This is however, not removing the header, as I still see the Set-Cookie header when the auth cookie is being updated for the requests that use this action result.
Here is the custom IHttpActionResult:
public class NonAuthResetResult<T> : IHttpActionResult where T: class
{
private HttpRequestMessage _request;
private T _body;
public NonAuthResetResult(HttpRequestMessage request, T body)
{
_request = request;
_body = body;
}
public string Message { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var msg = _request.CreateResponse(_body);
msg.Headers.Remove("Set-Cookie");
return Task.FromResult(msg);
}
}
How do I edit the response header in Web API 2, because this is not working.
Global.asax can remove cookies in the Application_EndRequest event. And you can set a variable to be later picked up by Application_EndRequest.
Step 1. Create an action filter which sets a variable in Context.Items:
public class NoResponseCookieAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
System.Web.HttpContext.Current.Items.Add("remove-auth-cookie", "true");
}
}
Step 2. Handle the Application_EndRequest event in your global.asax file. If the variable from Step 1 is present, remove the cookie.
protected void Application_EndRequest()
{
if (HttpContext.Current.Items["remove-auth-cookie"] != null)
{
Context.Response.Cookies.Remove(System.Web.Security.FormsAuthentication.FormsCookieName);
}
}
Step 3. Decorate your web api actions with the custom filter:
[NoResponseCookie]
public IHttpActionResult GetTypes()
{
// your code here
}
If you're using Web API 2, you're probably using the OWIN Cookie Middleware. What you are describing sounds like you want to disable the sliding expiry window on the auth cookie.
In the standard Web API template, you should have an App_Start/Startup.Auth.cs. In it you'll find the line...
app.UseCookieAuthentication(new CookieAuthenticationOptions());
This enables and configures the cookie middleware. You can pass in some options to change the timeout window and disable sliding expiry...
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
SlidingExpiration = false,
ExpireTimeSpan = new TimeSpan(1, 0, 0) // 1 hour
});
I have a http module which redirects to a website when a user is not authorised. This website then checks for credentials and returns the user to the original page based on a query string.
The problem I have is that Request.Url.AbsoluteUri seems to omit default.aspx whenever a root directory is requested, e.g. http://example/application/
This behaviour can be observed using the test case below. When using Response.Redirect within Application_AuthenticateRequest
Please note, the VS web development server Cassini behaves normally and will correctly redirect to http://example/application/?url=http://example/application/default.aspx I presume this is related to IIS processing the request differently. (I'm running IIS6)
namespace HI.Test {
public class Authentication : IHttpModule {
private HttpRequest Request { get { return HttpContext.Current.Request; } }
private HttpResponse Response { get { return HttpContext.Current.Response; } }
private Cache Cache { get { return HttpContext.Current.Cache; } }
public void Init(HttpApplication application) {
application.AuthenticateRequest += (new EventHandler(Application_AuthenticateRequest));
}
private void Application_AuthenticateRequest(Object source, EventArgs e) {
if (Request.QueryString["url"] == null) {
Cache.Insert("URLRedirected", Request.Url.AbsoluteUri);
Response.Redirect(Request.Url.AbsoluteUri + "?url=" + Request.Url.AbsoluteUri);
}
}
public void Dispose() {
}
}
}
I'm obviously looking for a fix to the problem and also I'd like to understand why this happens.