Fix cookie bugs, Authenticate => HandleAuthenticate

This commit is contained in:
Hao Kung 2015-07-14 13:22:04 -07:00
parent 5065835a05
commit ab4ba794e5
11 changed files with 469 additions and 261 deletions

View File

@ -27,75 +27,101 @@ namespace Microsoft.AspNet.Authentication.Cookies
private DateTimeOffset? _renewIssuedUtc;
private DateTimeOffset? _renewExpiresUtc;
private string _sessionKey;
private Task<AuthenticationTicket> _cookieTicketTask;
protected override async Task<AuthenticationTicket> AuthenticateAsync()
private Task<AuthenticationTicket> EnsureCookieTicket()
{
// We only need to read the ticket once
if (_cookieTicketTask == null)
{
_cookieTicketTask = ReadCookieTicket();
}
return _cookieTicketTask;
}
private async Task<AuthenticationTicket> ReadCookieTicket()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName);
if (string.IsNullOrEmpty(cookie))
{
return null;
}
var ticket = Options.TicketDataFormat.Unprotect(cookie);
if (ticket == null)
{
Logger.LogWarning(@"Unprotect ticket failed");
return null;
}
if (Options.SessionStore != null)
{
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
if (claim == null)
{
Logger.LogWarning(@"SessionId missing");
return null;
}
_sessionKey = claim.Value;
ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
if (ticket == null)
{
Logger.LogWarning(@"Identity missing in session store");
return null;
}
}
var currentUtc = Options.SystemClock.UtcNow;
var issuedUtc = ticket.Properties.IssuedUtc;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
if (Options.SessionStore != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
return null;
}
var allowRefresh = ticket.Properties.AllowRefresh ?? true;
if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
var timeRemaining = expiresUtc.Value.Subtract(currentUtc);
if (timeRemaining < timeElapsed)
{
_shouldRenew = true;
_renewIssuedUtc = currentUtc;
var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value);
_renewExpiresUtc = currentUtc.Add(timeSpan);
}
}
// Finally we have a valid ticket
return ticket;
}
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
{
AuthenticationTicket ticket = null;
try
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName);
if (string.IsNullOrEmpty(cookie))
{
return null;
}
ticket = Options.TicketDataFormat.Unprotect(cookie);
ticket = await EnsureCookieTicket();
if (ticket == null)
{
Logger.LogWarning(@"Unprotect ticket failed");
return null;
}
if (Options.SessionStore != null)
{
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
if (claim == null)
{
Logger.LogWarning(@"SessionId missing");
return null;
}
_sessionKey = claim.Value;
ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
if (ticket == null)
{
Logger.LogWarning(@"Identity missing in session store");
return null;
}
}
var currentUtc = Options.SystemClock.UtcNow;
var issuedUtc = ticket.Properties.IssuedUtc;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
if (Options.SessionStore != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
return null;
}
var allowRefresh = ticket.Properties.AllowRefresh ?? true;
if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
var timeRemaining = expiresUtc.Value.Subtract(currentUtc);
if (timeRemaining < timeElapsed)
{
_shouldRenew = true;
_renewIssuedUtc = currentUtc;
var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value);
_renewExpiresUtc = currentUtc.Add(timeSpan);
}
}
var context = new CookieValidatePrincipalContext(Context, ticket, Options);
await Options.Notifications.ValidatePrincipal(context);
if (context.Principal == null)
{
return null;
}
if (context.ShouldRenew)
{
_shouldRenew = true;
@ -135,46 +161,6 @@ namespace Microsoft.AspNet.Authentication.Cookies
return cookieOptions;
}
private async Task ApplyCookie(AuthenticationTicket ticket)
{
if (_renewIssuedUtc.HasValue)
{
ticket.Properties.IssuedUtc = _renewIssuedUtc;
}
if (_renewExpiresUtc.HasValue)
{
ticket.Properties.ExpiresUtc = _renewExpiresUtc;
}
if (Options.SessionStore != null && _sessionKey != null)
{
await Options.SessionStore.RenewAsync(_sessionKey, ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.AuthenticationScheme));
ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme);
}
var cookieValue = Options.TicketDataFormat.Protect(ticket);
var cookieOptions = BuildCookieOptions();
if (ticket.Properties.IsPersistent && _renewExpiresUtc.HasValue)
{
cookieOptions.Expires = _renewExpiresUtc.Value.ToUniversalTime().DateTime;
}
Options.CookieManager.AppendResponseCookie(
Context,
Options.CookieName,
cookieValue,
cookieOptions);
Response.Headers.Set(HeaderNameCacheControl, HeaderValueNoCache);
Response.Headers.Set(HeaderNamePragma, HeaderValueNoCache);
Response.Headers.Set(HeaderNameExpires, HeaderValueMinusOne);
}
protected override async Task FinishResponseAsync()
{
// Only renew if requested, and neither sign in or sign out was called
@ -183,15 +169,48 @@ namespace Microsoft.AspNet.Authentication.Cookies
return;
}
var ticket = await AuthenticateOnceAsync();
var ticket = await HandleAuthenticateOnceAsync();
try
{
await ApplyCookie(ticket);
if (_renewIssuedUtc.HasValue)
{
ticket.Properties.IssuedUtc = _renewIssuedUtc;
}
if (_renewExpiresUtc.HasValue)
{
ticket.Properties.ExpiresUtc = _renewExpiresUtc;
}
if (Options.SessionStore != null && _sessionKey != null)
{
await Options.SessionStore.RenewAsync(_sessionKey, ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.AuthenticationScheme));
ticket = new AuthenticationTicket(principal, null, Options.AuthenticationScheme);
}
var cookieValue = Options.TicketDataFormat.Protect(ticket);
var cookieOptions = BuildCookieOptions();
if (ticket.Properties.IsPersistent && _renewExpiresUtc.HasValue)
{
cookieOptions.Expires = _renewExpiresUtc.Value.ToUniversalTime().DateTime;
}
Options.CookieManager.AppendResponseCookie(
Context,
Options.CookieName,
cookieValue,
cookieOptions);
ApplyHeaders();
}
catch (Exception exception)
{
var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, ticket);
CookieExceptionContext.ExceptionLocation.FinishResponse, exception, ticket);
Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow)
{
@ -202,7 +221,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
protected override async Task HandleSignInAsync(SignInContext signin)
{
var model = await AuthenticateAsync();
var ticket = await EnsureCookieTicket();
try
{
var cookieOptions = BuildCookieOptions();
@ -239,21 +258,21 @@ namespace Microsoft.AspNet.Authentication.Cookies
signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime;
}
model = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme);
ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.AuthenticationScheme);
if (Options.SessionStore != null)
{
if (_sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
_sessionKey = await Options.SessionStore.StoreAsync(model);
_sessionKey = await Options.SessionStore.StoreAsync(ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.ClaimsIssuer));
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);
Options.CookieManager.AppendResponseCookie(
Context,
@ -270,36 +289,13 @@ namespace Microsoft.AspNet.Authentication.Cookies
Options.Notifications.ResponseSignedIn(signedInContext);
Response.Headers.Set(
HeaderNameCacheControl,
HeaderValueNoCache);
Response.Headers.Set(
HeaderNamePragma,
HeaderValueNoCache);
Response.Headers.Set(
HeaderNameExpires,
HeaderValueMinusOne);
var shouldLoginRedirect = Options.LoginPath.HasValue && Request.Path == Options.LoginPath;
if ((shouldLoginRedirect) && Response.StatusCode == 200)
{
var query = Request.Query;
var redirectUri = query.Get(Options.ReturnUrlParameter);
if (!string.IsNullOrEmpty(redirectUri)
&& IsHostRelative(redirectUri))
{
var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri);
Options.Notifications.ApplyRedirect(redirectContext);
}
}
var shouldLoginRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
ApplyHeaders(shouldLoginRedirect);
}
catch (Exception exception)
{
var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model);
CookieExceptionContext.ExceptionLocation.SignIn, exception, ticket);
Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow)
{
@ -310,11 +306,10 @@ namespace Microsoft.AspNet.Authentication.Cookies
protected override async Task HandleSignOutAsync(SignOutContext signOutContext)
{
var model = await AuthenticateAsync();
var ticket = await EnsureCookieTicket();
try
{
var cookieOptions = BuildCookieOptions();
if (Options.SessionStore != null && _sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
@ -332,43 +327,38 @@ namespace Microsoft.AspNet.Authentication.Cookies
Options.CookieName,
context.CookieOptions);
Response.Headers.Set(
HeaderNameCacheControl,
HeaderValueNoCache);
Response.Headers.Set(
HeaderNamePragma,
HeaderValueNoCache);
Response.Headers.Set(
HeaderNameExpires,
HeaderValueMinusOne);
var shouldLogoutRedirect = Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath;
if (shouldLogoutRedirect && Response.StatusCode == 200)
{
var query = Request.Query;
var redirectUri = query.Get(Options.ReturnUrlParameter);
if (!string.IsNullOrEmpty(redirectUri)
&& IsHostRelative(redirectUri))
{
var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri);
Options.Notifications.ApplyRedirect(redirectContext);
}
}
var shouldLogoutRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath;
ApplyHeaders(shouldLogoutRedirect);
}
catch (Exception exception)
{
var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, model);
CookieExceptionContext.ExceptionLocation.SignOut, exception, ticket);
Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow)
{
throw;
}
}
}
private void ApplyHeaders(bool shouldRedirectToReturnUrl = false)
{
Response.Headers.Set(HeaderNameCacheControl, HeaderValueNoCache);
Response.Headers.Set(HeaderNamePragma, HeaderValueNoCache);
Response.Headers.Set(HeaderNameExpires, HeaderValueMinusOne);
if (shouldRedirectToReturnUrl && Response.StatusCode == 200)
{
var query = Request.Query;
var redirectUri = query.Get(Options.ReturnUrlParameter);
if (!string.IsNullOrEmpty(redirectUri)
&& IsHostRelative(redirectUri))
{
var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri);
Options.Notifications.ApplyRedirect(redirectContext);
}
}
}
private static bool IsHostRelative(string path)
@ -387,36 +377,34 @@ namespace Microsoft.AspNet.Authentication.Cookies
protected override Task<bool> HandleForbiddenAsync(ChallengeContext context)
{
// HandleForbidden by redirecting to AccessDeniedPath if set
if (Options.AccessDeniedPath.HasValue)
{
try
{
var accessDeniedUri =
Request.Scheme +
"://" +
Request.Host +
Request.PathBase +
Options.AccessDeniedPath;
var redirectContext = new CookieApplyRedirectContext(Context, Options, accessDeniedUri);
Options.Notifications.ApplyRedirect(redirectContext);
}
catch (Exception exception)
{
var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null);
Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow)
{
throw;
}
}
return Task.FromResult(true);
}
else
if (!Options.AccessDeniedPath.HasValue)
{
return base.HandleForbiddenAsync(context);
}
try
{
var accessDeniedUri =
Request.Scheme +
"://" +
Request.Host +
OriginalPathBase +
Options.AccessDeniedPath;
var redirectContext = new CookieApplyRedirectContext(Context, Options, accessDeniedUri);
Options.Notifications.ApplyRedirect(redirectContext);
}
catch (Exception exception)
{
var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.Forbidden, exception, ticket: null);
Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow)
{
throw;
}
}
return Task.FromResult(true);
}
protected override Task<bool> HandleUnauthorizedAsync([NotNull] ChallengeContext context)
@ -431,27 +419,17 @@ namespace Microsoft.AspNet.Authentication.Cookies
{
if (string.IsNullOrEmpty(redirectUri))
{
redirectUri =
Request.PathBase +
Request.Path +
Request.QueryString;
redirectUri = OriginalPathBase + Request.Path + Request.QueryString;
}
var loginUri =
Request.Scheme +
"://" +
Request.Host +
Request.PathBase +
Options.LoginPath +
QueryString.Create(Options.ReturnUrlParameter, redirectUri);
var redirectContext = new CookieApplyRedirectContext(Context, Options, loginUri);
var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
var redirectContext = new CookieApplyRedirectContext(Context, Options, BuildRedirectUri(loginUri));
Options.Notifications.ApplyRedirect(redirectContext);
}
catch (Exception exception)
{
var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null);
CookieExceptionContext.ExceptionLocation.Unauthorized, exception, ticket: null);
Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow)
{

View File

@ -48,14 +48,30 @@ namespace Microsoft.AspNet.Authentication.Cookies
Authenticate,
/// <summary>
/// The exception was reported in the ApplyResponseGrant code path, during sign-in, sign-out, or refresh.
/// The exception was reported in the FinishResponse code path, during sign-in, sign-out, or refresh.
/// </summary>
ApplyResponseGrant,
FinishResponse,
/// <summary>
/// The exception was reported in the ApplyResponseChallenge code path, during redirect generation.
/// The exception was reported in the Unauthorized code path, during redirect generation.
/// </summary>
ApplyResponseChallenge,
Unauthorized,
/// <summary>
/// The exception was reported in the Forbidden code path, during redirect generation.
/// </summary>
Forbidden,
/// <summary>
/// The exception was reported in the SignIn code path
/// </summary>
SignIn,
/// <summary>
/// The exception was reported in the SignOut code path
/// </summary>
SignOut,
}
/// <summary>

