Skip to content
This repository has been archived by the owner on Jan 24, 2021. It is now read-only.

Commit

Permalink
Removed use of object serializer for Csrf cookie generation
Browse files Browse the repository at this point in the history
  • Loading branch information
thecodejunkie committed Jul 10, 2017
1 parent d2d0240 commit 292185a
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 51 deletions.
56 changes: 36 additions & 20 deletions src/Nancy.Tests/Unit/Security/CsrfFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
using System.Linq;
using System.Threading;
using FakeItEasy;

using Nancy.Bootstrapper;
using Nancy.Cryptography;
using Nancy.Helpers;
using Nancy.Security;
using Nancy.Tests.Fakes;

Expand All @@ -16,28 +14,18 @@
public class CsrfFixture
{
private readonly IPipelines pipelines;

private readonly Request request;

private readonly FakeRequest optionsRequest;

private readonly Response response;

private readonly CryptographyConfiguration cryptographyConfiguration;

private readonly DefaultObjectSerializer objectSerializer;


public CsrfFixture()
{
this.pipelines = new MockPipelines();

this.cryptographyConfiguration = CryptographyConfiguration.Default;

this.objectSerializer = new DefaultObjectSerializer();
var csrfStartup = new CsrfApplicationStartup(
this.cryptographyConfiguration,
this.objectSerializer,
new DefaultCsrfTokenValidator(this.cryptographyConfiguration));

csrfStartup.Initialize(this.pipelines);
Expand All @@ -53,30 +41,38 @@ public CsrfFixture()
[Fact]
public void Should_create_cookie_in_response_if_token_exists_in_context()
{
// Given
var context = new NancyContext { Request = this.request, Response = this.response };
context.Items[CsrfToken.DEFAULT_CSRF_KEY] = "TestingToken";

// When
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());

// Then
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
this.response.Cookies.FirstOrDefault(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).Value.ShouldEqual("TestingToken");
}

[Fact]
public void Should_copy_request_cookie_to_context_but_not_response_if_it_exists_and_context_does_not_contain_token()
{
// Given
this.request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, "ValidToken");
var fakeValidator = A.Fake<ICsrfTokenValidator>();

A.CallTo(() => fakeValidator.CookieTokenStillValid(A<CsrfToken>.Ignored)).Returns(true);

var csrfStartup = new CsrfApplicationStartup(
this.cryptographyConfiguration,
this.objectSerializer,
fakeValidator);

csrfStartup.Initialize(this.pipelines);
var context = new NancyContext { Request = this.request, Response = this.response };

// When
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());

// Then
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeFalse();
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
context.Items[CsrfToken.DEFAULT_CSRF_KEY].ShouldEqual("ValidToken");
Expand All @@ -85,20 +81,24 @@ public void Should_copy_request_cookie_to_context_but_not_response_if_it_exists_
[Fact]
public void Should_not_generate_a_new_token_on_an_options_request_and_not_add_a_cookie()
{
// Given
this.optionsRequest.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, "ValidToken");

var fakeValidator = A.Fake<ICsrfTokenValidator>();
A.CallTo(() => fakeValidator.CookieTokenStillValid(A<CsrfToken>.Ignored)).Returns(true);

var csrfStartup = new CsrfApplicationStartup(
this.cryptographyConfiguration,
this.objectSerializer,
fakeValidator);

csrfStartup.Initialize(this.pipelines);
var context = new NancyContext { Request = this.optionsRequest, Response = this.response };
context.Items[CsrfToken.DEFAULT_CSRF_KEY] = "ValidToken";

// When
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());

// Then
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeFalse();
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
context.Items[CsrfToken.DEFAULT_CSRF_KEY].ShouldEqual("ValidToken");
Expand All @@ -107,18 +107,23 @@ public void Should_not_generate_a_new_token_on_an_options_request_and_not_add_a_
[Fact]
public void Should_regenerage_token_if_invalid()
{
// Given
this.request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, "InvalidToken");
var fakeValidator = A.Fake<ICsrfTokenValidator>();

A.CallTo(() => fakeValidator.CookieTokenStillValid(A<CsrfToken>.Ignored)).Returns(false);

var csrfStartup = new CsrfApplicationStartup(
this.cryptographyConfiguration,
this.objectSerializer,
fakeValidator);

csrfStartup.Initialize(this.pipelines);
var context = new NancyContext { Request = this.request, Response = this.response };

// When
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());

