Per module authentication with returnUrl - c#

I need to have Forms Authentication for some modules and Stateless Authentication for others. With Forms Authentication, the login page needs to set a returnUrl.
We had this working with application-wide Forms Authentication (enabled for pipelines), but we're stuck trying to enable hybrid authentication.
Per https://github.com/NancyFx/Nancy/issues/2439 we have to enable the forms authentication in the module constructor:
public abstract class FormsAuthenticationModuleBase : NancyModule
{
protected FormsAuthenticationModuleBase(IUserMapper userMapper)
: this(userMapper, string.Empty)
{
}
protected FormsAuthenticationModuleBase(IUserMapper userMapper, string modulePath)
: base(modulePath)
{
var formsAuthConfiguration = new FormsAuthenticationConfiguration
{
RequiresSSL = false,
UserMapper = userMapper,
RedirectUrl = "/login?returnUrl=" + Context.Request.Headers["X-Original-URL"].FirstOrDefault()
};
// Enable calls RequiresAuthentication.
FormsAuthentication.Enable(this, formsAuthConfiguration);
}
}
The issue is that Context is null at construction time.
How can we enable Forms Authentication on a per module basis and set a RedirectUrl that includes a returnUrl?
(See also https://github.com/NancyFx/Nancy/wiki/Forms-authentication )
Update
On the advice of #khellang I tried adding a before hook:
Before.AddItemToStartOfPipeline(
ctx =>
{
if (!formsAuthConfiguration.RedirectUrl.Contains("?"))
{
formsAuthConfiguration.RedirectUrl +=
"?returnUrl=" + ctx.Request.Headers["X-Original-URL"].FirstOrDefault();
}
return null;
});
(With the original RedirectUrl = "/login")
This results in a 405 that I can't debug into. (Same for the Before += syntax.)
He also suggested moving everything into the hook, rather than storing the configuration and mutating it; that didn't work either.
Currently I'm using if/else logic in RequestStartup (as opposed to the above approach). That works.

Using if/else logic in RequestStartup works.

Related

Custom authorize attribute doesn't work after deploying to IIS

I have overridden the HandleUnauthorizedRequest method in my asp.net mvc application to ensure it sends a 401 response to unauthorized ajax calls instead of redirecting to login page. This works perfectly fine when I run it locally, but my overridden method doesn't get called once I deploy to IIS. The debug point doesn't hit my method at all and straight away gets redirected to the login page.
This is my code:
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
filterContext.Result = new JsonResult
{
Data = new
{
success = false,
resultMessage = "Errors"
},
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
filterContext.HttpContext.Response.End();
base.HandleUnauthorizedRequest(filterContext);
}
else
{
var url = HttpContext.Current.Request.Url.AbsoluteUri;
url = HttpUtility.UrlEncode(url);
filterContext.Result = new RedirectResult(ConfigurationManager.AppSettings["LoginUrl"] + "?ReturnUrl=" + url);
}
}
}
and I have the attribute [AjaxAuthorize] declared on top of my controller. What could be different once it's deployed to IIS?
Update:
Here's how I'm testing, it's very simple, doesn't even matter whether it's an ajax request or a simple page refresh after the login session has expired -
I deploy the site onto my local IIS
Login to the website, go to the home page - "/Home"
Right click on the "Logout" link, "Open in a new tab" - This ensures that the home page is still open on the current tab while
the session is logged out.
Refresh Home page. Now here, the debug point should hit my overridden HandleUnauthorizedRequest method and go through the
if/else condition and then redirect me to login page. But it
doesn't! it just simply redirects to login page straight away. I'm
thinking it's not even considering my custom authorize attribute.
When I run the site from visual studio however, everything works fine, the control enters the debug point in my overridden method and goes through the if/else condition.
When you deploy your web site to IIS, it will run under IIS integrated mode by default. This is usually the best option. But it also means that the HTTP request/response model isn't completely initialized during the authorization check. I suspect this is causing IsAjaxRequest() to always return false when your application is hosted on IIS.
Also, the default HandleUnauthorizedRequest implementation looks like this:
protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Returns HTTP 401 - see comment in HttpUnauthorizedResult.cs.
filterContext.Result = new HttpUnauthorizedResult();
}
Effectively, by calling base.HandleUnauthorizedRequest(context) you are overwriting the JsonResult instance that you are setting with the default HttpUnauthorizedResult instance.
There is a reason why these are called filters. They are meant for filtering requests that go into a piece of logic, not for actually executing that piece of logic. The handler (ActionResult derived class) is supposed to do the work.
To accomplish this, you need to build a separate handler so the logic that the filter executes waits until after HttpContext is fully initialized.
public class AjaxAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new AjaxHandler();
}
}
public class AjaxHandler : JsonResult
{
public override void ExecuteResult(ControllerContext context)
{
var httpContext = context.HttpContext;
var request = httpContext.Request;
var response = httpContext.Response;
if (request.IsAjaxRequest())
{
response.StatusCode = (int)HttpStatusCode.Unauthorized;
this.Data = new
{
success = false,
resultMessage = "Errors"
};
this.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
base.ExecuteResult(context);
}
else
{
var url = request.Url.AbsoluteUri;
url = HttpUtility.UrlEncode(url);
url = ConfigurationManager.AppSettings["LoginUrl"] + "?ReturnUrl=" + url;
var redirectResult = new RedirectResult(url);
redirectResult.ExecuteResult(context);
}
}
}
NOTE: The above code is untested. But this should get you moving in the right direction.