View File

@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Authentication.OAuth
public async Task<bool> InvokeReturnPathAsync()
{
var ticket = await AuthenticateAsync();
var ticket = await HandleAuthenticateOnceAsync();
if (ticket == null)
{
Logger.LogWarning("Invalid return state, unable to redirect.");
@ -78,7 +78,7 @@ namespace Microsoft.AspNet.Authentication.OAuth
return context.IsRequestCompleted;
}
protected override async Task<AuthenticationTicket> AuthenticateAsync()
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
{
AuthenticationProperties properties = null;
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.
/// </summary>
/// <returns></returns>
protected override async Task<AuthenticationTicket> AuthenticateAsync()
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
{
string token = null;
try

View File

@ -224,7 +224,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
/// </summary>
/// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns>
/// <remarks>Uses log id's OIDCH-0000 - OIDCH-0025</remarks>
protected override async Task<AuthenticationTicket> AuthenticateAsync()
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
{
Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType());
@ -903,7 +903,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
private async Task<bool> InvokeReplyPathAsync()
{
var ticket = await AuthenticateAsync();
var ticket = await HandleAuthenticateOnceAsync();
if (ticket != null)
{
if (ticket.Principal != null)

View File

@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
return false;
}
protected override async Task<AuthenticationTicket> AuthenticateAsync()
protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
{
AuthenticationProperties properties = null;
try
@ -169,7 +169,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
public async Task<bool> InvokeReturnPathAsync()
{
var model = await AuthenticateAsync();
var model = await HandleAuthenticateOnceAsync();
if (model == null)
{
Logger.LogWarning("Invalid return state, unable to redirect.");

View File

@ -38,6 +38,8 @@ namespace Microsoft.AspNet.Authentication
protected PathString OriginalPathBase { get; private set; }
protected PathString OriginalPath { get; private set; }
protected ILogger Logger { get; private set; }
protected IUrlEncoder UrlEncoder { get; private set; }
@ -62,6 +64,7 @@ namespace Microsoft.AspNet.Authentication
_baseOptions = options;
Context = context;
OriginalPathBase = Request.PathBase;
OriginalPath = Request.Path;
Logger = logger;
UrlEncoder = encoder;
@ -72,7 +75,7 @@ namespace Microsoft.AspNet.Authentication
// Automatic authentication is the empty scheme
if (ShouldHandleScheme(string.Empty))
{
var ticket = await AuthenticateOnceAsync();
var ticket = await HandleAuthenticateOnceAsync();
if (ticket?.Principal != null)
{
Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal);
@ -157,13 +160,10 @@ namespace Microsoft.AspNet.Authentication
}
}
protected Task<AuthenticationTicket> AuthenticateOnceAsync()
public bool ShouldHandleScheme(string authenticationScheme)
{
if (_authenticateTask == null)
{
_authenticateTask = AuthenticateAsync();
}
return _authenticateTask;
return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) ||
(BaseOptions.AutomaticAuthentication && string.IsNullOrEmpty(authenticationScheme));
}
public async Task AuthenticateAsync(AuthenticateContext context)
@ -171,11 +171,10 @@ namespace Microsoft.AspNet.Authentication
if (ShouldHandleScheme(context.AuthenticationScheme))
{
// Calling Authenticate more than once should always return the original value.
var ticket = await AuthenticateOnceAsync();
var ticket = await HandleAuthenticateOnceAsync();
if (ticket?.Principal != null)
{
context.Authenticated(ticket.Principal, ticket.Properties.Items, BaseOptions.Description.Items);
_authenticateTask = Task.FromResult(ticket);
}
else
{
@ -189,14 +188,17 @@ namespace Microsoft.AspNet.Authentication
}
}
protected abstract Task<AuthenticationTicket> AuthenticateAsync();
public bool ShouldHandleScheme(string authenticationScheme)
protected Task<AuthenticationTicket> HandleAuthenticateOnceAsync()
{
return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) ||
(BaseOptions.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme));
if (_authenticateTask == null)
{
_authenticateTask = HandleAuthenticateAsync();
}
return _authenticateTask;
}
protected abstract Task<AuthenticationTicket> HandleAuthenticateAsync();
public async Task SignInAsync(SignInContext context)
{
if (ShouldHandleScheme(context.AuthenticationScheme))
@ -270,7 +272,7 @@ namespace Microsoft.AspNet.Authentication
{
case ChallengeBehavior.Automatic:
// If there is a principal already, invoke the forbidden code path
var ticket = await AuthenticateAsync();
var ticket = await HandleAuthenticateOnceAsync();
if (ticket?.Principal != null)
{
handled = await HandleForbiddenAsync(context);

View File

@ -43,8 +43,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Serializer
var principal = model.Principal;
if (principal == null)
{
// Use -1 to signal null
writer.Write(-1);
throw new ArgumentNullException("model.Principal");
}
else
{
@ -75,37 +74,36 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Serializer
{
return null;
}
string authenticationScheme = reader.ReadString();
int identityCount = reader.ReadInt32();
ClaimsPrincipal principal = null;
var authenticationScheme = reader.ReadString();
var identityCount = reader.ReadInt32();
// Negative values are used to signify null
if (identityCount >= 0)
if (identityCount < 0)
{
var identities = new ClaimsIdentity[identityCount];
for (int i = 0; i != identityCount; ++i)
return null;
}
var identities = new ClaimsIdentity[identityCount];
for (var i = 0; i != identityCount; ++i)
{
var authenticationType = reader.ReadString();
var nameClaimType = ReadWithDefault(reader, DefaultValues.NameClaimType);
var roleClaimType = ReadWithDefault(reader, DefaultValues.RoleClaimType);
var count = reader.ReadInt32();
var claims = new Claim[count];
for (int index = 0; index != count; ++index)
{
string authenticationType = reader.ReadString();
string nameClaimType = ReadWithDefault(reader, DefaultValues.NameClaimType);
string roleClaimType = ReadWithDefault(reader, DefaultValues.RoleClaimType);
int count = reader.ReadInt32();
var claims = new Claim[count];
for (int index = 0; index != count; ++index)
{
string type = ReadWithDefault(reader, nameClaimType);
string value = reader.ReadString();
string valueType = ReadWithDefault(reader, DefaultValues.StringValueType);
string issuer = ReadWithDefault(reader, DefaultValues.LocalAuthority);
string originalIssuer = ReadWithDefault(reader, issuer);
claims[index] = new Claim(type, value, valueType, issuer, originalIssuer);
}
identities[i] = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType);
var type = ReadWithDefault(reader, nameClaimType);
var value = reader.ReadString();
var valueType = ReadWithDefault(reader, DefaultValues.StringValueType);
var issuer = ReadWithDefault(reader, DefaultValues.LocalAuthority);
var originalIssuer = ReadWithDefault(reader, issuer);
claims[index] = new Claim(type, value, valueType, issuer, originalIssuer);
}
principal = new ClaimsPrincipal(identities);
identities[i] = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType);
}
var properties = PropertiesSerializer.Read(reader);
return new AuthenticationTicket(principal, properties, authenticationScheme);
return new AuthenticationTicket(new ClaimsPrincipal(identities), properties, authenticationScheme);
}
private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue)
@ -122,7 +120,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Serializer
private static string ReadWithDefault(BinaryReader reader, string defaultValue)
{
string value = reader.ReadString();
var value = reader.ReadString();
if (string.Equals(value, DefaultValues.DefaultStringPlaceholder, StringComparison.Ordinal))
{
return defaultValue;

View File

@ -32,6 +32,20 @@ namespace Microsoft.AspNet.Authentication
Assert.True(handler.ShouldHandleScheme(""));
}
[Fact]
public void AutomaticHandlerHandlesNullScheme()
{
var handler = new TestAutoHandler("ignored", true);
Assert.True(handler.ShouldHandleScheme(null));
}
[Fact]
public void AutomaticHandlerIgnoresWhitespaceScheme()
{
var handler = new TestAutoHandler("ignored", true);
Assert.False(handler.ShouldHandleScheme(" "));
}
[Fact]
public void AutomaticHandlerShouldHandleSchemeWhenSchemeMatches()
{
@ -53,6 +67,37 @@ namespace Microsoft.AspNet.Authentication
Assert.False(handler.ShouldHandleScheme("Alpha"));
}
[Theory]
[InlineData("Alpha")]
[InlineData("")]
public async Task AuthHandlerAuthenticateCachesTicket(string scheme)
{
var handler = new CountHandler(scheme);
var context = new AuthenticateContext(scheme);
await handler.AuthenticateAsync(context);
await handler.AuthenticateAsync(context);
Assert.Equal(1, handler.AuthCount);
}
private class CountHandler : AuthenticationHandler<AuthenticationOptions>
{
public int AuthCount = 0;
public CountHandler(string scheme)
{
Initialize(new TestOptions(), new DefaultHttpContext(), new LoggerFactory().CreateLogger("TestHandler"), Framework.WebEncoders.UrlEncoder.Default);
Options.AuthenticationScheme = scheme;
Options.AutomaticAuthentication = true;
}
protected override Task<AuthenticationTicket> HandleAuthenticateAsync()
{
AuthCount++;
return Task.FromResult(new AuthenticationTicket(null, null));
}
}
private class TestHandler : AuthenticationHandler<AuthenticationOptions>
{
public TestHandler(string scheme)
@ -61,7 +106,7 @@ namespace Microsoft.AspNet.Authentication
Options.AuthenticationScheme = scheme;
}
protected override Task<AuthenticationTicket> AuthenticateAsync()
protected override Task<AuthenticationTicket> HandleAuthenticateAsync()
{
throw new NotImplementedException();
}
@ -86,7 +131,7 @@ namespace Microsoft.AspNet.Authentication
Options.AutomaticAuthentication = auto;
}
protected override Task<AuthenticationTicket> AuthenticateAsync()
protected override Task<AuthenticationTicket> HandleAuthenticateAsync()
{
throw new NotImplementedException();
}

View File

@ -371,6 +371,36 @@ namespace Microsoft.AspNet.Authentication.Cookies
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe(null);
}
[Fact]
public async Task CookieCanBeRejectedAndSignedOutByValidator()
{
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.RejectPrincipal();
ctx.HttpContext.Authentication.SignOutAsync("Cookies");
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.ShouldContain(".AspNet.Cookies=; expires=");
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe(null);
}
[Fact]
public async Task CookieCanBeRenewedByValidator()
{
@ -671,6 +701,149 @@ namespace Microsoft.AspNet.Authentication.Cookies
transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task MapWillNotAffectChallenge()
{
var server = TestServer.Create(app =>
{
app.UseCookieAuthentication(options => options.LoginPath = new PathString("/page"));
app.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" })));
},
services => services.AddAuthentication());
var transaction = await server.SendAsync("http://example.com/login");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
var location = transaction.Response.Headers.Location;
location.LocalPath.ShouldBe("/page");
location.Query.ShouldBe("?ReturnUrl=%2F");
}
[Fact]
public async Task MapWithSignInOnlyRedirectToReturnUrlOnLoginPath()
{
var server = TestServer.Create(app =>
{
app.UseCookieAuthentication(options => options.LoginPath = new PathString("/login"));
app.Map("/notlogin", signoutApp => signoutApp.Run(context => context.Authentication.SignInAsync("Cookies",
new ClaimsPrincipal())));
},
services => services.AddAuthentication());
var transaction = await server.SendAsync("http://example.com/notlogin?ReturnUrl=%2Fpage");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK);
transaction.SetCookie.ShouldNotBe(null);
}
[Fact]
public async Task MapWillNotAffectSignInRedirectToReturnUrl()
{
var server = TestServer.Create(app =>
{
app.UseCookieAuthentication(options => options.LoginPath = new PathString("/login"));
app.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.SignInAsync("Cookies",
new ClaimsPrincipal())));
},
services => services.AddAuthentication());
var transaction = await server.SendAsync("http://example.com/login?ReturnUrl=%2Fpage");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
transaction.SetCookie.ShouldNotBe(null);
var location = transaction.Response.Headers.Location;
location.OriginalString.ShouldBe("/page");
}
[Fact]
public async Task MapWithSignOutOnlyRedirectToReturnUrlOnLogoutPath()
{
var server = TestServer.Create(app =>
{
app.UseCookieAuthentication(options => options.LogoutPath = new PathString("/logout"));
app.Map("/notlogout", signoutApp => signoutApp.Run(context => context.Authentication.SignOutAsync("Cookies")));
},
services => services.AddAuthentication());
var transaction = await server.SendAsync("http://example.com/notlogout?ReturnUrl=%2Fpage");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK);
transaction.SetCookie[0].ShouldContain(".AspNet.Cookies=; expires=");
}
[Fact]
public async Task MapWillNotAffectSignOutRedirectToReturnUrl()
{
var server = TestServer.Create(app =>
{
app.UseCookieAuthentication(options => options.LogoutPath = new PathString("/logout"));
app.Map("/logout", signoutApp => signoutApp.Run(context => context.Authentication.SignOutAsync("Cookies")));
},
services => services.AddAuthentication());
var transaction = await server.SendAsync("http://example.com/logout?ReturnUrl=%2Fpage");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
transaction.SetCookie[0].ShouldContain(".AspNet.Cookies=; expires=");
var location = transaction.Response.Headers.Location;
location.OriginalString.ShouldBe("/page");
}
[Fact]
public async Task MapWillNotAffectAccessDenied()
{
var server = TestServer.Create(app =>
{
app.UseCookieAuthentication(options => options.AccessDeniedPath = new PathString("/denied"));
app.Map("/forbid", signoutApp => signoutApp.Run(context => context.Authentication.ForbidAsync("Cookies")));
},
services => services.AddAuthentication());
var transaction = await server.SendAsync("http://example.com/forbid");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
var location = transaction.Response.Headers.Location;
location.LocalPath.ShouldBe("/denied");
}
[Fact]
public async Task NestedMapWillNotAffectLogin()
{
var server = TestServer.Create(app =>
app.Map("/base", map =>
{
map.UseCookieAuthentication(options => options.LoginPath = new PathString("/page"));
map.Map("/login", signoutApp => signoutApp.Run(context => context.Authentication.ChallengeAsync("Cookies", new AuthenticationProperties() { RedirectUri = "/" })));
}),
services => services.AddAuthentication());
var transaction = await server.SendAsync("http://example.com/base/login");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
var location = transaction.Response.Headers.Location;
location.LocalPath.ShouldBe("/base/page");
location.Query.ShouldBe("?ReturnUrl=%2F");
}
[Fact]
public async Task NestedMapWillNotAffectAccessDenied()
{
var server = TestServer.Create(app =>
app.Map("/base", map =>
{
map.UseCookieAuthentication(options => options.AccessDeniedPath = new PathString("/denied"));
map.Map("/forbid", signoutApp => signoutApp.Run(context => context.Authentication.ForbidAsync("Cookies")));
}),
services => services.AddAuthentication());
var transaction = await server.SendAsync("http://example.com/base/forbid");
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
var location = transaction.Response.Headers.Location;
location.LocalPath.ShouldBe("/base/denied");
}
private static string FindClaimValue(Transaction transaction, string claimType)
{
var claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType);

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Security.Claims;
@ -14,7 +15,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Encoder
public class TicketSerializerTests
{
[Fact]
public void CanRoundTripNullPrincipal()
public void NullPrincipalThrows()
{
var properties = new AuthenticationProperties();
properties.RedirectUri = "bye";
@ -24,12 +25,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Encoder
using (var writer = new BinaryWriter(stream))
using (var reader = new BinaryReader(stream))
{
TicketSerializer.Write(writer, ticket);
stream.Position = 0;
var readTicket = TicketSerializer.Read(reader);
readTicket.Principal.ShouldBe(null);
readTicket.Properties.RedirectUri.ShouldBe("bye");
readTicket.AuthenticationScheme.ShouldBe("Hello");
Assert.Throws<ArgumentNullException>(() => TicketSerializer.Write(writer, ticket));
}
}