Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FormsAuthenticationTicket encryption #23

Open
wants to merge 2 commits into
base: update-netcore-3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,28 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Synercoding.FormsAuthentication;
using System.Threading.Tasks;

namespace TestImplementation.ReadCookie.Controllers
{
[Authorize]
public class SecurityController : Controller
{
private readonly FormsAuthenticationOptions _formsAuthenticationOptions;

public SecurityController(IOptions<FormsAuthenticationOptions> options)
{
_formsAuthenticationOptions = options.Value;
}

public IActionResult Index()
{
var authCryptor = new FormsAuthenticationCryptor(_formsAuthenticationOptions);
var ticket = authCryptor.Unprotect(Request.Cookies["TestCookie"]);
ViewData["TestCookie-UserData"] = ticket.UserData;

return View();
}

Expand Down
3 changes: 3 additions & 0 deletions samples/TestImplementation.ReadCookie/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public void ConfigureServices(IServiceCollection services)
ValidationMethod = section.GetValue<ValidationMethod>("ValidationMethod"),
};

// Enables injection of IOptions<FormsAuthenticationOptions>
services.Configure<FormsAuthenticationOptions>(section);

services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,7 @@
</tr>
}
</tbody>
</table>
</table>

<h3>Ticket User Data</h3>
<div>@ViewData["TestCookie-UserData"]</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Web.Mvc;
using System;
using System.Web;
using System.Web.Mvc;
using TestImplementation.SetCookie.Models;

