-
Notifications
You must be signed in to change notification settings - Fork 0
Forms Authentication
Forms authentication is a feature Microsoft added to ASP.NET in version 1.1, and enhanced with the introducing of membership and security controls in version 2.0. Before that many classic ASP and ASP.NET applications store user information such as username as session variables. Since http communication is stateless, classic ASP and ASP.NET runtime always maintain a session for each browser within each application to help maintain user state. A session always exists no matter if the user logs in or not. Such session is often called browser session since there exists one session per browser at any time. A session consists of a session Id and session state. The session Id is generated by server and sent to client as a cookie. Thus this session Id cookie is sent back to server with every subsequent request, in this way at the server side we can recognize requests from the same browser. Session state is an array of data associated with a session id and lives at server side only. At server side, we can find the user data in session state using the session Id sent from the client side. Session state can be stored in various places on server, such as in database, or in a separate state service, but most common in application memory. If we store user data in memory session, then restarting application pool would kick users out of the application. In summary, session allows an application to store data for each user, but classic ASP and ASP.NET runtime do not provide extra support for using session to control user authentication.
Forms authentication stores username, and other user data if necessary, as an encrypted cookie, thus can survive from application pool restart. When we store user identity in session state, for each request, we need to check session state to decide if user is authenticated in application code. ASP.NET framework provides extra support to the forms authentication that makes it easy to use, for example since the server has knowledge of if user is authenticated or not, we can use settings in web.config, outside of application code, to prevent unauthorized users from access the whole site or certain pages. In application code, we can use User.IsAuthenticated to check if user is authenticated, and from User.Identity we can find username as well as the ticket content.
The logic to authenticate user is beyond the scope of the forms authentication and can be still fully controlled by each application. Forms authentication provides a way to maintain such information and establish authentication to generic features provided by ASP.NET such as security controls. Often once we get the username and password from user, we verify them against user information stored in a database, then call the FormsAuthentication.SetAuthCookie with the username to generate the forms authentication ticket cookie for that user, in this way that user is authenticated.
Session timeout value can be set in web.config and the default value is 20 minutes. Session timeout can be either fixed, which means no matter what you do, after that many minutes the session expires; or sliding, which means session expires only if you idle for than many minutes, before session expires every time you send request to server the session expiration time is extended to have that many minutes from the current time. Forms authentication ticket also has a timeout value that can also be set in web.config and the default is 30 minutes, and the expiration can also be fixed or sliding. The meaning of sliding is a little different, the expiration time gets extended only if server receives a request after half of the timeout value has passed.
Session and ticket can time out separately and independently. If we want the session to always time out first, then we just need to make the ticket timeout value to be at least double of the session timeout value. Say session time is N minutes and ticket timeout is 2N minutes. In order for the ticket times out, there cannot be any request to server in the last N minutes before the expiration. But if there is no request in N minutes, the session would already time out, thus it times out before the ticket does. One advantage of doing so is we can have an opportunity to expire the ticket when session times out by handling the session ends event, thus make them expire at the same time. See https://itworksonmymachine.wordpress.com/2008/07/17/forms-authentication-timeout-vs-session-timeout/.
We want to design a strategy to handle forms authentication expiration. The goal is to display a warning popup window to user before forms authentication expires to give user a chance to extend the ticket. We need to take into consideration that there are multiple applications sharing the same ticket and a user may work on them at the same time. Each may display their warning because it is not possible to display a warning on top of every window on certain machines. If the user clicks the button on a warning popup to indicate he wants to continue to work, the forms authentication expiration time should be extended and all warning popups should disappear. If we reach the ticket expiration time, all application should either display the login page or simple disappear depending on if it is the application user logged in from.
The ticket expiration time is part of the forms authentication data, which is encrypted at server side. Thus we cannot get this value at client side using javascript code. We need to find it at server side and pass it to the client side as a separate cookie (in the following we will call it expiration-time cookie for convenience). We can have a filter to add that value as a cookie in the OnActionExecuted method in a MVC application, see here, or better do that in a http application event handler since then the code can be shared by both MVC and WebForm applications:
protected void Application_EndRequest(object sender, EventArgs e) {
if (this.Context.Response.Cookies.AllKeys.Contains(FormsAuthentication.FormsCookieName)) {
HttpCookie tcookie = this.Context.Response.Cookies[FormsAuthentication.FormsCookieName];
if (string.IsNullOrEmpty(tcookie.Value)) {
var cookie = new HttpCookie("ticketExpirationTime") {
Expires = DateTime.Now.AddDays(-1),
};
this.Context.Response.Cookies.Add(cookie);
}
else {
var ticket = FormsAuthentication.Decrypt(tcookie.Value);
var cookie = new HttpCookie("ticketExpirationTime", ticket.Expiration.ToString());
cookie.Path = ticket.CookiePath;
if (ticket.IsPersistent) cookie.Expires = ticket.Expiration;
this.Context.Response.Cookies.Add(cookie);
}
}
}Notice in classic ASP and ASP.NET, The Request and Response objects both have a property called Cookies, which makes cookie looks like symmetric in http request and http response. But actually there is a difference which can be seen better in the http level: in a http request, there is a header named Cookie, but in a http response, the header that contain cookies is named Set-Cookie, which indicates the fact that the client sends every cookie to server in every request, but the server does not send back every cookie in every response. It only sends the cookie initially when creating it or when modifying it. This is true to any cookie including ticket cookie. Thus we should only set the expiration-time cookie when the response contains the ticket cookie. Notice the way we check if ticket cookie exists in response, we cannot check directly if Context.Response.Cookies[FormsAuthentication.FormsCookieName] is null since that would create the cookie if it does not exist.
Our approach is to check the ticket remaining time Periodically and act based on it. When ticket exists and not expired, we check every second to see
- if the ticket already expired or is missing, take user to login page or closing the window
- if the ticket is going to expire in 2 minutes, display the warning popup if it is not opened yet
- if the ticket’s remaining time is more than 2 minutes, close the popup if it is opened
Originally I thought if there is more than 2 minutes left, we do not need to check every second, and can wait it we thin there are only 2 minutes left to check again. But that approach cannot handle the situation that user may click a logoff button on a page (not the logoff button in the warning popup window) to manually logoff.
The warning popup window may contain the following elements:
- a remaining seconds counter that will be updated by every checking
- an OK button for user to indicate he wants to continue working. Once clicked, send an Ajax call to server to extend the ticket expiration time
- a Log Off button for user to indicate he wants to stop working. Once clicked, post back to server to sign off authentication as well as clear all cookies (which therefore terminates the session)
That are all an application needs to do if it does not use session, which is typical situation in a MVC application. If an application uses session, we need some extra work to make sure session not expired before ticket expires. Here we assume the session timeout value is equal to or greater than the ticket timeout value. We want to renew session before it expires as long as the ticket is not expired. We need to modify the last step of the checking routine a little as
- if the ticket’s remaining time is more than 2 minutes, close the popup if it is opened, send an Ajax call to server to extend the session if we detect ticket expiration time changes (the reason we need this condition is we should only extend session only every time ticket extends)
We are going to prove our session will not expire before the ticket expires as long as the session timeout value is a little bigger than the ticket timeout value using mathematical induction. For convinence, we denote the session timeout value as ST and the session expiration time as SE; the ticket timeout value as TT and the ticket expiration time is TE; and current time as CT.
- First at the beginning when the page is initially loaded, session has ST remaining time, The ticket may have TT remaining time or less depending on how many remaining time the ticket has. Since ST>=TT, we know SE=CT+ST>=CE+TT>=TE at that time
- Now we assume after Nth checking, SE>=TE, now consider after 1 second the (N+1)-th time of checking happens: if there is no change to TE, then that inequality is still valid; if TE changes, we send a request to extend session. At then end that inequality is valid again
We should expire session when ticket expires so no remnant is carried over when the current user or a new user login from the same browser. Unfortunately there is no ticket expiration event we can attach a handler to, but we can still expire session in the following way:
- in the action that handles user log off, abandon session in addition to sign off forms authentication
- when user gets to the login page, sign off forms authentication and abandon session
If the session timeout value is less than the ticket timeout value, I believe there is no proper way to extend session when needed without affecting the ticket expiration time.
When ticket expires, we would like to either take user to the login page or simply close the window. Notice that not every window can be closed by calling window.close. Based on my observation of IE 11, Firefox 50 and Chrome 74, a window (including tab) can be closed if
- it is opened by a
window.opencall - it is opened by a user clicking a hyperlink (on a web page or email in display)
- (Chrome only) it is opened by user, and the current document is displayed because user entered its url of in the location bar On the other hand, a window (including tab) can be not closed if
- (IE and Firefox) if it is opened by user
- (Chrome) if it is opened by user, and the url of the current document was not entered by user, i.e. the current page is loaded by an Url redirection (HTTP response code 302) initiated from server or a call to set
window.location.hrefat the client side If we callwindow.closefor a non-closable window, an error "Scripts may not close windows that were not opened by script" would be thrown in Firefox and Chrome, or a prompt window "The webpage you are viewing is trying to close the window. Do you want to close this window?" would appear asking user to confirm in IE. Unfortunately there is no reliable way in script to determine the window is closable. Even a try/catch statement cannot catch such error and rescue such call. Normally if a window is opened by the call towindow.open, thenwindow.openercontains value, but at least in some browsers, this value can be set in code and thus is not reliable. One possible solution is to base on the name of the window to determine if we want to close it, that requires some pattern in the window name for the applications on that server.
In the last section, I mentioned the need to make an Ajax call to extend session. How can we achieve that? Notice session is specific to each application, and thus this call has to be a request to a resource within an individual application. Is it possible we can make it to happen without modifying each individual application? The solution is to use a http handler. The handler is implemented in a separate assembly added to GAC, then used by every application through global configuration.
The tricky part is to choose a proper url path for this handler. First of all, we better choose a path extension that is handled by aspnet_isapi.dll (in IIS below 7.0) or IsapiModule module (in IIS 7.0 and above, if using classic mode), otherwise we need to add Url mapping (in IIS below 7.0) or module mapping (in IIS 7.0 and above, if using classic mode) in addition to adding the handler mapping. Secondly MVC applications may require additional setting. For details, see here. The best solution we find is use the path extensession.axd.
At the beginning, as shown above, I store the following value in the cookie:
ticket.Expiration.ToString()then at the client side, I can easily get the ticket remaining time as follows
var ticketExpirationTime = $.cookie('ticketExpirationTime');
var remainingMilliseconds = new Date(ticketExpirationTime) - new Date();but soon I realized that that value does not include the millisecond part of ticket.Expiration, which is one reason the ticket expires later than I thought. I tried to include the milliseconds in the cookie as:
ticket.Expiration.ToString("MM/dd/yyyy hh:mm:ss.fff tt")but my javascript code have problem to understand such format (e.g. 04/09/2019 08:10:10.123 AM) when passing to new Date and returns NaN. I found Date.parse can handle this value in Chrome, but not in IE. In IE we can do the following
Date.pase('04/09/2019 08:10:10.123 AM')but it simply ignores the millisecond part (e.g. 123). To support all browsers without any surprise, we better pass datetime as milliseconds instead as string. I tried to store expiration time as milliseconds as follows:
Math.Ceiling((ticket.Expiration.ToUniversalTime() - new DateTime(1970, 1, 1)).TotalMilliseconds).ToString()This is the total milliseconds from 1/1/1970 to the current UTC time. This value is comparable to the client side getTime call, so the remaining milliseconds can be found from
var millisecondsRemaining = expirationTime - (new Date()).getTime();It works fine except now it becomes hard to inspect the expiration time when debugging. Therefore I still prefer to store datetime value in cookie. The solution is to round the expiration time up to the next whole second:
var dt = ticket.Expiration;
var roundupdt = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
if (dt.Millisecond > 0) roundupdt = roundupdt.AddSeconds(1);
var cookie = new HttpCookie("ticketExpirationTime", roundupdt.ToString());Another issue I realized is the ticket expiration time is regarding the server clock, set at server and check by server-side framework code, and thus we cannot simply compare this value with client clock to determine if ticket expires or not. We need to know the clock difference between browser and server. It is not possible to get the exact difference since we cannot check clock at server side and at client side at the exact same time, but we can easily get a upper bound of it: let server expose the server time in a method, then client makes an Ajax call to this method to get the server time back and compare it with the client time at the receiving time. This is not exact the clock difference but an upper bound of it (the difference of this upper bound and the exact difference is the time between the server executing the method and the client gets the response). Using this information we can make sure the ticket expires no later than when we think it expires:
public string GetServerTime() {
return DateTime.Now.ToString();
}var clockDifference = (function () {
var serverTime;
$.ajax({
url: serverTimeUrl,
type: 'get',
dataType: 'text',
async: false,
success: function (data) {
serverTime = new Date(data);
}
});
var clientTime = new Date();
return clientTime - serverTime;
})();We only need to call it once when we start the timeout monitoring. Then every time we check timeout, use that value to adjust the remaining time calculation:
var millisecondsRemaining = new Date(expirationTime) - new Date() + clockDifference;Our timeout monitoring routine is a javascript file stored in a centralized location and included in every page in every web application. How can we make sure client will get the newest version if we update that javascript file? One possible approach is to prevent client side from caching, e.g. add a version based on the current time when adding reference to that script file. But this approach is not favored since it adds two much traffic. Therefore I decide to add a driver file: that is the file included in every page. This file is very simple and its only functionality is to load the monitoring routine. This file has no version since I don’t expect it to change. The version of the monitoring routine file in introduced in the driver script when it includes the monitoring routine. How do we generate that version number directly determines how often the client side can cache the monitoring routine. I have thought of the following approaches:
- Every time user creates a new browser session, we use a new version number. I expect we can achieve that using a session cookie. The driver checks if that cookie exists. If not exist, it means this is a browser user just opened, so derive a version number of the current time then put it in the session cookie. Then as long as that cookie exists, use that cookie value as the version number. In that way we only get the monitoring routine once in every session. I like this approach but I do not have a reliable simple way to create cookie in javascript without using jquery.cookie.js. So I did not use it
- Due to the difficulty of creating cookie, I prefer approach that is based on existing cookies. We cannot use the asp.net session id cookie because it is http-only cookie and thus not available to javascript code. I end up using the ticket expiration-time cookie, which is still OK since the client side only get the monitoring routine every time the ticket extends
One thing deserves mentioning is the way the driver adds the monitoring routine script. At first I use jquery way to add that script line to the body. Then I notice in some application, jquery add an extra version number, which causes the script not cachable. I believe it is caused by a global no-caching Ajax call setting in those applications. Therefore I switch to the javascript way of creating a script element set its src property then add to the body.
Ideally we should be able to use just one file to serve both purposes: if the script reference has no query string named v, then the script runs like a driver; otherwise it runs the monitoring routine. To achieve that we need to find out the query string in the script reference from the script code. One simple way is to call document.currentScript.src, but that does not work in IE. There are more complex hacker ways to achieve that, e.g. https://loopj.com/2010/06/12/simple-way-to-extract-get-params-from-a-javascript-script-tag/. But I am not very sure if it will already work. In the end, I still use two files to be safe.
If a web site contains silverlight content, our popup window may not appear on screen. The reason is silverlight window although looks like embedded in the browser window is actually a separate window just positioned in its container DOM element, and is always on top of the browser window that contains it, whereas our popup is part of the browser window. In order to make our popup window to appear, we need to move the silverlight window container out of the browser viewport when displaying the popup, and move it back when binding the popup window. We can add such code in the silverlight application (the name of the function will be known to our generic monitoring routine), then in our generic monitoring routine, when we have the need to hide or show the popup, check if function of that name exists in the current page, if exists call it. I also tried to change the display or the visibility properties of the container, but neither work well, sometimes a call to server for the silverlight content occurred, some other times an error occurred.
In summary, our solution of timeout warning feature consists of the following 7 pieces:
- an assembly in GAC containing (both requires configuration)
- a http module to setup ticket expiration time cookie
- a http handler to extend session
- a seperate supporting web application that contains
- a js file to establish client-side timeout monitoring checking, and to support the popup warning window
- a css file containing the popup warning window styles, it is included to web pages by the js file above
- a second js file plays the role of a driver, this is the file that should be included in every web page in each web application. This file loads the first js file and add versioning support to it
- a generic handler to extend ticket
- a generic handler to logoff
The things I like of this approach are
- each application cares only of its own business. It never has the need to check if any other application is running
The thing I am not happy with are
- we need to add that driver script reference to every web page in every application. Is it possible to inject that script reference globally? I thought of doing that using response filter in a module, but it may hurts performance of those individual applications, which is the last thing I want to do
How can multiple ASP.NET applications share the same forms authentication? The web.config file of these applications need to have some common elements:
- same system.web/authentication/forms value
- same machineKey value
- have all of the following 3 entries in appSettings
<add key="aspnet:UseLegacyFormsAuthenticationTicketCompatibility" value="true"/>
<add key="aspnet:UseLegacyEncryption" value="true"/>
<add key="aspnet:UseLegacyMachineKeyEncryption" value="true"/>
- have similar entry in system.web section (the targetFramework number might not need to be the same, for example 4.5 and 4.6.1 works fine together, but 4.5 and 4.0, and 4.5 and missing are not fine)
<httpRuntime targetFramework="4.6.1" />Now let us add one more dimention to the consideration: an SPA. Let us consider a concrete scenario, say we have the following web sites:
- an ASP.NET WebForm application having a login form, running at http://localhost:4000
- an Angular application, running at http://localhost:4200
- an ASP.NET WebApi application, running at http://localhost:4400
Our goal is to login from the WebForm application, then navigate to the Angular page, then send http request to the WebApi to get data from there. Would the WebApi application be able to get the authentication information?
In order to achieve that, we first need to make the WebForm and WebApi application to share forms authentication (see the requirement above).
Then we make a endpoint to be a secured one: add Authorize to the api action:
public class ValuesController : ApiController {
// GET api/values
[Authorize]
public IEnumerable<string> Get() {
return new string[] { "value1", "value2" };
}Then I can access the api via http://localhost:4002/api/values, where we assume the WebApi application running at http://localhost:4002.
Optionally we want the api to return json object instead of xml, we can add the following code in the Register method of the WebApiConfig class:
config.Formatters.JsonFormatter.SupportedMediaTypes
.Add(new MediaTypeHeaderValue("text/html"));Secondly we need to add CORS support in the system.webServer section of the web.config of the WebApi application:
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="http://localhost:4200" />
<add name="Access-Control-Allow-Methods" value="*" />
<add name="Access-Control-Allow-Headers" value="*" />
<add name="Access-Control-Allow-Credentials" value="true" />
</customHeaders>
</httpProtocol>Then we have to add the http call in the Angular application:
- modify app.module.ts to import http library
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],- modify app.component.ts to make the http call
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
value$: any;
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.value$ = this.http.get<string[]>(
'http://localhost:4400/api/values',
{ withCredentials: true }
);
}- modify app.component.html to display the value obtained from the http call
<div *ngIf="value$ | async as v"> {{ v }} </div>- Question: In general a http module needs to be registered in order to be used in a web application, but then how come I do not find FormsAuthenticationModule mentioned anywhere in the web.config file of my application?
- Answer: It is registered in a machine-level web.config (C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\web.config) and thus available to every web application running in the classic pipeline mode, for more details, see https://stackoverflow.com/questions/20163911/where-is-formsauthenticationmodule-registered.
<system.web>
<httpModules>
<add name="OutputCache" type="System.Web.Caching.OutputCacheModule"/>
<add name="Session" type="System.Web.SessionState.SessionStateModule"/>
<add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule"/>
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule"/>It is also available to every web application running in the integrated pipeline mode since it is registered in C:\Windows\System32\inetsrv\config\applicationHost.config:
<system.webServer>
<modules>
...
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" preCondition="managedHandler" />- Question: We know that the forms authentication ticket gets extended only when user sends a request to server after half way through the expiration time. Exactly where does this logic implemented?
- Answer: It is implemented in the RenewTicketIfOld method of the FormsAuthentication class, which is called by the FormsAuthenticationModule class in the handler (OnEnter->OnAuthentication) to the AuthenticateRequest event.
public static FormsAuthenticationTicket RenewTicketIfOld(FormsAuthenticationTicket tOld) {
if (tOld == null) return null;
DateTime utcNow = DateTime.UtcNow;
TimeSpan ticketAge = utcNow - tOld.IssueDateUtc;
TimeSpan ticketRemainingLifetime = tOld.ExpirationUtc - utcNow;
if (ticketRemainingLifetime > ticketAge) return tOld; // no need to renew
// The original ticket may have had a custom-specified lifetime separate from
// the default timeout specified in config. We should honor that original
// lifetime when renewing the ticket.
TimeSpan originalTicketTotalLifetime = tOld.ExpirationUtc - tOld.IssueDateUtc;
DateTime newExpirationUtc = utcNow + originalTicketTotalLifetime;
FormsAuthenticationTicket ticket = FormsAuthenticationTicket.FromUtc(
tOld.Version /* version */,
tOld.Name /* name */,
utcNow /* issueDateUtc */,
newExpirationUtc /* expirationUtc */,
tOld.IsPersistent /* isPersistent */,
tOld.UserData /* userData */,
tOld.CookiePath /* cookiePath */);
return ticket;
}-
Question: Say we have multiple applications share the same forms authentication ticket, the new expiration time when a ticket get renewed is based on the timeout setting of the application that initially creates the ticket, or the timeout setting of the application that renews this ticket?
-
Answer: The code of the RenewTicketIfOld method shows that it is based on the timeout setting of the application that initially creates the ticket.
-
Question: How to create a http module used for all applications on a server?
-
Answer:
- create a project of type class library and add reference to System.Web.dll, then create a class to inherit IHttpModule and add the application event handler code there. Clicking Signing tab of the Property Page window, and check 'Sign the Assembly' where we can choose a string key name file (the extension is .snk), and we can ask VS to generate the content (i.e. VS can generate a key for us)
- add it to GAC on my machine: run Developer Command Prompt for Visual Studio 2017 as Administrator, and execute
gacutil -i C:\dev\MyTestModule.dll - find this dll in GAC: C:\Windows\Microsoft.NET\assembly\GAC_MSIL\MyTestModule\v4.0_1.0.0.0__012345aaaaaaaaaa, where 4.0 is .NET runtime version, 1.0.0.0 is the dll version, and 0012345aaaaaaaaaa is public key token
- add it to the system.web/httpModules section in the global level web.config (C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\web.config) to support applications running in the classic pipeline mode, and to the system.webServer/modules section in C:\Windows\System32\inetsrv\config\applicationHost.config to support application running in integrated pipeline mode. As an alternative, we can add it to the web.config in the web site root folder (normally C:\inetpub\wwwroot) instead in both the system.web/httpModules and system.webServer/modules sections.
-
Question: I added handlers/modules to C:\Windows\System32\inetsrv\config\applicationHost.config, but it seems they are not running?
-
Answer: We can verify if a handler or module is running by inspecting the Handler Mappings and Modules panes to see if the handler or the module is listed there. If they are not there, probably the reason is IIS uses a different applicationHost.config file. C:\Windows\System32\inetsrv\config is the default path of this file, but its location can be changed: in IIS Manager, select the machine node, then in the Features View pane, select Management->Shared Configuration, there we can set Configuration Location->Physical Path; or we can modify the redirection.config file in C:\Windows\System32\inetsrv\config, and enter the new path there.
-
Question: The FormsAuthenticationTicket class has a property named IsPersistent that determines if the forms authentication ticket cookie should be persistent or not. But how come there is no corresponding attribute in the system.web/authentication/forms node in web.config that allows application owner to set this value?
-
Answer: Microsoft expects this is not an application-level setting, instead it is decision made by each user. Take a look at the Login control (https://github.com/microsoft/referencesource/blob/master/System.Web/UI/WebControls/login.cs) . There is a "Remember Me" checkbox in the login screen. If a user select that checkbox, then the Login control would create a persistent cookie for the forms authentication ticket:
private void AttemptLogin() {
...
AuthenticateEventArgs authenticateEventArgs = new AuthenticateEventArgs();
OnAuthenticate(authenticateEventArgs);
if (authenticateEventArgs.Authenticated) {
System.Web.Security.FormsAuthentication.SetAuthCookie(UserNameInternal, RememberMeSet);
...- Question: How does server determine if user is authenticated?
- Answer: A user is authenticated if User.Identity.IsAuthenticated returns true, where User is a property that we can find in various places, such as a property of the controller and httpcontext. For example the Authorize attribute that that information to tell if user is authenticated, see https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Http/AuthorizeAttribute.cs.
In ASP.NET framework, the proper place to set up user authentication status is in an AuthenticateRequest event handler in the ASP.NET pipeline. https://stackoverflow.com/questions/9594229/accessing-session-using-asp-net-web-api
In Forms Authentication, this happens in the http module FormsAuthenticationModule https://github.com/microsoft/referencesource/blob/master/System.Web/Security/FormsAuthenticationModule.cs
- in the Init method, attach an event handler OnEnter to the AuthenticateRequest event
- in the OnEnter method, calls the OnAuthenticate method
- in the OnAuthenticate method,
// Step 6: Create a user object for the ticket
e.Context.SetPrincipalNoDemand(new GenericPrincipal(new FormsIdentity(ticket2), new String[0]));Notice SetPrincipalNoDemand is defined in the HttpContext class, and it is equivalent to us to set User property to HttpContext, since the User property is defined as
public IPrincipal User {
get { return _principalContainer.Principal; }
[SecurityPermission(SecurityAction.Demand, ControlPrincipal=true)]
set { SetPrincipalNoDemand(value); }
}