// Then
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
context.Items[CsrfToken.DEFAULT_CSRF_KEY].ShouldNotEqual("InvalidToken");
Expand All @@ -127,18 +132,22 @@ public void Should_regenerage_token_if_invalid()
[Fact]
public void Should_http_decode_cookie_token_when_copied_to_the_context()
{
// Given
var fakeValidator = A.Fake<ICsrfTokenValidator>();
A.CallTo(() => fakeValidator.CookieTokenStillValid(A<CsrfToken>.Ignored)).Returns(true);

var csrfStartup = new CsrfApplicationStartup(
this.cryptographyConfiguration,
this.objectSerializer,
fakeValidator);

csrfStartup.Initialize(this.pipelines);
this.request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, "Testing Token");
var context = new NancyContext { Request = this.request, Response = this.response };

// When
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());

// Then
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeFalse();
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
context.Items[CsrfToken.DEFAULT_CSRF_KEY].ShouldEqual("Testing Token");
Expand All @@ -147,10 +156,13 @@ public void Should_http_decode_cookie_token_when_copied_to_the_context()
[Fact]
public void Should_create_a_new_token_if_one_doesnt_exist_in_request_or_context()
{
// Given
var context = new NancyContext { Request = this.request, Response = this.response };

// When
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());

// Then
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
var cookieValue = this.response.Cookies.FirstOrDefault(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).Value;
Expand All @@ -162,12 +174,16 @@ public void Should_create_a_new_token_if_one_doesnt_exist_in_request_or_context(
[Fact]
public void Should_be_able_to_disable_csrf()
{
// Given
var context = new NancyContext { Request = this.request, Response = this.response };
context.Items[CsrfToken.DEFAULT_CSRF_KEY] = "TestingToken";

Csrf.Disable(this.pipelines);

// When
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());

// Then
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeFalse();
}

Expand All @@ -178,7 +194,7 @@ public void ValidateCsrfToken_gets_provided_token_from_form_data()
var token = Csrf.GenerateTokenString();
var context = new NancyContext { Request = this.request };
var module = new FakeNancyModule { Context = context };

// When
context.Request.Form[CsrfToken.DEFAULT_CSRF_KEY] = token;
context.Request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, token);
Expand All @@ -194,7 +210,7 @@ public void ValidateCsrfToken_gets_provided_token_from_request_header_if_not_pre
var token = Csrf.GenerateTokenString();
var context = new NancyContext();
var module = new FakeNancyModule { Context = context };

// When
context.Request = RequestWithHeader(CsrfToken.DEFAULT_CSRF_KEY, token);
context.Request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, token);
Expand All @@ -208,4 +224,4 @@ private static FakeRequest RequestWithHeader(string header, string value)
return new FakeRequest("GET", "/", new Dictionary<string, IEnumerable<string>> { { header, new[] { value } } });
}
}
}
}
100 changes: 87 additions & 13 deletions src/Nancy/Security/Csrf.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
namespace Nancy.Security
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;

using Cookies;
using Nancy.Bootstrapper;
using Nancy.Cookies;
using Nancy.Cryptography;
using Nancy.Helpers;

