Cookie fixes

This commit is contained in:
Hao Kung 2015-07-01 11:55:06 -07:00
parent d7ce42dacc
commit b9f152ebb1
11 changed files with 197 additions and 63 deletions

View File

@ -24,17 +24,17 @@ namespace Microsoft.AspNet.Authentication.Cookies
private const string SessionIdClaim = "Microsoft.AspNet.Authentication.Cookies-SessionId"; private const string SessionIdClaim = "Microsoft.AspNet.Authentication.Cookies-SessionId";
private bool _shouldRenew; private bool _shouldRenew;
private DateTimeOffset _renewIssuedUtc; private DateTimeOffset? _renewIssuedUtc;
private DateTimeOffset _renewExpiresUtc; private DateTimeOffset? _renewExpiresUtc;
private string _sessionKey; private string _sessionKey;
public override async Task<AuthenticationTicket> AuthenticateAsync() protected override async Task<AuthenticationTicket> AuthenticateAsync()
{ {
AuthenticationTicket ticket = null; AuthenticationTicket ticket = null;
try try
{ {
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName);
if (string.IsNullOrWhiteSpace(cookie)) if (string.IsNullOrEmpty(cookie))
{ {
return null; return null;
} }
@ -96,6 +96,11 @@ namespace Microsoft.AspNet.Authentication.Cookies
await Options.Notifications.ValidatePrincipal(context); await Options.Notifications.ValidatePrincipal(context);
if (context.ShouldRenew)
{
_shouldRenew = true;
}
return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme);
} }
catch (Exception exception) catch (Exception exception)
@ -130,28 +135,33 @@ namespace Microsoft.AspNet.Authentication.Cookies
return cookieOptions; return cookieOptions;
} }
private async Task ApplyCookie(AuthenticationTicket model) private async Task ApplyCookie(AuthenticationTicket ticket)
{ {
var cookieOptions = BuildCookieOptions(); if (_renewIssuedUtc.HasValue)
{
model.Properties.IssuedUtc = _renewIssuedUtc; ticket.Properties.IssuedUtc = _renewIssuedUtc;
model.Properties.ExpiresUtc = _renewExpiresUtc; }
if (_renewExpiresUtc.HasValue)
{
ticket.Properties.ExpiresUtc = _renewExpiresUtc;
}
if (Options.SessionStore != null && _sessionKey != null) if (Options.SessionStore != null && _sessionKey != null)
{ {
await Options.SessionStore.RenewAsync(_sessionKey, model); await Options.SessionStore.RenewAsync(_sessionKey, ticket);
var principal = new ClaimsPrincipal( var principal = new ClaimsPrincipal(
new ClaimsIdentity( new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) }, new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.AuthenticationScheme)); Options.AuthenticationScheme));
model = new AuthenticationTicket(principal, null, Options.AuthenticationScheme); ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme);
} }
var cookieValue = Options.TicketDataFormat.Protect(model); var cookieValue = Options.TicketDataFormat.Protect(ticket);
if (model.Properties.IsPersistent) var cookieOptions = BuildCookieOptions();
if (ticket.Properties.IsPersistent && _renewExpiresUtc.HasValue)
{ {
cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; cookieOptions.Expires = _renewExpiresUtc.Value.ToUniversalTime().DateTime;
} }
Options.CookieManager.AppendResponseCookie( Options.CookieManager.AppendResponseCookie(
@ -160,17 +170,9 @@ namespace Microsoft.AspNet.Authentication.Cookies
cookieValue, cookieValue,
cookieOptions); cookieOptions);
Response.Headers.Set( Response.Headers.Set(HeaderNameCacheControl, HeaderValueNoCache);
HeaderNameCacheControl, Response.Headers.Set(HeaderNamePragma, HeaderValueNoCache);
HeaderValueNoCache); Response.Headers.Set(HeaderNameExpires, HeaderValueMinusOne);
Response.Headers.Set(
HeaderNamePragma,
HeaderValueNoCache);
Response.Headers.Set(
HeaderNameExpires,
HeaderValueMinusOne);
} }
protected override async Task FinishResponseAsync() protected override async Task FinishResponseAsync()
@ -181,15 +183,15 @@ namespace Microsoft.AspNet.Authentication.Cookies
return; return;
} }
var model = await AuthenticateAsync(); var ticket = await AuthenticateOnceAsync();
try try
{ {
await ApplyCookie(model); await ApplyCookie(ticket);
} }
catch (Exception exception) catch (Exception exception)
{ {
var exceptionContext = new CookieExceptionContext(Context, Options, var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model); CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, ticket);
Options.Notifications.Exception(exceptionContext); Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow) if (exceptionContext.Rethrow)
{ {
@ -286,7 +288,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
{ {
var query = Request.Query; var query = Request.Query;
var redirectUri = query.Get(Options.ReturnUrlParameter); var redirectUri = query.Get(Options.ReturnUrlParameter);
if (!string.IsNullOrWhiteSpace(redirectUri) if (!string.IsNullOrEmpty(redirectUri)
&& IsHostRelative(redirectUri)) && IsHostRelative(redirectUri))
{ {
var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri);
@ -348,7 +350,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
{ {
var query = Request.Query; var query = Request.Query;
var redirectUri = query.Get(Options.ReturnUrlParameter); var redirectUri = query.Get(Options.ReturnUrlParameter);
if (!string.IsNullOrWhiteSpace(redirectUri) if (!string.IsNullOrEmpty(redirectUri)
&& IsHostRelative(redirectUri)) && IsHostRelative(redirectUri))
{ {
var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri);
@ -427,7 +429,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
var redirectUri = new AuthenticationProperties(context.Properties).RedirectUri; var redirectUri = new AuthenticationProperties(context.Properties).RedirectUri;
try try
{ {
if (string.IsNullOrWhiteSpace(redirectUri)) if (string.IsNullOrEmpty(redirectUri))
{ {
redirectUri = redirectUri =
Request.PathBase + Request.PathBase +

View File

@ -39,14 +39,19 @@ namespace Microsoft.AspNet.Authentication.Cookies
/// </summary> /// </summary>
public AuthenticationProperties Properties { get; private set; } public AuthenticationProperties Properties { get; private set; }
/// <summary>
/// If true, the cookie will be renewed
/// </summary>
public bool ShouldRenew { get; set; }
/// <summary> /// <summary>
/// Called to replace the claims principal. The supplied principal will replace the value of the /// Called to replace the claims principal. The supplied principal will replace the value of the
/// Principal property, which determines the identity of the authenticated request. /// Principal property, which determines the identity of the authenticated request.
/// </summary> /// </summary>
/// <param name="identity">The identity used as the replacement</param> /// <param name="identity">The identity used as the replacement</param>
public void ReplacePrincipal(IPrincipal principal) public void ReplacePrincipal(ClaimsPrincipal principal)
{ {
Principal = new ClaimsPrincipal(principal); Principal = principal;
} }
/// <summary> /// <summary>

View File

@ -79,7 +79,7 @@ namespace Microsoft.AspNet.Authentication.OAuth
return context.IsRequestCompleted; return context.IsRequestCompleted;
} }
public override async Task<AuthenticationTicket> AuthenticateAsync() protected override async Task<AuthenticationTicket> AuthenticateAsync()
{ {
AuthenticationProperties properties = null; AuthenticationProperties properties = null;
try try

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Authentication.OAuthBearer
/// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options. /// Searches the 'Authorization' header for a 'Bearer' token. If the 'Bearer' token is found, it is validated using <see cref="TokenValidationParameters"/> set in the options.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public override async Task<AuthenticationTicket> AuthenticateAsync() protected override async Task<AuthenticationTicket> AuthenticateAsync()
{ {
string token = null; string token = null;
try try

View File

@ -201,7 +201,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// </summary> /// </summary>
/// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns> /// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns>
/// <remarks>Uses log id's OIDCH-0000 - OIDCH-0025</remarks> /// <remarks>Uses log id's OIDCH-0000 - OIDCH-0025</remarks>
public override async Task<AuthenticationTicket> AuthenticateAsync() protected override async Task<AuthenticationTicket> AuthenticateAsync()
{ {
Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType()); Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType());

View File

@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
return false; return false;
} }
public override async Task<AuthenticationTicket> AuthenticateAsync() protected override async Task<AuthenticationTicket> AuthenticateAsync()
{ {
AuthenticationProperties properties = null; AuthenticationProperties properties = null;
try try

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNet.Authentication
/// </summary> /// </summary>
public abstract class AuthenticationHandler : IAuthenticationHandler public abstract class AuthenticationHandler : IAuthenticationHandler
{ {
private Task<AuthenticationTicket> _authenticateTask;
private bool _finishCalled; private bool _finishCalled;
private AuthenticationOptions _baseOptions; private AuthenticationOptions _baseOptions;
@ -68,9 +69,10 @@ namespace Microsoft.AspNet.Authentication
Response.OnStarting(OnStartingCallback, this); Response.OnStarting(OnStartingCallback, this);
if (BaseOptions.AutomaticAuthentication) // Automatic authentication is the empty scheme
if (ShouldHandleScheme(string.Empty))
{ {
var ticket = await AuthenticateAsync(); var ticket = await AuthenticateOnceAsync();
if (ticket?.Principal != null) if (ticket?.Principal != null)
{ {
SecurityHelper.AddUserPrincipal(Context, ticket.Principal); SecurityHelper.AddUserPrincipal(Context, ticket.Principal);
@ -155,14 +157,25 @@ namespace Microsoft.AspNet.Authentication
} }
} }
protected Task<AuthenticationTicket> AuthenticateOnceAsync()
{
if (_authenticateTask == null)
{
_authenticateTask = AuthenticateAsync();
}
return _authenticateTask;
}
public async Task AuthenticateAsync(AuthenticateContext context) public async Task AuthenticateAsync(AuthenticateContext context)
{ {
if (ShouldHandleScheme(context.AuthenticationScheme)) if (ShouldHandleScheme(context.AuthenticationScheme))
{ {
var ticket = await AuthenticateAsync(); // Calling Authenticate more than once should always return the original value.
var ticket = await AuthenticateOnceAsync();
if (ticket?.Principal != null) if (ticket?.Principal != null)
{ {
context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items); context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items);
_authenticateTask = Task.FromResult(ticket);
} }
else else
{ {
@ -176,11 +189,7 @@ namespace Microsoft.AspNet.Authentication
} }
} }
/// <summary> protected abstract Task<AuthenticationTicket> AuthenticateAsync();
/// Calling Authenticate more than once should always return the original value.
/// </summary>
/// <returns>The ticket data provided by the authentication logic</returns>
public abstract Task<AuthenticationTicket> AuthenticateAsync();
public bool ShouldHandleScheme(string authenticationScheme) public bool ShouldHandleScheme(string authenticationScheme)
{ {

View File

@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Serializer
writer.Write(principal.Identities.Count()); writer.Write(principal.Identities.Count());
foreach (var identity in principal.Identities) foreach (var identity in principal.Identities)
{ {
var authenticationType = string.IsNullOrWhiteSpace(identity.AuthenticationType) ? string.Empty : identity.AuthenticationType; var authenticationType = string.IsNullOrEmpty(identity.AuthenticationType) ? string.Empty : identity.AuthenticationType;
writer.Write(authenticationType); writer.Write(authenticationType);
WriteWithDefault(writer, identity.NameClaimType, DefaultValues.NameClaimType); WriteWithDefault(writer, identity.NameClaimType, DefaultValues.NameClaimType);
WriteWithDefault(writer, identity.RoleClaimType, DefaultValues.RoleClaimType); WriteWithDefault(writer, identity.RoleClaimType, DefaultValues.RoleClaimType);

View File

@ -61,7 +61,7 @@ namespace Microsoft.AspNet.Authentication
Options.AuthenticationScheme = scheme; Options.AuthenticationScheme = scheme;
} }
public override Task<AuthenticationTicket> AuthenticateAsync() protected override Task<AuthenticationTicket> AuthenticateAsync()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -86,7 +86,7 @@ namespace Microsoft.AspNet.Authentication
Options.AutomaticAuthentication = auto; Options.AutomaticAuthentication = auto;
} }
public override Task<AuthenticationTicket> AuthenticateAsync() protected override Task<AuthenticationTicket> AuthenticateAsync()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -341,6 +341,129 @@ namespace Microsoft.AspNet.Authentication.Cookies
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null); FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null);
} }
[Fact]
public async Task ExpiredCookieWithValidatorStillExpired()
{
var clock = new TestClock();
var server = CreateServer(options =>
{
options.SystemClock = clock;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.Notifications = new CookieAuthenticationNotifications
{
OnValidatePrincipal = ctx =>
{
ctx.ShouldRenew = true;
return Task.FromResult(0);
}
};
},
context =>
context.Authentication.SignInAsync("Cookies",
new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
var transaction1 = await SendAsync(server, "http://example.com/testpath");
clock.Add(TimeSpan.FromMinutes(11));
var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe(null);
}
[Fact]
public async Task CookieCanBeRenewedByValidator()
{
var clock = new TestClock();
var server = CreateServer(options =>
{
options.SystemClock = clock;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.SlidingExpiration = false;
options.Notifications = new CookieAuthenticationNotifications
{
OnValidatePrincipal = ctx =>
{
ctx.ShouldRenew = true;
return Task.FromResult(0);
}
};
},
context =>
context.Authentication.SignInAsync("Cookies",
new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
var transaction1 = await SendAsync(server, "http://example.com/testpath");
var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldNotBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(5));
var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
transaction3.SetCookie.ShouldNotBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(6));
var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction4.SetCookie.ShouldBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null);
clock.Add(TimeSpan.FromMinutes(5));
var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
transaction5.SetCookie.ShouldBe(null);
FindClaimValue(transaction5, ClaimTypes.Name).ShouldBe(null);
}
[Fact]
public async Task CookieCanBeRenewedByValidatorWithSlidingExpiry()
{
var clock = new TestClock();
var server = CreateServer(options =>
{
options.SystemClock = clock;
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.Notifications = new CookieAuthenticationNotifications
{
OnValidatePrincipal = ctx =>
{
ctx.ShouldRenew = true;
return Task.FromResult(0);
}
};
},
context =>
context.Authentication.SignInAsync("Cookies",
new ClaimsPrincipal(new ClaimsIdentity(new GenericIdentity("Alice", "Cookies")))));
var transaction1 = await SendAsync(server, "http://example.com/testpath");
var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldNotBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(5));
var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction2.CookieNameValue);
transaction3.SetCookie.ShouldNotBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(6));
var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction3.CookieNameValue);
transaction4.SetCookie.ShouldNotBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(11));
var transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
transaction5.SetCookie.ShouldBe(null);
FindClaimValue(transaction5, ClaimTypes.Name).ShouldBe(null);
}
[Fact] [Fact]
public async Task CookieExpirationCanBeOverridenInEvent() public async Task CookieExpirationCanBeOverridenInEvent()
{ {
@ -362,19 +485,18 @@ namespace Microsoft.AspNet.Authentication.Cookies
var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction1 = await SendAsync(server, "http://example.com/testpath");
var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(3)); clock.Add(TimeSpan.FromMinutes(3));
var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction3.SetCookie.ShouldBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(3)); clock.Add(TimeSpan.FromMinutes(3));
var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
transaction3.SetCookie.ShouldBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
transaction4.SetCookie.ShouldBe(null); transaction4.SetCookie.ShouldBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null); FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe(null);
} }
@ -393,26 +515,25 @@ namespace Microsoft.AspNet.Authentication.Cookies
var transaction1 = await SendAsync(server, "http://example.com/testpath"); var transaction1 = await SendAsync(server, "http://example.com/testpath");
var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(4)); clock.Add(TimeSpan.FromMinutes(4));
var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); var transaction3 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction3.SetCookie.ShouldBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(4)); clock.Add(TimeSpan.FromMinutes(4));
// transaction4 should arrive with a new SetCookie value // transaction4 should arrive with a new SetCookie value
var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue); var transaction4 = await SendAsync(server, "http://example.com/me/Cookies", transaction1.CookieNameValue);
transaction4.SetCookie.ShouldNotBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe("Alice");
clock.Add(TimeSpan.FromMinutes(4)); clock.Add(TimeSpan.FromMinutes(4));
Transaction transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue); Transaction transaction5 = await SendAsync(server, "http://example.com/me/Cookies", transaction4.CookieNameValue);
transaction2.SetCookie.ShouldBe(null);
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe("Alice");
transaction3.SetCookie.ShouldBe(null);
FindClaimValue(transaction3, ClaimTypes.Name).ShouldBe("Alice");
transaction4.SetCookie.ShouldNotBe(null);
FindClaimValue(transaction4, ClaimTypes.Name).ShouldBe("Alice");
transaction5.SetCookie.ShouldBe(null); transaction5.SetCookie.ShouldBe(null);
FindClaimValue(transaction5, ClaimTypes.Name).ShouldBe("Alice"); FindClaimValue(transaction5, ClaimTypes.Name).ShouldBe("Alice");
} }
@ -427,10 +548,8 @@ namespace Microsoft.AspNet.Authentication.Cookies
}); });
var transaction = await SendAsync(server, "http://example.com/protected", ajaxRequest: true); var transaction = await SendAsync(server, "http://example.com/protected", ajaxRequest: true);
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK);
var responded = transaction.Response.Headers.GetValues("X-Responded-JSON"); var responded = transaction.Response.Headers.GetValues("X-Responded-JSON");
responded.Count().ShouldBe(1); responded.Count().ShouldBe(1);
responded.Single().ShouldContain("\"location\""); responded.Single().ShouldContain("\"location\"");
} }

View File

@ -52,6 +52,5 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Encoder
readTicket.AuthenticationScheme.ShouldBe("Hello"); readTicket.AuthenticationScheme.ShouldBe("Hello");
} }
} }
} }
} }