I need some insight on the proper way to setup an ASP.NET MVC 3 web app where the users are logged in two ways:
They are part of the Domain (Windows Authentication)
The application's user table/store says they have access to the application
To handle the first requirement I am setting the authenication element to Windows in the web.config:
<authentication mode="Windows" />
The second one is the one that is a bit trickier. My current thought was to utilize the HttpApplications AuthenticationRequested event. So the code might look something like this:
void MvcApplication_AuthenticateRequest( object sender, EventArgs e )
{
var formsAuthTicket = GetFormsAuthTicket();
if ( formsAuthTicket == null || formsAuthTicket.Expired )
{
var userHasAccess = true; //TODO: Ask the someone if the user has access
if ( userHasAccess )
{
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket( 1,
HttpContext.Current.User.Identity.Name,
DateTime.Now,
DateTime.Now.AddMinutes( 1 ),
true,
HttpContext.Current.User.Identity.Name );
string encTicket = FormsAuthentication.Encrypt( authTicket );
this.Response.Cookies.Remove( FormsAuthentication.FormsCookieName );
this.Response.Cookies.Add( new HttpCookie( FormsAuthentication.FormsCookieName, encTicket ) );
}
else
throw new HttpException( 401, "User is not authorized" );
}
}
Any thoughts?
I think there is better way to implement it in ASP.NET MVC by creating global authorization filter
Related
I have a strange problem with using Owin cookie authentication.
When I start my IIS server authentication works perfectly fine on IE/Firefox and Chrome.
I started doing some testing with Authentication and logging in on different platforms and I have come up with a strange error. Sporadically the Owin framework / IIS just doesn't send any cookies to the browsers. I will type in a username and password which is correct the code runs but no cookie gets delivered to the browser at all. If I restart the server it starts working then at some point I will try login and again cookies stop getting delivered. Stepping over the code does nothing and throws no errors.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
CookieHttpOnly = true,
AuthenticationType = "ABC",
LoginPath = new PathString("/Account/Login"),
CookiePath = "/",
CookieName = "ABC",
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
And within my login procedure I have the following code:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var authentication = HttpContext.Current.GetOwinContext().Authentication;
var identity = new ClaimsIdentity("ABC");
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.User_ID.ToString()));
identity.AddClaim(new Claim(ClaimTypes.Role, role.myRole.ToString()));
authentication.AuthenticationResponseGrant =
new AuthenticationResponseGrant(identity, new AuthenticationProperties()
{
IsPersistent = isPersistent
});
authenticationManager.SignIn(new AuthenticationProperties() {IsPersistent = isPersistent}, identity);
Update 1: It seems that one cause of the problem is when I add items to session the problems start. Adding something simple like Session.Content["ABC"]= 123 seems to create the problem.
What I can make out is as follows:
1) (Chrome)When I login I get ASP.NET_SessionId + my authentication cookie.
2) I go to a page that sets a session.contents...
3) Open a new browser (Firefox) and try login and it does not receive an ASP.NET_SessionId nor does it get a Authentication Cookie
4) Whilst the first browser has the ASP.NET_SessionId it continues to work. The minute I remove this cookie it has the same problem as all the other browsers
I am working on ip address (10.x.x.x) and localhost.
Update 2: Force creation of ASPNET_SessionId first on my login_load page before authentication with OWIN.
1) before I authenticate with OWIN I make a random Session.Content value on my login page to start the ASP.NET_SessionId
2) then I authenticate and make further sessions
3) Other browsers seem to now work
This is bizarre. I can only conclude that this has something to do with ASP and OWIN thinking they are in different domains or something like that.
Update 3 - Strange behaviour between the two.
Additional strange behaviour identified - Timeout of Owin and ASP session is different. What I am seeing is that my Owin sessions are staying alive longer than my ASP sessions through some mechanism. So when logging in:
1.) I have a cookied based auth session
2.) I set a few session variables
My session variables(2) "die" before the owin cookie session variable forces re-login, which causes unexpected behaviour throughout my entire application. (Person is logged in but is not really logged in)
Update 3B
After some digging I saw some comments on a page that say the "forms" authentication timeout and session timeout need to match. I am thinking normally the two are in sync but for whatever reason the two are not in sync.
Summary of Workarounds
1) Always create a Session first before authentication. Basically create session when you start the application Session["Workaround"] = 0;
2) [Experimental] if you persist cookies make sure your OWIN timeout / length is longer than your sessionTimeout in your web.config (in testing)
I have encountered the same problem and traced the cause to OWIN ASP.NET hosting implementation. I would say it's a bug.
Some background
My findings are based on these assembly versions:
Microsoft.Owin, Version=2.0.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
Microsoft.Owin.Host.SystemWeb, Version=2.0.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
OWIN uses it's own abstraction to work with response Cookies (Microsoft.Owin.ResponseCookieCollection). This implementation directly wraps response headers collection and accordingly updates Set-Cookie header. OWIN ASP.NET host (Microsoft.Owin.Host.SystemWeb) just wraps System.Web.HttpResponse and it's headers collection. So when new cookie is created through OWIN, response Set-Cookie header is changed directly.
But ASP.NET also uses it's own abstraction to work with response Cookies. This is exposed to us as System.Web.HttpResponse.Cookies property and implemented by sealed class System.Web.HttpCookieCollection. This implementation does not wrap response Set-Cookie header directly but uses some optimizations and handful of internal notifications to manifest it's changed state to response object.
Then there is a point late in request lifetime where HttpCookieCollection changed state is tested (System.Web.HttpResponse.GenerateResponseHeadersForCookies()) and cookies are serialized to Set-Cookie header. If this collection is in some specific state, whole Set-Cookie header is first cleared and recreated from cookies stored in collection.
ASP.NET session implementation uses System.Web.HttpResponse.Cookies property to store it's ASP.NET_SessionId cookie. Also there is some basic optimization in ASP.NET session state module (System.Web.SessionState.SessionStateModule) implemented through static property named s_sessionEverSet which is quite self explanatory. If you ever store something to session state in your application, this module will do a little more work for each request.
Back to our login problem
With all these pieces your scenarios can be explained.
Case 1 - Session was never set
System.Web.SessionState.SessionStateModule, s_sessionEverSet property is false. No session id's are generated by session state module and System.Web.HttpResponse.Cookies collection state is not detected as changed. In this case OWIN cookies are sent correctly to the browser and login works.
Case 2 - Session was used somewhere in application, but not before user tries to authenticate
System.Web.SessionState.SessionStateModule, s_sessionEverSet property is true. Session Id's are generated by SessionStateModule, ASP.NET_SessionId is added to System.Web.HttpResponse.Cookies collection but it's removed later in request lifetime as user's session is in fact empty. In this case System.Web.HttpResponse.Cookies collection state is detected as changed and Set-Cookie header is first cleared before cookies are serialized to header value.
In this case OWIN response cookies are "lost" and user is not authenticated and is redirected back to login page.
Case 3 - Session is used before user tries to authenticate
System.Web.SessionState.SessionStateModule, s_sessionEverSet property is true. Session Id's are generated by SessionStateModule, ASP.NET_SessionId is added to System.Web.HttpResponse.Cookies. Due to internal optimization in System.Web.HttpCookieCollection and System.Web.HttpResponse.GenerateResponseHeadersForCookies() Set-Cookie header is NOT first cleared but only updated.
In this case both OWIN authentication cookies and ASP.NET_SessionId cookie are sent in response and login works.
More general problem with cookies
As you can see the problem is more general and not limited to ASP.NET session. If you are hosting OWIN through Microsoft.Owin.Host.SystemWeb and you/something is directly using System.Web.HttpResponse.Cookies collection you are at risk.
For example this works and both cookies are correctly sent to browser...
public ActionResult Index()
{
HttpContext.GetOwinContext()
.Response.Cookies.Append("OwinCookie", "SomeValue");
HttpContext.Response.Cookies["ASPCookie"].Value = "SomeValue";
return View();
}
But this does not and OwinCookie is "lost"...
public ActionResult Index()
{
HttpContext.GetOwinContext()
.Response.Cookies.Append("OwinCookie", "SomeValue");
HttpContext.Response.Cookies["ASPCookie"].Value = "SomeValue";
HttpContext.Response.Cookies.Remove("ASPCookie");
return View();
}
Both tested from VS2013, IISExpress and default MVC project template.
In short, the .NET cookie manager will win over the OWIN cookie manager and overwrite cookies set on the OWIN layer. The fix is to use the SystemWebCookieManager class, provided as a solution on the Katana Project here. You need to use this class or one similar to it, which will force OWIN to use the .NET cookie manager so there are no inconsistencies:
public class SystemWebCookieManager : ICookieManager
{
public string GetRequestCookie(IOwinContext context, string key)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
var cookie = webContext.Request.Cookies[key];
return cookie == null ? null : cookie.Value;
}
public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
bool expiresHasValue = options.Expires.HasValue;
var cookie = new HttpCookie(key, value);
if (domainHasValue)
{
cookie.Domain = options.Domain;
}
if (pathHasValue)
{
cookie.Path = options.Path;
}
if (expiresHasValue)
{
cookie.Expires = options.Expires.Value;
}
if (options.Secure)
{
cookie.Secure = true;
}
if (options.HttpOnly)
{
cookie.HttpOnly = true;
}
webContext.Response.AppendCookie(cookie);
}
public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
AppendResponseCookie(
context,
key,
string.Empty,
new CookieOptions
{
Path = options.Path,
Domain = options.Domain,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}
}
In your application startup, just assign it when you create your OWIN dependencies:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
...
CookieManager = new SystemWebCookieManager()
...
});
A similar answer has been provided here but it does not include all of the code-base required to solve the problem, so I see a need to add it here because the external link to the Katana Project may go down and this should be fully chronicled as a solution here as well.
Starting with the great analysis by #TomasDolezal, I had a look at both the Owin and the System.Web source.
The problem is that System.Web has its own master source of cookie information and that isn't the Set-Cookie header. Owin only knows about the Set-Cookie header. A workaround is to make sure that any cookies set by Owin are also set in the HttpContext.Current.Response.Cookies collection.
I've made a small middleware (source, nuget) that does exactly that, which is intended to be placed immediately above the cookie middleware registration.
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
Katana team answered to the issue Tomas Dolezar raised, and posted documentation about workarounds:
Workarounds fall into two categories. One is to re-configure
System.Web so it avoids using the Response.Cookies collection and
overwriting the OWIN cookies. The other approach is to re-configure
the affected OWIN components so they write cookies directly to
System.Web's Response.Cookies collection.
Ensure session is established prior to authentication: The conflict between System.Web and Katana cookies is per request, so it may be
possible for the application to establish the session on some request
prior to the authentication flow. This should be easy to do when the
user first arrives, but it may be harder to guarantee later when the
session or auth cookies expire and/or need to be refreshed.
Disable the SessionStateModule - If the application is not relying on session information, but the session module is still setting a
cookie that causes the above conflict, then you may consider disabling
the session state module.
Reconfigure the CookieAuthenticationMiddleware to write directly to System.Web's cookie collection.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ...
CookieManager = new SystemWebCookieManager()
});
See SystemWebCookieManager implementation from the documentation (link above)
More information here
Edit
Below the steps we took to solve the issue. Both 1. and 2. solved the problem also separately but we decided to apply both just in case:
1.
Use SystemWebCookieManager
2.
Set the session variable:
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
// See http://stackoverflow.com/questions/20737578/asp-net-sessionid-owin-cookies-do-not-send-to-browser/
requestContext.HttpContext.Session["FixEternalRedirectLoop"] = 1;
}
(side note: the Initialize method above is the logical place for the fix because base.Initialize makes Session available. However, the fix could also be applied later because in OpenId there's first an anonymous request, then redirect to the OpenId provider and then back to the app. The problems would occur after the redirect back to the app while the fix sets the session variable already during the first anonymous request thus fixing the problem before any redirect back even happens)
Edit 2
Copy-paste from the Katana project 2016-05-14:
Add this:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ...
CookieManager = new SystemWebCookieManager()
});
...and this:
public class SystemWebCookieManager : ICookieManager
{
public string GetRequestCookie(IOwinContext context, string key)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
var cookie = webContext.Request.Cookies[key];
return cookie == null ? null : cookie.Value;
}
public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
bool expiresHasValue = options.Expires.HasValue;
var cookie = new HttpCookie(key, value);
if (domainHasValue)
{
cookie.Domain = options.Domain;
}
if (pathHasValue)
{
cookie.Path = options.Path;
}
if (expiresHasValue)
{
cookie.Expires = options.Expires.Value;
}
if (options.Secure)
{
cookie.Secure = true;
}
if (options.HttpOnly)
{
cookie.HttpOnly = true;
}
webContext.Response.AppendCookie(cookie);
}
public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
AppendResponseCookie(
context,
key,
string.Empty,
new CookieOptions
{
Path = options.Path,
Domain = options.Domain,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}
}
Answers have been provided already, but in owin 3.1.0, there is a SystemWebChunkingCookieManager class that can be used.
https://github.com/aspnet/AspNetKatana/blob/dev/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs
https://raw.githubusercontent.com/aspnet/AspNetKatana/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
...
CookieManager = new SystemWebChunkingCookieManager()
...
});
If you are setting cookies in OWIN middleware yourself, then using OnSendingHeaders seems to get round the problem.
For example, using the code below owinResponseCookie2 will be set, even though owinResponseCookie1 is not:
private void SetCookies()
{
var owinContext = HttpContext.GetOwinContext();
var owinResponse = owinContext.Response;
owinResponse.Cookies.Append("owinResponseCookie1", "value1");
owinResponse.OnSendingHeaders(state =>
{
owinResponse.Cookies.Append("owinResponseCookie2", "value2");
},
null);
var httpResponse = HttpContext.Response;
httpResponse.Cookies.Remove("httpResponseCookie1");
}
I faced the Similar Issue with Visual Studio 2017 and .net MVC 5.2.4, Updating Nuget Microsoft.Owin.Security.Google to lastest version which currently is 4.0.1 worked for me!
Hope this Helps someone!
The fastest one-line code solution:
HttpContext.Current.Session["RunSession"] = "1";
Just add this line before CreateIdentity method:
HttpContext.Current.Session["RunSession"] = "1";
var userIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
_authenticationManager.SignIn(new AuthenticationProperties { IsPersistent = rememberLogin }, userIdentity);
I had the same symptom of the Set-Cookie header not being sent but none of these answers helped me. Everything worked on my local machine but when deployed to production the set-cookie headers would never get set.
It turns out it was a combination of using a custom CookieAuthenticationMiddleware with WebApi along with WebApi compression support
Luckily I was using ELMAH in my project which let me to this exception being logged:
System.Web.HttpException Server cannot append header after HTTP
headers have been sent.
Which led me to this GitHub Issue
Basically, if you have an odd setup like mine you will want to disable compression for your WebApi controllers/methods that set cookies, or try the OwinServerCompressionHandler.
I'm developing a C# MVC application and I can't seem to get the Authentication and Session timeouts to synchronize. I have a basic Forms Authentication setup and some limited session values. I set the Authentication timeout less than the session (28 minutes vs 30) but running against the development web server, the session will be wiped on a restart of the server but the authentication sticks around. I'm assuming that the authentication is being stored in a cookie that obviously survives the server restart.
<authentication mode="Forms" >
<forms loginUrl="~/Account/Login" timeout="28" />
</authentication>
<sessionState timeout="30" />
I think I want to force the the authentication to timeout if Session is null, to then force a login.
Is that what I actually want to do? If so how and where do I do this?
If not, what is the proper way to handle this?
EDIT
For more of a perspective I also posted this question for this same project: Login as... best practices?
I found my answer. Override the Authorize attribute. This seems like the most elegant approach:
public class AuthorizeWithSessionAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext.Session == null || httpContext.Session["CurrentUser"] == null)
return false;
return base.AuthorizeCore(httpContext);
}
}
You could handle this in global.asax with PreRequestHandlerExecute event handler
protected void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
//Check if user is authenticated
HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
if (!authTicket.Expired)
{
if (Session["XYZ"] == null)
{
//Session is null, redirect to login page
FormsAuthentication.SignOut();
Response.Redirect(FormsAuthentication.LoginUrl, true);
return;
}
}
}
}
Or, you could write a Httpmodule and implement context_AuthenticateRequest to check if session exists and handle the request accordingly.
Hope that helps.
Edit by Valamas
See answer https://stackoverflow.com/a/1446575/511438 for help with the session error.
Im building a website with the new ASP.NET MVC3 framework and using FormsAuth. for securing the website. I'm storing the role of a user in the UserData property of the FormsAuthenticationTicket, (setting the cookie manually), I then call the encrypt method on the ticket before adding it to the cookie(see below a Standard ticket sniplet).
if (Validate(model.UserName, model.Password))
{
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1,
model.UserName,
DateTime.Now,
DateTime.Now.AddMinutes(30),
false,
UserType.Administrator.ToString());
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
Response.Cookies.Add(faCookie);
return RedirectToAction("startpage", "mycontroller");
}
}
Now I've made a custom AuthorizeAttribute thats able to check if the user is 1. authenticated and 2. has the admin role (from the ticket). (below)
The AuthorizeCore method of this derived class will be called when an action takes places in a class that has the attribute annotion.
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
string cookieName = FormsAuthentication.FormsCookieName;
HttpCookie authCookie = httpContext.Request.Cookies[cookieName];
if (authCookie == null)
return false;
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
if (authTicket.UserData != UserType.Administrator.ToString())
return false;
return true;
So here's where im getting confused.
When I follow the code being executed (with valid credentials, in debug), and check the values of the variables made on each line, the encryptedTicket encrypts just fine before adding it to the reponsecookie.
But when I then check the AuthorizeCore method when the controller (of the index page) is being called, the parameter its getting, the HttpContext, contains the ticket with everything unencrypted, so there is no need to decrypt the ticket anymore when reading the cookie.
Why do I see the ticket succesfully being encrypted in the logon controller where I send it back to the client, but then when I receive the httpcontext in the AuthorizeAdministrator class its all unencrypted again.
Sorry for the long question/story, there's probably a simple and short answer for it.
Hope my story is clear.
Thanks.
Forms auth needs to decrypt the cookie early in the page processing pipeline, to determine if the user is authorized -- that's when it fills in the details for User.Identity, etc.
I have a .NET Webforms site thanks needs to post to my MVC Application which currently sits inside the Webform site as a separate application.
The Webform application need to POST some sensitive values to the MVC Application.
Is there a way to generate a AntiForgeryToken() in my WebForms Application so it can be passed with the form post.
Otherwise does anyone know of any other custom anti forgery code that will allow me to do something similar to the MVC's AntiForgeryValidation.
Implementing it yourself is not too difficult.
Generate a GUID
Put it in a hidden field
Also put it in Session or Cookie (in the latter case, with some anti-tamper protection)
At the start of processing the form compare the field and stored token.
(If you look at the implementation of MVC, there is very little more to it. A few helper methods is all you need.)
This is an old question, but the latest Visual Studio 2012 ASP.NET template for web forms includes anti CSRF code baked into the master page. If you don't have the templates, here's the code it generates:
Protected Sub Page_Init(sender As Object, e As System.EventArgs)
' The code below helps to protect against XSRF attacks
Dim requestCookie As HttpCookie = Request.Cookies(AntiXsrfTokenKey)
Dim requestCookieGuidValue As Guid
If ((Not requestCookie Is Nothing) AndAlso Guid.TryParse(requestCookie.Value, requestCookieGuidValue)) Then
' Use the Anti-XSRF token from the cookie
_antiXsrfTokenValue = requestCookie.Value
Page.ViewStateUserKey = _antiXsrfTokenValue
Else
' Generate a new Anti-XSRF token and save to the cookie
_antiXsrfTokenValue = Guid.NewGuid().ToString("N")
Page.ViewStateUserKey = _antiXsrfTokenValue
Dim responseCookie As HttpCookie = New HttpCookie(AntiXsrfTokenKey) With {.HttpOnly = True, .Value = _antiXsrfTokenValue}
If (FormsAuthentication.RequireSSL And Request.IsSecureConnection) Then
responseCookie.Secure = True
End If
Response.Cookies.Set(responseCookie)
End If
AddHandler Page.PreLoad, AddressOf master_Page_PreLoad
End Sub
Private Sub master_Page_PreLoad(sender As Object, e As System.EventArgs)
If (Not IsPostBack) Then
' Set Anti-XSRF token
ViewState(AntiXsrfTokenKey) = Page.ViewStateUserKey
ViewState(AntiXsrfUserNameKey) = If(Context.User.Identity.Name, String.Empty)
Else
' Validate the Anti-XSRF token
If (Not DirectCast(ViewState(AntiXsrfTokenKey), String) = _antiXsrfTokenValue _
Or Not DirectCast(ViewState(AntiXsrfUserNameKey), String) = If(Context.User.Identity.Name, String.Empty)) Then
Throw New InvalidOperationException("Validation of Anti-XSRF token failed.")
End If
End If
End Sub
The C# version of Ian Ippolito answer here:
public partial class SiteMaster : MasterPage
{
private const string AntiXsrfTokenKey = "__AntiXsrfToken";
private const string AntiXsrfUserNameKey = "__AntiXsrfUserName";
private string _antiXsrfTokenValue;
protected void Page_Init(object sender, EventArgs e)
{
// The code below helps to protect against XSRF attacks
var requestCookie = Request.Cookies[AntiXsrfTokenKey];
Guid requestCookieGuidValue;
if (requestCookie != null && Guid.TryParse(requestCookie.Value, out requestCookieGuidValue))
{
// Use the Anti-XSRF token from the cookie
_antiXsrfTokenValue = requestCookie.Value;
Page.ViewStateUserKey = _antiXsrfTokenValue;
}
else
{
// Generate a new Anti-XSRF token and save to the cookie
_antiXsrfTokenValue = Guid.NewGuid().ToString("N");
Page.ViewStateUserKey = _antiXsrfTokenValue;
var responseCookie = new HttpCookie(AntiXsrfTokenKey)
{
HttpOnly = true,
Value = _antiXsrfTokenValue
};
if (FormsAuthentication.RequireSSL && Request.IsSecureConnection)
{
responseCookie.Secure = true;
}
Response.Cookies.Set(responseCookie);
}
Page.PreLoad += master_Page_PreLoad;
}
protected void master_Page_PreLoad(object sender, EventArgs e)
{
if (!IsPostBack)
{
// Set Anti-XSRF token
ViewState[AntiXsrfTokenKey] = Page.ViewStateUserKey;
ViewState[AntiXsrfUserNameKey] = Context.User.Identity.Name ?? String.Empty;
}
else
{
// Validate the Anti-XSRF token
if ((string)ViewState[AntiXsrfTokenKey] != _antiXsrfTokenValue
|| (string)ViewState[AntiXsrfUserNameKey] != (Context.User.Identity.Name ?? String.Empty))
{
throw new InvalidOperationException("Validation of Anti-XSRF token failed.");
}
}
}
protected void Page_Load(object sender, EventArgs e)
{
}
}
WebForms has a pretty similar analog in Page.ViewStateUserKey. By setting that to a per-user value (most choose HttpSessionState.SessionId), WebForms will validate the ViewState1 as part of the MAC check.
overrides OnInit(EventArgs e) {
base.OnInit(e);
ViewStateUserKey = Session.SessionId;
}
1 There are scenarios where ViewStateUserKey will not help. Mainly, they boil down to doing dangerous things with GET requests (or in Page_Load without checking IsPostback), or disabling ViewStateMAC.
You can use reflection to get at the MVC methods used to set the cookie and matching form input used for the MVC validation. That way you can have an MVC action with [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken] attributes that you can post to from a WebForms generated page.
See this answer: Using an MVC HtmlHelper from a WebForm
I have a question about custom authorization in MVC.
I have a site that I want to limit access to certain pages, depending on their group membership. Now I have seen tons of examples on how to do this if there is a single admin group and a single user group, for example, but not any examples for a third level.
For example, only users of a company can view orders for their own company (and each company has its own admins, etc). These companies are stored in a DB. So I have seen ways to do custom authorization, overriding the AuthorizeCore method on the AuthorizeAttribute, but I don't know how to access to the parameters passed into the controller to see if the user has access to the order (order id, for example).
Is this even the best place to do the check, or should this just be handled directly from the method of the controller?
The AuthorizationContext (parameter to OnAuthorize) provides access to the Controller, RouteData, HttpContext, etc. You should be able to use these in a custom authorization filter to do what you want. Below is a sample of code from a RoleOrOwnerAttribute derived from AuthorizeAttribute.
public override void OnAuthorization( AuthorizationContext filterContext )
{
if (filterContext == null)
{
throw new ArgumentNullException( "filterContext" );
}
if (AuthorizeCore( filterContext.HttpContext )) // checks roles/users
{
SetCachePolicy( filterContext );
}
else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
// auth failed, redirect to login page
filterContext.Result = new HttpUnauthorizedResult();
}
// custom check for global role or ownership
else if (filterContext.HttpContext.User.IsInRole( "SuperUser" ) || IsOwner( filterContext ))
{
SetCachePolicy( filterContext );
}
else
{
ViewDataDictionary viewData = new ViewDataDictionary();
viewData.Add( "Message", "You do not have sufficient privileges for this operation." );
filterContext.Result = new ViewResult { MasterName = this.MasterName, ViewName = this.ViewName, ViewData = viewData };
}
}
// helper method to determine ownership, uses factory to get data context,
// then check the specified route parameter (property on the attribute)
// corresponds to the id of the current user in the database.
private bool IsOwner( AuthorizationContext filterContext )
{
using (IAuditableDataContextWrapper dc = this.ContextFactory.GetDataContextWrapper())
{
int id = -1;
if (filterContext.RouteData.Values.ContainsKey( this.RouteParameter ))
{
id = Convert.ToInt32( filterContext.RouteData.Values[this.RouteParameter] );
}
string userName = filterContext.HttpContext.User.Identity.Name;
return dc.Table<Participant>().Where( p => p.UserName == userName && p.ParticipantID == id ).Any();
}
}
protected void SetCachePolicy( AuthorizationContext filterContext )
{
// ** IMPORTANT **
// Since we're performing authorization at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether a page should be served from the cache.
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge( new TimeSpan( 0 ) );
cachePolicy.AddValidationCallback( CacheValidateHandler, null /* data */);
}
If the authorization is really that dynamic, I would handle it in the controller. I have one action where I do this - you can return a HttpUnauthorizedResult to redirect to the login page or you can show a custom error in your view.
I don't the default redirect to the login page when somebody is already logged in, but not in the correct role. That's very confusing for the user.
My answer isn't great, because it kills unit testing, but I'm pulling values from System.Web.HttpContext.Current.Session. The singleton is available throughout the project. By saving the current user in session, you can get to it from anywhere, including utility classes like AuthorizeAttribute.
I'd love to see a unit-testable solution, though.