Skip to content

Cookie Authentication

txgz999 edited this page Jul 27, 2019 · 29 revisions

Cookie Sharing

Cookie can be shared by all ports if we do not set its port attribute, see https://stackoverflow.com/questions/1612177/are-http-cookies-port-specific.

Say I setup a cookie at server side in an action method of an ASP.NET MVC application running at http://localhost/MyApp as follows:

var cookie = new System.Web.HttpCookie("mytest", "123");
this.Response.Cookies.Add(cookie);

(which creates a cookie of Path /, see https://stackoverflow.com/questions/27833462/httpcookie-path-and-mapped-urls), then I can retrieve it at server side in another application running at http://localhost:56142/

var cookie = this.Request.Cookies["mytest"];
Response.Write(cookie.Value);

Cookie is an effective way for different applications running at the same domain to share information. On the other hand, we need to be considerable when using cookies that suppose to be used by the current application only, since without extra care, it is possible that different web applications hosted on the same domain may override cookies. To make a cookie only available to the current application, we can add a Path value when creating it:

cookie.Path = "~/";

It is not possible to set port number in ASP.NET for System.Web.HttpCookie, see https://stackoverflow.com/questions/7349946/asp-net-httpcookie-specify-port.

Notice sharing cookie is one thing, being able to share encrypted cookie content is another thing that has more requirements. For example, in order to share the form authentication, both sites need to have some common setting in web.config:

  • 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" />

I have all those values in the web.config file in C:\inetpub\wwwroot, which is the IIS root folder on my local machine, thus I have no need to set these values in each application. But that works fine only if I run the application in IIS. If I run the application from VS through IIS Express, these settings are not inherited by the application.

Cookie Authentication and Login Form

Cookie authentication is similar to forms authentication, but it is provided by an OWIN middleware and thus is used by OWIN applications.

As an example, in the following we create a web application that allows user to login. Once login successfully set authentication cookie: create an ASP.NET web application with MVC template, then

  • install package Microsoft.Owin.Host.SystemWeb
  • install package Microsoft.Owin.Security.Cookies
  • create Startup.cs
public class Startup {
    public void Configuration(IAppBuilder app) {
        ConfigureOAuth(app);
    }
    public void ConfigureOAuth(IAppBuilder app) {
        app.UseCookieAuthentication(new CookieAuthenticationOptions {
            AuthenticationType = "AppCookie",
            LoginPath = new PathString("/Home/Login"),
        });
    }
}
  • create a login form, first create the action methods in the Home controller:
public class HomeController : Controller {
    [HttpGet]
    public ActionResult Login() {
        return View();
    }

    [HttpPost]
    public ActionResult Login(LoginModel model) {
        if (model.UserName == "test" && model.Password == "test") {
            var claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.Name, model.UserName));
            var id = new ClaimsIdentity(claims, "AppCookie");
            var ctx = Request.GetOwinContext();
            var authenticationManager = ctx.Authentication;
            authenticationManager.SignIn(id);
            return RedirectToAction("Index", "Home");
        }
        return View(model);
    }

    [Authorize]
    public ActionResult Index() {
        return View();
    }
}

Notice that I have added the Authorize decorator to the Index page, so if the user has not logged in yet, the application would take the user to the Login page when he tries to access the home page.

  • and the corresponding view
<h2>Login</h2>
@using (Html.BeginForm("Login", "Home", FormMethod.Post)) {
    @Html.ValidationSummary(true)
  <fieldset>
    @Html.LabelFor(m => m.UserName):
    @Html.TextBoxFor(m => m.UserName)
    <br />
    @Html.LabelFor(m => m.Password):
    @Html.TextBoxFor(m => m.Password)
    <br />
    <input type="submit" value="Submit" />
  </fieldset>
}
  • LoginModel class is defined as
public class LoginModel {
    public string UserName { get; set; }
    public string Password { get; set; }
}
  • then we want to show the current login status by adding the following to _Layout.cshtml
    @User.Identity.Name

Notice that I hard code the AuthenticationType value. It can be any value, as long as the Startup class and the Login method use the same value. Some sample uses the value DefaultAuthenticationTypes.ApplicationCookie, which is the constant ApplicationCookie, but in order to use it we have to install the package Microsoft.AspNet.Identity.Core. That value determines the cookie name (.AspNet.<AuthenticationType>).

Cookie authentication is not related to OAuth, thus the package Microsoft.Owin.Security.OAuth is not needed. In a sense, Microsoft.Owin.Security.Cookies plays a similar role.

Share Authentication Among Applications

Cookie can possibly be shared by all applications running on the same domain (e.g. http://www.xyz.com/test1, http://www.xyz.com/test2, http://www.xyz.com:3000). But in order for each application to share the authentication cookie, all we need is to make them to use the same machine key, see https://www.dotnetexpertguide.com/2016/11/sharing-owin-authentication-cookie-across-iis-applications.html.

Say, the example above runs at http://localhost:4000, then I can create a second ASP.NET web application using the WebForm template and running at http://localhost:4001 and is setup as follows:

  • install package Microsoft.Owin.Host.SystemWeb
  • install package Microsoft.Owin.Security.Cookies
  • create Startup.cs
public class Startup {
    public void Configuration(IAppBuilder app) {
        ConfigureOAuth(app);
    }
    public void ConfigureOAuth(IAppBuilder app) {
        app.UseCookieAuthentication(new CookieAuthenticationOptions {
            AuthenticationType = "AppCookie",
        });
    }
}
  • add the following to Default.aspx:
<%=User.Identity.Name %>
  • add a common machineKey to the system.web section of the web.config file in both applications, e.g.
<machineKey decryptionKey="85F7ABF05FD7713140FB4A6E1A402F68FDCC0330CED20080" validationKey="BA4C9F70AB3AC0A6CB2AF7753982378244269374CDE3C5A7BA07F78583CF882551D5144C4425D10C05AA96701AA1A0680E318DD99C4D14AAB4A780E0783E60D0" />  

Then once I login from the first application, then navigate to the second application, my user name appears on the page.

  • To get the claims, we can have code like the following in The Default.aspx.cs:
protected void Page_Load(object sender, EventArgs e) {
     var ctx = Request.GetOwinContext();
     var user = ctx.Authentication.User;
     var claims = user.Claims;
}

I also tested a ASP.NET WebApi application, with the following setup:

  • install package Microsoft.Owin.Host.SystemWeb
  • install package Microsoft.Owin.Security.Cookies
  • create Startup.cs
public class Startup {
    public void Configuration(IAppBuilder app) {
        ConfigureOAuth(app);
    }
    public void ConfigureOAuth(IAppBuilder app) {
        app.UseCookieAuthentication(new CookieAuthenticationOptions {
            AuthenticationType = "AppCookie",
        });
    }
}
  • add the same machineKey to the system.web section of the web.config file
  • 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 if we can 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"));

Angular Client

Now let us consider a concrete scenario, we have the following web sites:

  • an ASP.NET WebForm application having login form, running at http://localhost:4000 (setup in the way described above)
  • an Angular application, running at http://localhost:4200
  • an ASP.NET WebApi application, running at http://localhost:4400 (setup in the way described above, including the change to make api to return json objects)

The 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 the same machineKey. 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>

Resources

Clone this wiki locally