c# Identity Server Bad Request - Request Too Long

I have an odd issue that I am trying to track down.
If I deploy my client and Identity Server to Azure, using a self signed certificate then the code works.
I have now moved it to our UAT environment, where the identity server is configured to use a purchased certificate. This certificate has been provided for a single domain. identity.mydomain.com
The client has the password for this certificate so it can do what it needs to.
When I browse to the identity server I can log in to the admin section, so that is all running correctly. If I browse to the client, it redirects to the identity service where I can log in. But as soon as I log in, and am redirected back to my website, I get the following error;
Bad Request - Request Too Long
HTTP Error 400. The size of the request headers is too long.
Looking at the cookies, I can see a whole load of cookies created. I have deleted those and restarted, but I still have the same issue.
If I increase the size of the buffers by using.
<httpRuntime maxRequestLength="2097151" executionTimeout="2097151">
Then it works, but I am concerned that I am masking a problem rather than fixing it.
Has anyone else had to do this to get identity server to work on iis?
I've had this issue recently. The solution was to downgrade the used NuGet package Microsoft.Owin.Security.OpenIdConnect. I was using 3.0.1. You must downgrade to 3.0.0. This is an issue with Owin/Katana middleware. Descriptioin of the issue can be found here. Note that the page states how to fix the actual issue in the library. I haven't tried that, it could also work and is worth the try.
Note that you must clear your cookies the first time you redeploy with the fix in place. As temporary fix, you can always clear your cookies, and just visit the site again. At some point however, it will always stick bunch of nonce strings in the cookie. Similar issue can be found here.
What solved the problem for me was using AdamDotNet's Custom OpenIdConnectAuthenticationHandler to delete old nonce cookies.
public static class OpenIdConnectAuthenticationPatchedMiddlewareExtension
{
public static Owin.IAppBuilder UseOpenIdConnectAuthenticationPatched(this Owin.IAppBuilder app, Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions openIdConnectOptions)
{
if (app == null)
{
throw new System.ArgumentNullException("app");
}
if (openIdConnectOptions == null)
{
throw new System.ArgumentNullException("openIdConnectOptions");
}
System.Type type = typeof(OpenIdConnectAuthenticationPatchedMiddleware);
object[] objArray = new object[] { app, openIdConnectOptions };
return app.Use(type, objArray);
}
}
/// <summary>
/// Patched to fix the issue with too many nonce cookies described here: https://github.com/IdentityServer/IdentityServer3/issues/1124
/// Deletes all nonce cookies that weren't the current one
/// </summary>
public class OpenIdConnectAuthenticationPatchedMiddleware : OpenIdConnectAuthenticationMiddleware
{
private readonly Microsoft.Owin.Logging.ILogger _logger;
public OpenIdConnectAuthenticationPatchedMiddleware(Microsoft.Owin.OwinMiddleware next, Owin.IAppBuilder app, Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions options)
: base(next, app, options)
{
this._logger = Microsoft.Owin.Logging.AppBuilderLoggerExtensions.CreateLogger<OpenIdConnectAuthenticationPatchedMiddleware>(app);
}
protected override Microsoft.Owin.Security.Infrastructure.AuthenticationHandler<OpenIdConnectAuthenticationOptions> CreateHandler()
{
return new SawtoothOpenIdConnectAuthenticationHandler(_logger);
}
public class SawtoothOpenIdConnectAuthenticationHandler : OpenIdConnectAuthenticationHandler
{
public SawtoothOpenIdConnectAuthenticationHandler(Microsoft.Owin.Logging.ILogger logger)
: base(logger) { }
protected override void RememberNonce(OpenIdConnectMessage message, string nonce)
{
var oldNonces = Request.Cookies.Where(kvp => kvp.Key.StartsWith(OpenIdConnectAuthenticationDefaults.CookiePrefix + "nonce"));
if (oldNonces.Any())
{
Microsoft.Owin.CookieOptions cookieOptions = new Microsoft.Owin.CookieOptions
{
HttpOnly = true,
Secure = Request.IsSecure
};
foreach (KeyValuePair<string, string> oldNonce in oldNonces)
{
Response.Cookies.Delete(oldNonce.Key, cookieOptions);
}
}
base.RememberNonce(message, nonce);
}
}
}
And use:
app.UseOpenIdConnectAuthenticationPatched(new OpenIdConnectAuthenticationOptions(){...});
As detailed here:
https://github.com/IdentityServer/IdentityServer3/issues/1124#issuecomment-226519073
Just clearing cookies worked for me. It is the easiest answer to try first.

