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? _renewIssuedUtc;
private DateTimeOffset? _renewExpiresUtc; private DateTimeOffset? _renewExpiresUtc;
private string _sessionKey; 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; AuthenticationTicket ticket = null;
try try
{ {
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); ticket = await EnsureCookieTicket();
if (string.IsNullOrEmpty(cookie))
{
return null;
}
ticket = Options.TicketDataFormat.Unprotect(cookie);
if (ticket == null) if (ticket == null)
{ {
Logger.LogWarning(@"Unprotect ticket failed");
return null; 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); var context = new CookieValidatePrincipalContext(Context, ticket, Options);
await Options.Notifications.ValidatePrincipal(context); await Options.Notifications.ValidatePrincipal(context);
if (context.Principal == null)
{
return null;
}
if (context.ShouldRenew) if (context.ShouldRenew)
{ {
_shouldRenew = true; _shouldRenew = true;
@ -135,46 +161,6 @@ namespace Microsoft.AspNet.Authentication.Cookies
return cookieOptions; 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() protected override async Task FinishResponseAsync()
{ {
// Only renew if requested, and neither sign in or sign out was called // Only renew if requested, and neither sign in or sign out was called
@ -183,15 +169,48 @@ namespace Microsoft.AspNet.Authentication.Cookies
return; return;
} }
var ticket = await AuthenticateOnceAsync(); var ticket = await HandleAuthenticateOnceAsync();
try 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) catch (Exception exception)
{ {
var exceptionContext = new CookieExceptionContext(Context, Options, var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.ApplyResponseGrant, exception, ticket); CookieExceptionContext.ExceptionLocation.FinishResponse, exception, ticket);
Options.Notifications.Exception(exceptionContext); Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow) if (exceptionContext.Rethrow)
{ {
@ -202,7 +221,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
protected override async Task HandleSignInAsync(SignInContext signin) protected override async Task HandleSignInAsync(SignInContext signin)
{ {
var model = await AuthenticateAsync(); var ticket = await EnsureCookieTicket();
try try
{ {
var cookieOptions = BuildCookieOptions(); var cookieOptions = BuildCookieOptions();
@ -239,21 +258,21 @@ namespace Microsoft.AspNet.Authentication.Cookies
signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; 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 (Options.SessionStore != null)
{ {
if (_sessionKey != null) if (_sessionKey != null)
{ {
await Options.SessionStore.RemoveAsync(_sessionKey); await Options.SessionStore.RemoveAsync(_sessionKey);
} }
_sessionKey = await Options.SessionStore.StoreAsync(model); _sessionKey = await Options.SessionStore.StoreAsync(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.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( Options.CookieManager.AppendResponseCookie(
Context, Context,
@ -270,36 +289,13 @@ namespace Microsoft.AspNet.Authentication.Cookies
Options.Notifications.ResponseSignedIn(signedInContext); Options.Notifications.ResponseSignedIn(signedInContext);
Response.Headers.Set( var shouldLoginRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
HeaderNameCacheControl, ApplyHeaders(shouldLoginRedirect);
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);
}
}
} }
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.SignIn, exception, ticket);
Options.Notifications.Exception(exceptionContext); Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow) if (exceptionContext.Rethrow)
{ {
@ -310,11 +306,10 @@ namespace Microsoft.AspNet.Authentication.Cookies
protected override async Task HandleSignOutAsync(SignOutContext signOutContext) protected override async Task HandleSignOutAsync(SignOutContext signOutContext)
{ {
var model = await AuthenticateAsync(); var ticket = await EnsureCookieTicket();
try try
{ {
var cookieOptions = BuildCookieOptions(); var cookieOptions = BuildCookieOptions();
if (Options.SessionStore != null && _sessionKey != null) if (Options.SessionStore != null && _sessionKey != null)
{ {
await Options.SessionStore.RemoveAsync(_sessionKey); await Options.SessionStore.RemoveAsync(_sessionKey);
@ -332,43 +327,38 @@ namespace Microsoft.AspNet.Authentication.Cookies
Options.CookieName, Options.CookieName,
context.CookieOptions); context.CookieOptions);
Response.Headers.Set( var shouldLogoutRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath;
HeaderNameCacheControl, ApplyHeaders(shouldLogoutRedirect);
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);
}
}
} }
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.SignOut, exception, ticket);
Options.Notifications.Exception(exceptionContext); Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow) if (exceptionContext.Rethrow)
{ {
throw; 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) private static bool IsHostRelative(string path)
@ -387,36 +377,34 @@ namespace Microsoft.AspNet.Authentication.Cookies
protected override Task<bool> HandleForbiddenAsync(ChallengeContext context) protected override Task<bool> HandleForbiddenAsync(ChallengeContext context)
{ {
// HandleForbidden by redirecting to AccessDeniedPath if set // HandleForbidden by redirecting to AccessDeniedPath if set
if (Options.AccessDeniedPath.HasValue) 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
{ {
return base.HandleForbiddenAsync(context); 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) protected override Task<bool> HandleUnauthorizedAsync([NotNull] ChallengeContext context)
@ -431,27 +419,17 @@ namespace Microsoft.AspNet.Authentication.Cookies
{ {
if (string.IsNullOrEmpty(redirectUri)) if (string.IsNullOrEmpty(redirectUri))
{ {
redirectUri = redirectUri = OriginalPathBase + Request.Path + Request.QueryString;
Request.PathBase +
Request.Path +
Request.QueryString;
} }
var loginUri = var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
Request.Scheme + var redirectContext = new CookieApplyRedirectContext(Context, Options, BuildRedirectUri(loginUri));
"://" +
Request.Host +
Request.PathBase +
Options.LoginPath +
QueryString.Create(Options.ReturnUrlParameter, redirectUri);
var redirectContext = new CookieApplyRedirectContext(Context, Options, loginUri);
Options.Notifications.ApplyRedirect(redirectContext); Options.Notifications.ApplyRedirect(redirectContext);
} }
catch (Exception exception) catch (Exception exception)
{ {
var exceptionContext = new CookieExceptionContext(Context, Options, var exceptionContext = new CookieExceptionContext(Context, Options,
CookieExceptionContext.ExceptionLocation.ApplyResponseChallenge, exception, ticket: null); CookieExceptionContext.ExceptionLocation.Unauthorized, exception, ticket: null);
Options.Notifications.Exception(exceptionContext); Options.Notifications.Exception(exceptionContext);
if (exceptionContext.Rethrow) if (exceptionContext.Rethrow)
{ {

View File

@ -48,14 +48,30 @@ namespace Microsoft.AspNet.Authentication.Cookies
Authenticate, Authenticate,
/// <summary> /// <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> /// </summary>
ApplyResponseGrant, FinishResponse,
/// <summary> /// <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> /// </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> /// <summary>

View File

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

View File

@ -224,7 +224,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>
protected override async Task<AuthenticationTicket> AuthenticateAsync() protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
{ {
Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType()); Logger.LogDebug(Resources.OIDCH_0000_AuthenticateCoreAsync, this.GetType());
@ -903,7 +903,7 @@ namespace Microsoft.AspNet.Authentication.OpenIdConnect
private async Task<bool> InvokeReplyPathAsync() private async Task<bool> InvokeReplyPathAsync()
{ {
var ticket = await AuthenticateAsync(); var ticket = await HandleAuthenticateOnceAsync();
if (ticket != null) if (ticket != null)
{ {
if (ticket.Principal != null) if (ticket.Principal != null)

View File

@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
return false; return false;
} }
protected override async Task<AuthenticationTicket> AuthenticateAsync() protected override async Task<AuthenticationTicket> HandleAuthenticateAsync()
{ {
AuthenticationProperties properties = null; AuthenticationProperties properties = null;
try try
@ -169,7 +169,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
public async Task<bool> InvokeReturnPathAsync() public async Task<bool> InvokeReturnPathAsync()
{ {
var model = await AuthenticateAsync(); var model = await HandleAuthenticateOnceAsync();
if (model == null) if (model == null)
{ {
Logger.LogWarning("Invalid return state, unable to redirect."); 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 OriginalPathBase { get; private set; }
protected PathString OriginalPath { get; private set; }
protected ILogger Logger { get; private set; } protected ILogger Logger { get; private set; }
protected IUrlEncoder UrlEncoder { get; private set; } protected IUrlEncoder UrlEncoder { get; private set; }
@ -62,6 +64,7 @@ namespace Microsoft.AspNet.Authentication
_baseOptions = options; _baseOptions = options;
Context = context; Context = context;
OriginalPathBase = Request.PathBase; OriginalPathBase = Request.PathBase;
OriginalPath = Request.Path;
Logger = logger; Logger = logger;
UrlEncoder = encoder; UrlEncoder = encoder;
@ -72,7 +75,7 @@ namespace Microsoft.AspNet.Authentication
// Automatic authentication is the empty scheme // Automatic authentication is the empty scheme
if (ShouldHandleScheme(string.Empty)) if (ShouldHandleScheme(string.Empty))
{ {
var ticket = await AuthenticateOnceAsync(); var ticket = await HandleAuthenticateOnceAsync();
if (ticket?.Principal != null) if (ticket?.Principal != null)
{ {
Context.User = SecurityHelper.MergeUserPrincipal(Context.User, ticket.Principal); 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) return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) ||
{ (BaseOptions.AutomaticAuthentication && string.IsNullOrEmpty(authenticationScheme));
_authenticateTask = AuthenticateAsync();
}
return _authenticateTask;
} }
public async Task AuthenticateAsync(AuthenticateContext context) public async Task AuthenticateAsync(AuthenticateContext context)
@ -171,11 +171,10 @@ namespace Microsoft.AspNet.Authentication
if (ShouldHandleScheme(context.AuthenticationScheme)) if (ShouldHandleScheme(context.AuthenticationScheme))
{ {
// Calling Authenticate more than once should always return the original value. // Calling Authenticate more than once should always return the original value.
var ticket = await AuthenticateOnceAsync(); var ticket = await HandleAuthenticateOnceAsync();
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
{ {
@ -189,14 +188,17 @@ namespace Microsoft.AspNet.Authentication
} }
} }
protected abstract Task<AuthenticationTicket> AuthenticateAsync(); protected Task<AuthenticationTicket> HandleAuthenticateOnceAsync()
public bool ShouldHandleScheme(string authenticationScheme)
{ {
return string.Equals(BaseOptions.AuthenticationScheme, authenticationScheme, StringComparison.Ordinal) || if (_authenticateTask == null)
(BaseOptions.AutomaticAuthentication && string.IsNullOrWhiteSpace(authenticationScheme)); {
_authenticateTask = HandleAuthenticateAsync();
}
return _authenticateTask;
} }
protected abstract Task<AuthenticationTicket> HandleAuthenticateAsync();
public async Task SignInAsync(SignInContext context) public async Task SignInAsync(SignInContext context)
{ {
if (ShouldHandleScheme(context.AuthenticationScheme)) if (ShouldHandleScheme(context.AuthenticationScheme))
@ -270,7 +272,7 @@ namespace Microsoft.AspNet.Authentication
{ {
case ChallengeBehavior.Automatic: case ChallengeBehavior.Automatic:
// If there is a principal already, invoke the forbidden code path // If there is a principal already, invoke the forbidden code path
var ticket = await AuthenticateAsync(); var ticket = await HandleAuthenticateOnceAsync();
if (ticket?.Principal != null) if (ticket?.Principal != null)
{ {
handled = await HandleForbiddenAsync(context); handled = await HandleForbiddenAsync(context);

View File

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

View File

@ -32,6 +32,20 @@ namespace Microsoft.AspNet.Authentication
Assert.True(handler.ShouldHandleScheme("")); 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] [Fact]
public void AutomaticHandlerShouldHandleSchemeWhenSchemeMatches() public void AutomaticHandlerShouldHandleSchemeWhenSchemeMatches()
{ {
@ -53,6 +67,37 @@ namespace Microsoft.AspNet.Authentication
Assert.False(handler.ShouldHandleScheme("Alpha")); 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> private class TestHandler : AuthenticationHandler<AuthenticationOptions>
{ {
public TestHandler(string scheme) public TestHandler(string scheme)
@ -61,7 +106,7 @@ namespace Microsoft.AspNet.Authentication
Options.AuthenticationScheme = scheme; Options.AuthenticationScheme = scheme;
} }
protected override Task<AuthenticationTicket> AuthenticateAsync() protected override Task<AuthenticationTicket> HandleAuthenticateAsync()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -86,7 +131,7 @@ namespace Microsoft.AspNet.Authentication
Options.AutomaticAuthentication = auto; Options.AutomaticAuthentication = auto;
} }
protected override Task<AuthenticationTicket> AuthenticateAsync() protected override Task<AuthenticationTicket> HandleAuthenticateAsync()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -371,6 +371,36 @@ namespace Microsoft.AspNet.Authentication.Cookies
FindClaimValue(transaction2, ClaimTypes.Name).ShouldBe(null); 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] [Fact]
public async Task CookieCanBeRenewedByValidator() public async Task CookieCanBeRenewedByValidator()
{ {
@ -671,6 +701,149 @@ namespace Microsoft.AspNet.Authentication.Cookies
transaction2.Response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); 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) private static string FindClaimValue(Transaction transaction, string claimType)
{ {
var claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == 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. // 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. // 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.IO;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
@ -14,7 +15,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Encoder
public class TicketSerializerTests public class TicketSerializerTests
{ {
[Fact] [Fact]
public void CanRoundTripNullPrincipal() public void NullPrincipalThrows()
{ {
var properties = new AuthenticationProperties(); var properties = new AuthenticationProperties();
properties.RedirectUri = "bye"; properties.RedirectUri = "bye";
@ -24,12 +25,7 @@ namespace Microsoft.AspNet.Authentication.DataHandler.Encoder
using (var writer = new BinaryWriter(stream)) using (var writer = new BinaryWriter(stream))
using (var reader = new BinaryReader(stream)) using (var reader = new BinaryReader(stream))
{ {
TicketSerializer.Write(writer, ticket); Assert.Throws<ArgumentNullException>(() => 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");
} }
} }