namespace TestImplementation.SetCookie.Controllers
Expand All @@ -19,6 +21,10 @@ public ActionResult Index(LoginVM model)
{
if (ModelState.IsValid)
{
var ticket = new System.Web.Security.FormsAuthenticationTicket(1, "TestTicket", DateTime.Now, DateTime.Now.AddDays(1), true, "The answer is '42'.");
var encryptedTicket = System.Web.Security.FormsAuthentication.Encrypt(ticket);
Response.Cookies.Add(new HttpCookie("TestCookie", encryptedTicket));

System.Web.Security.FormsAuthentication.SetAuthCookie(model.UserName, true);
return Redirect(model.ReturnUrl);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ public string Protect(FormsAuthenticationCookie cookie)
return CryptoUtil.BinaryToHex(protectedData);
}

public string Protect(FormsAuthenticationTicket ticket)
{
if (ticket == null)
throw new ArgumentNullException(nameof(ticket));

var unprotectedData = FormsAuthenticationTicketSerializer.Serialize(ticket);

var cryptoProvider = AspNetCryptoServiceProvider.GetCryptoServiceProvider(_options);
var cryptoService = cryptoProvider.GetCryptoService();
byte[] protectedData = cryptoService.Protect(unprotectedData);

return CryptoUtil.BinaryToHex(protectedData);
}

public FormsAuthenticationCookie Unprotect(string protectedText)
{
if (protectedText == null)
Expand Down Expand Up @@ -99,7 +113,7 @@ private FormsAuthenticationCookie ConvertToAuthenticationTicket(byte[] data)
byte footer = ticketReader.ReadByte();
if (footer != 0xFF)
throw new ArgumentException("The data is not in the correct format, footer byte must be 0xFF.", nameof(data));

//create ticket
return new FormsAuthenticationCookie()
{
Expand Down
251 changes: 251 additions & 0 deletions src/Synercoding.FormsAuthentication/FormsAuthenticationTicket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
//------------------------------------------------------------------------------
// <copyright file="FormsAuthenticationTicket.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

/*
* FormsAuthenticationTicket class
*
* Copyright (c) 1999 Microsoft Corporation
*/

namespace Synercoding.FormsAuthentication
{
using System.Security.Principal;
using System.Security.Permissions;
//using System.Web.Configuration;
using System.Runtime.Serialization;
using System;


/// <devdoc>
/// <para>This class encapsulates the information represented in
/// an authentication cookie as used by FormsAuthenticationModule.</para>
/// </devdoc>
[Serializable]
public sealed class FormsAuthenticationTicket
{

/// <devdoc>
/// <para>A one byte version number for future
/// use.</para>
/// </devdoc>
public int Version { get { return _Version; } }

/// <devdoc>
/// The user name associated with the
/// authentication cookie. Note that, at most, 32 bytes are stored in the
/// cookie.
/// </devdoc>
public String Name { get { return _Name; } }

/// <devdoc>
/// The date/time at which the cookie
/// expires.
/// </devdoc>
public DateTime Expiration { get { return _Expiration; } }

/// <devdoc>
/// The time at which the cookie was originally
/// issued. This can be used for custom expiration schemes.
/// </devdoc>
public DateTime IssueDate { get { return _IssueDate; } }

/// <devdoc>
/// True if a durable cookie was issued.
/// Otherwise, the authentication cookie is scoped to the browser lifetime.
/// </devdoc>
public bool IsPersistent { get { return _IsPersistent; } }

/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public bool Expired
{
get
{
/*
* Two DateTime instances can only be compared if they are of the same DateTimeKind.
* Therefore we normalize everything to UTC to do the comparison. See comments on
* the ExpirationUtc property for more information
*/
return (ExpirationUtc < DateTime.UtcNow);
}
}

/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public String UserData { get { return _UserData; } }


/// <devdoc>
/// <para>[To be supplied.]</para>
/// </devdoc>
public String CookiePath { get { return _CookiePath; } }

/*
* We always prefer UTC expiration dates to work around issues like a daylight
* saving time changes between the time the ticket was issued and the time the
* ticket was checked. If we have a firm UTC expiration date, just use it
* directly.
*
* If we don't have a firm UTC expiration date, try converting the developer-
* provided date to UTC before doing the comparison. There are three types of
* DateTime, and the .NET Framework converts as so:
*
* - The DateTime is already UTC, in which case it is returned unmodified.
* - The DateTime is local, in which case the .NET Framework converts it to
* UTC. There is also a hidden bit in the DateTime struct which essentially
* states whether daylight saving time was active when this DateTime was
* generated, i.e. whether this was 2:02 AM PDT or 2:02 AM PST. The .NET
* framework handles round-tripping Local <-> UTC correctly, but comparisons
* can still fail as described in detail below.
* - The DateTime is of an undefined type, in which case it is implicitly
* treated as local in a manner consistent with .NET 1.1.
*
* However, this alone is insufficient to work around DST issues when comparing
* local dates. For example, assume that a ticket is issued on Nov 6, 2011 at
* 1:30 AM PDT (UTC -0700) with a timeout period of 20 minutes. The expiration
* date is thus calculated to be Nov 6, 2011 at 1:50 AM PDT (UTC -0700). Now
* say a request comes in 25 minutes after expiration; the time is currently
* 1:15 AM PST (UTC -0800). [A DST boundary has been crossed.] Since this
* request came in *after* the ticket expiration date, the ticket should be
* rejected. And if we were doing all of our comparisons in UTC, this would
* indeed be the case. However, since the DateTime struct doesn't have UTC
* offset information embedded in it, comparisons of their dates are taken at
* face value as simple wall time comparisons. Thus the current time is
* interpreted just as "1:15 AM" and the expiration time is intepreted just as
* "1:50 AM", and from this simple comparison the token is considered unexpired
* and is accepted by the system.
*
* To see this incorrect behavior in action, run the following on a machine
* in the Pacific Time Zone. Contrast the behavior of the DateTimeOffset type
* (which is designed to handle UTC offsets correctly) with the DateTime type, in
* which the FromFileTime method implicitly does a local time conversion.
*
* long ft1 = 129650430000000000; // Nov 6, 2011 1:50 AM PDT (UTC -0700)
* long ft2 = 129650445000000000; // Nov 6, 2011 1:15 AM PST (UTC -0800)
* DateTimeOffset.FromFileTime(ft1) < DateTimeOffset.FromFileTime(ft2) = true
* DateTime.FromFileTime(ft1) < DateTime.FromFileTime(ft2) = false (INCORRECT!)
*
* To be absolutely safe, we must perform comparisons *only* on DateTime instances
* we know to have correct UTC information, or we must use an offset-aware type
* like DateTimeOffset which just does the right thing automatically.
*
* More info: http://msdn.microsoft.com/en-us/library/bb546099.aspx
*/
internal DateTime ExpirationUtc
{
get { return (_ExpirationUtcHasValue) ? _ExpirationUtc : Expiration.ToUniversalTime(); }
}

internal DateTime IssueDateUtc
{
get { return (_IssueDateUtcHasValue) ? _IssueDateUtc : IssueDate.ToUniversalTime(); }
}

private int _Version;
private String _Name;
private DateTime _Expiration;
private DateTime _IssueDate;
private bool _IsPersistent;
private String _UserData;
private String _CookiePath;

#pragma warning disable 0169 // unused field
// These fields were added in .NET 4 but weren't actually used anywhere.
// We can't remove them since they're part of the serialization contract.
[OptionalField(VersionAdded = 2)]
private int _InternalVersion;
[OptionalField(VersionAdded = 2)]
private Byte[] _InternalData;
#pragma warning restore 0169

// Issue and expiration times as UTC.
// We can't use nullable types since they didn't exist in v1.1, and this assists backporting fixes downlevel.
[NonSerialized]
private bool _ExpirationUtcHasValue;
[NonSerialized]
private DateTime _ExpirationUtc;
[NonSerialized]
private bool _IssueDateUtcHasValue;
[NonSerialized]
private DateTime _IssueDateUtc;


///// <devdoc>
///// <para>This constructor creates a
///// FormsAuthenticationTicket instance with explicit values.</para>
///// </devdoc>
//public FormsAuthenticationTicket(int version,
// String name,
// DateTime issueDate,
// DateTime expiration,
// bool isPersistent,
// String userData)
//{
// _Version = version;
// _Name = name;
// _Expiration = expiration;
// _IssueDate = issueDate;
// _IsPersistent = isPersistent;
// _UserData = userData;
// _CookiePath = FormsAuthentication.FormsCookiePath;
//}


public FormsAuthenticationTicket(int version,
String name,
DateTime issueDate,
DateTime expiration,
bool isPersistent,
String userData,
String cookiePath)
{
_Version = version;
_Name = name;
_Expiration = expiration;
_IssueDate = issueDate;
_IsPersistent = isPersistent;
_UserData = userData;
_CookiePath = cookiePath;
}



///// <devdoc>
///// <para> This constructor creates
///// a FormsAuthenticationTicket instance with the specified name and cookie durability,
///// and default values for the other settings.</para>
///// </devdoc>
//public FormsAuthenticationTicket(String name, bool isPersistent, Int32 timeout)
//{
// _Version = 2;
// _Name = name;
// _IssueDateUtcHasValue = true;
// _IssueDateUtc = DateTime.UtcNow;
// _IssueDate = DateTime.Now;
// _IsPersistent = isPersistent;
// _UserData = "";
// _ExpirationUtcHasValue = true;
// _ExpirationUtc = _IssueDateUtc.AddMinutes(timeout);
// _Expiration = _IssueDate.AddMinutes(timeout);
// _CookiePath = FormsAuthentication.FormsCookiePath;
//}

internal static FormsAuthenticationTicket FromUtc(int version, String name, DateTime issueDateUtc, DateTime expirationUtc, bool isPersistent, String userData, String cookiePath)
{
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(version, name, issueDateUtc.ToLocalTime(), expirationUtc.ToLocalTime(), isPersistent, userData, cookiePath);

ticket._IssueDateUtcHasValue = true;
ticket._IssueDateUtc = issueDateUtc;
ticket._ExpirationUtcHasValue = true;
ticket._ExpirationUtc = expirationUtc;

return ticket;
}

}
}
Loading