"An item with the same key has already been added." while adding "/&output=embed"

Implementing a MVC application in C# with Evernote API. I am using the AsyncOAuth.Evernote.Simple nuget package. Receiving and error of Refused to display in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN', when trying to navigate to URL that fires off the OAuth process.
There is an iframe that is surrounding my code (which can not be altered). After implementing the code an error is generated: "An item with the same key has already been added". This error occurs when requestToken is hit for the first time.
Below is my EvernoteProviderController.cs
public class EvernoteProviderController : Controller
{
// Initialize Oauth call, pulling values from web.config
EvernoteAuthorizer EvernoteAuthorizer = new EvernoteAuthorizer(ConfigurationManager.AppSettings["Evernote.Url"] + "&output=embed", ConfigurationManager.AppSettings["Evernote.Key"], ConfigurationManager.AppSettings["Evernote.Secret"]);
// This method makes the original call to Evernote to get a token so that the user can validate that they want to access this site.
public ActionResult Authorize(bool reauth = false)
{
// Allow for reauth
if (reauth)
SessionHelper.Clear();
// First of all, check to see if the user is already registered, in which case tell them that
if (SessionHelper.EvernoteCredentials != null)
return Redirect(Url.Action("AlreadyAuthorized"));
// Evernote will redirect the user to this URL once they have authorized your application
var callBackUrl = Request.Url.GetLeftPart(UriPartial.Authority) + Url.Action("ObtainTokenCredentials");
// Generate a request token - this needs to be persisted till the callback
var requestToken = EvernoteAuthorizer.GetRequestToken(callBackUrl);
// Persist the token
SessionHelper.RequestToken = requestToken;
// Redirect the user to Evernote so they can authorize the app
var callForwardUrl = EvernoteAuthorizer.BuildAuthorizeUrl(requestToken);
return Redirect(callForwardUrl);
}
// This action is the callback that Evernote will redirect to after the call to Authorize above
public ActionResult ObtainTokenCredentials(string oauth_verifier)
{
// Use the verifier to get all the user details we need and store them in EvernoteCredentials
var credentials = EvernoteAuthorizer.ParseAccessToken(oauth_verifier, SessionHelper.RequestToken);
if (credentials != null)
{
SessionHelper.EvernoteCredentials = credentials;
return Redirect(Url.Action("Authorized"));
}
else
{
return Redirect(Url.Action("Unauthorized"));
}
}
// Show the user if they are authorized
public ActionResult Authorized()
{
return View(SessionHelper.EvernoteCredentials);
}
public ActionResult Unauthorized()
{
return View();
}
//Redirects user if already authorized, then dump out the EvernoteCredentials object
public ActionResult AlreadyAuthorized()
{
return View(SessionHelper.EvernoteCredentials);
}
public ActionResult Settings()
{
return View();
}
}
Has anyone had this issue with iframes before or knows in what direction I should go? I am trying to embed my URL endpoint so I can get around the iframe error.
Solved the error.
A bit of back story:
The purpose of this application was to provide the OAuth page where a user can sign up which will generate a AuthToken and NotebookURL, (both are needed with Evernote API to pull read/write Notes - which is Evernote's object).
The previous behavior (before I changed it), was when a user clicked on the link - they will be redirected (in the same window) to the Evernote OAuth page.
This caused issues for me, because I had another wrapper around my code (iframe). So in non-technical terms, I had a iframe within an iframe within an iframe.
Workaround
Created a JavaScript code which would add an click event listener, which would then create a popup using window.open.
$("#btnStart").click(function () {
myWindow = window.open(baseUrl + "/EvernoteProvider/Authorize", '_blank', 'width=500,height=500, scrollbars=no,resizable=no');
myWindow.focus();
});

Adding custom properties for each request in Application Insights metrics

I d'like to add custom properties to metrics taken by Application Insights to each request of my app. For example, I want to add the user login and the tenant code, such as I can segment/group the metrics in the Azure portal.
The relevant doc page seems to be this one : Set default property values
But the example is for an event (i.e. gameTelemetry.TrackEvent("WinGame");), not for an HTTP request :
var context = new TelemetryContext();
context.Properties["Game"] = currentGame.Name;
var gameTelemetry = new TelemetryClient(context);
gameTelemetry.TrackEvent("WinGame");
My questions :
What is the relevant code for a request, as I have no specific code at this time (it seems to be automatically managed by the App Insights SDK) : Is just creating a TelemetryContext sufficient ? Should I create also a TelemetryClient and if so, should I link it to the current request ? How ?
Where should I put this code ? Is it ok in the Application_BeginRequest method of global.asax ?
It looks like adding new properties to existing request is possible using ITelemetryInitializer as mentioned here.
I created sample class as given below and added new property called "LoggedInUser" to request telemetry.
public class CustomTelemetry : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry == null) return;
requestTelemetry.Properties.Add("LoggedInUserName", "DummyUser");
}
}
Register this class at application start event.
Example below is carved out of sample MVC application I created
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
TelemetryConfiguration.Active.TelemetryInitializers
.Add(new CustomTelemetry());
}
}
Now you can see custom property "LoggedInUserName" is displayed under Custom group of request properties. (please refer screen grab below)
Appinsight with custom property
Related to the first question "how to add custom event to my request / what is the relevant code to a request", I think the main confusion here is related to the naming.
The first thing that we need to point out is that there are different kinds of information that we can capture with Application Insights:
Custom Event
Request
Exception
Trace
Page View
Dependency
Once we know this, we can say that TrackEvent is related to "Custom Events", as TrackRequest is related to Requests.
When we want to save a request, what we need to do is the following:
var request = new RequestTelemetry();
var client = new TelemetryClient();
request.Name = "My Request";
client.TrackRequest(request);
So let's imagine that your user login and tenant code both are strings. We could make a new request just to log this information using the following code:
public void LogUserNameAndTenant(string userName, string tenantCode)
{
var request = new RequestTelemetry();
request.Name = "My Request";
request.Context.Properties["User Name"] = userName;
request.Context.Properties["Tenant Code"] = tenantCode;
var client = new TelemetryClient();
client.TrackRequest(request);
}
Doing just a TelemetryContext will not be enough, because we need a way to send the information, and that's where the TelemetryClient gets in place.
I hope it helps.
You can use the static HttpContext.Current's Items dictionary as a short term (near stateless) storage space to deliver your custom property values into the default telemetry handler with a custom ITelemetryInitializer
Implement handler
class AppInsightCustomProps : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
// Is this a TrackRequest() ?
if (requestTelemetry == null) return;
var httpCtx = HttpContext.Current;
if (httpCtx != null)
{
var customPropVal = (string)httpCtx.Items["PerRequestMyCustomProp"];
if (!string.IsNullOrWhiteSpace(customPropVal))
{
requestTelemetry.Properties["MyCustomProp"] = customPropVal;
}
}
}
}
Hook it in. Put this inside Application_Start in global.asax.cs
TelemetryConfiguration.Active.TelemetryInitializers.Add(new AppInsightCustomProps());
Program the desired custom property, anywhere in your request pipeline have something like
if (HttpContext.Current != null)
{
HttpContext.Current.Items["PerRequestMyCustomProp"] = myCustomPropValue;
}
As mentioned by Alan, you could implement the IContextInitializer interface to add custom properties to ALL telemetry sent by Application Insights. However, I would also suggest looking into the ITelemtryInitializer interface. It is very similar to the context initializer, but it is called for every piece of telemetry sent rather than only at the creation of a telemetry client. This seems more useful to me for logging property values that might change over the lifetime of your app such as user and tenant related info like you had mentioned.
I hope that helps you out. Here is a blog post with an example of using the ITelemetryInitializer.
In that doc, scroll down a few lines to where it talks about creating an implementation of IContextInitializer. You can call that in any method that will be called before telemetry starts rolling.
Your custom properties will be added to all events, exceptions, metrics, requests, everything.