Expand All @@ -14,12 +17,15 @@
public static class Csrf
{
private const string CsrfHookName = "CsrfPostHook";
private const char ValueDelimiter = '#';
private const char PairDelimiter = '|';

/// <summary>
/// Enables Csrf token generation.
/// This is disabled by default.
/// </summary>
/// <param name="pipelines">Application pipelines</param>
/// <remarks>This is disabled by default.</remarks>
/// <param name="pipelines">The application pipelines.</param>
/// <param name="cryptographyConfiguration">The cryptography configuration. This is <see langword="null" /> by default.</param>
public static void Enable(IPipelines pipelines, CryptographyConfiguration cryptographyConfiguration = null)
{
cryptographyConfiguration = cryptographyConfiguration ?? CsrfApplicationStartup.CryptographyConfiguration;
Expand All @@ -35,16 +41,18 @@ public static void Enable(IPipelines pipelines, CryptographyConfiguration crypto

if (context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY))
{
context.Response.Cookies.Add(new NancyCookie(CsrfToken.DEFAULT_CSRF_KEY,
(string)context.Items[CsrfToken.DEFAULT_CSRF_KEY],
true));
context.Response.Cookies.Add(new NancyCookie(
CsrfToken.DEFAULT_CSRF_KEY,
(string)context.Items[CsrfToken.DEFAULT_CSRF_KEY],
true));

return;
}

if (context.Request.Cookies.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY))
{
var cookieValue = context.Request.Cookies[CsrfToken.DEFAULT_CSRF_KEY];
var cookieToken = CsrfApplicationStartup.ObjectSerializer.Deserialize(cookieValue) as CsrfToken;
var cookieToken = ParseToCsrfToken(cookieValue);

if (CsrfApplicationStartup.TokenValidator.CookieTokenStillValid(cookieToken))
{
Expand Down Expand Up @@ -76,7 +84,7 @@ public static void Disable(IPipelines pipelines)
/// Only necessary if a particular route requires a new token for each request.
/// </summary>
/// <param name="module">Nancy module</param>
/// <returns></returns>
/// <param name="cryptographyConfiguration">The cryptography configuration. This is <c>null</c> by default.</param>
public static void CreateNewCsrfToken(this INancyModule module, CryptographyConfiguration cryptographyConfiguration = null)
{
var tokenString = GenerateTokenString(cryptographyConfiguration);
Expand All @@ -93,12 +101,21 @@ internal static string GenerateTokenString(CryptographyConfiguration cryptograph
cryptographyConfiguration = cryptographyConfiguration ?? CsrfApplicationStartup.CryptographyConfiguration;
var token = new CsrfToken
{
CreatedDate = DateTime.Now,
CreatedDate = DateTimeOffset.Now
};

token.CreateRandomBytes();
token.CreateHmac(cryptographyConfiguration.HmacProvider);
var tokenString = CsrfApplicationStartup.ObjectSerializer.Serialize(token);
return tokenString;

var builder = new StringBuilder();

builder.AppendFormat("RandomBytes{0}{1}", ValueDelimiter, Convert.ToBase64String(token.RandomBytes));
builder.Append(PairDelimiter);
builder.AppendFormat("Hmac{0}{1}", ValueDelimiter, Convert.ToBase64String(token.Hmac));
builder.Append(PairDelimiter);
builder.AppendFormat("CreatedDate{0}{1}", ValueDelimiter, token.CreatedDate.ToString("o", CultureInfo.InvariantCulture));

return builder.ToString();
}

/// <summary>
Expand Down Expand Up @@ -135,7 +152,7 @@ private static CsrfToken GetProvidedToken(Request request)
var providedTokenString = request.Form[CsrfToken.DEFAULT_CSRF_KEY].Value ?? request.Headers[CsrfToken.DEFAULT_CSRF_KEY].FirstOrDefault();
if (providedTokenString != null)
{
providedToken = CsrfApplicationStartup.ObjectSerializer.Deserialize(providedTokenString) as CsrfToken;
providedToken = ParseToCsrfToken(providedTokenString);
}

return providedToken;
Expand All @@ -148,10 +165,67 @@ private static CsrfToken GetCookieToken(Request request)
string cookieTokenString;
if (request.Cookies.TryGetValue(CsrfToken.DEFAULT_CSRF_KEY, out cookieTokenString))
{
cookieToken = CsrfApplicationStartup.ObjectSerializer.Deserialize(cookieTokenString) as CsrfToken;
cookieToken = ParseToCsrfToken(cookieTokenString);
}

return cookieToken;
}

private static void AddTokenValue(Dictionary<string, string> dictionary, string key, string value)
{
if (!string.IsNullOrEmpty(key))
{
dictionary.Add(key, value);
}
}

private static CsrfToken ParseToCsrfToken(string cookieTokenString)
{
var parsed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

var currentKey = string.Empty;
var buffer = new StringBuilder();

for (var index = 0; index < cookieTokenString.Length; index++)
{
var currentCharacter = cookieTokenString[index];

switch (currentCharacter)
{
case ValueDelimiter:
currentKey = buffer.ToString();
buffer.Clear();
break;
case PairDelimiter:
AddTokenValue(parsed, currentKey, buffer.ToString());
buffer.Clear();
break;
default:
buffer.Append(currentCharacter);
break;
}
}

AddTokenValue(parsed, currentKey, buffer.ToString());

if (parsed.Keys.Count() != 3)
{
return null;
}

try
{
return new CsrfToken
{
CreatedDate = DateTimeOffset.ParseExact(parsed["CreatedDate"], "o", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
Hmac = Convert.FromBase64String(parsed["Hmac"]),
RandomBytes = Convert.FromBase64String(parsed["RandomBytes"])
};
}
catch
{
return null;
}
}
}
}
Loading

0 comments on commit 292185a

Please sign in to comment.