Redirect users with suspended accounts without creating redirect loop

I have a subscription based MVC 2 application with the basic .NET Membership service in place (underneath some custom components to manage the account/subscription, etc). Users whose accounts have lapsed, or who have manually suspended their accounts, need to be able to get to a single view in the system that manages the status of their account. The controller driving that view is protected using the [Authorize] attribute.
I want to ensure that no other views in the system can be accessed until the user has re-activated their account. In my base controller (from which all my protected controllers derive) I tried modifying the OnActionExecuting method to intercept the action, check for a suspended account, and if it's suspended, redirect to the single view that manages the account status. But this puts me in an infinite loop. When the new action is hit, OnActionExecuting gets called again, and the cycle keeps going.
I don't really want to extend the [Authorize] attribute, but can if need be.
Any other thoughts on how to do this at the controller level?
EDIT: in the base controller, I was managing the redirect (that subsequently created the redirect loop) by modifying the filterContext.Result property, setting it to the RedirectToAction result of my view in question. I noticed everytime the loop occurs, filterContext.Result == null. Perhaps I should be checking against a different part of filterContext?
Ok, so here's my solution in case it helps anyone else. There's got to be a more elegant way to do this, and I'm all ears if anyone has a better idea.
In my BaseController.cs:
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
ViewData["CurrentUser"] = CurrentUser; // this is a public property in the BaseController
if (CurrentUser != null && CurrentUser.Account.Status != AccountStatus.Active)
{
// if the account is disabled and they are authenticated, we need to allow them
// to get to the account settings screen where they can re-activate, as well as the logoff
// action. Everything else should be disabled.
string[] actionWhiteList = new string[] {
Url.Action("Edit", "AccountSettings", new { id = CurrentUser.Account.Id, section = "billing" }),
Url.Action("Logoff", "Account")
};
var allowAccess = false;
foreach (string url in actionWhiteList)
{
// compare each of the whitelisted paths to the raw url from the Request context.
if (url == filterContext.HttpContext.Request.RawUrl)
{
allowAccess = true;
break;
}
}
if (!allowAccess)
{
filterContext.Result = RedirectToAction("Edit", "AccountSettings", new { id = CurrentUser.Account.Id, section = "billing" });
}
}
base.OnActionExecuting(filterContext);
}

Categories