aspnetcore/src/Identity/SecurityStampValidator.cs

166 lines
7.0 KiB
C#

// 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.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Provides default implementation of validation functions for security stamps.
/// </summary>
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public class SecurityStampValidator<TUser> : ISecurityStampValidator where TUser : class
{
/// <summary>
/// Creates a new instance of <see cref="SecurityStampValidator{TUser}"/>.
/// </summary>
/// <param name="options">Used to access the <see cref="IdentityOptions"/>.</param>
/// <param name="signInManager">The <see cref="SignInManager{TUser}"/>.</param>
/// <param name="clock">The system clock.</param>
public SecurityStampValidator(IOptions<SecurityStampValidatorOptions> options, SignInManager<TUser> signInManager, ISystemClock clock)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (signInManager == null)
{
throw new ArgumentNullException(nameof(signInManager));
}
SignInManager = signInManager;
Options = options.Value;
Clock = clock;
}
/// <summary>
/// The SignInManager.
/// </summary>
public SignInManager<TUser> SignInManager { get; }
/// <summary>
/// The <see cref="SecurityStampValidatorOptions"/>.
/// </summary>
public SecurityStampValidatorOptions Options { get; }
/// <summary>
/// The <see cref="ISystemClock"/>.
/// </summary>
public ISystemClock Clock { get; }
/// <summary>
/// Called when the security stamp has been verified.
/// </summary>
/// <param name="user">The user who has been verified.</param>
/// <param name="context">The <see cref="CookieValidatePrincipalContext"/>.</param>
/// <returns>A task.</returns>
protected virtual async Task SecurityStampVerified(TUser user, CookieValidatePrincipalContext context)
{
var newPrincipal = await SignInManager.CreateUserPrincipalAsync(user);
if (Options.OnRefreshingPrincipal != null)
{
var replaceContext = new SecurityStampRefreshingPrincipalContext
{
CurrentPrincipal = context.Principal,
NewPrincipal = newPrincipal
};
// Note: a null principal is allowed and results in a failed authentication.
await Options.OnRefreshingPrincipal(replaceContext);
newPrincipal = replaceContext.NewPrincipal;
}
// REVIEW: note we lost login authentication method
context.ReplacePrincipal(newPrincipal);
context.ShouldRenew = true;
}
/// <summary>
/// Verifies the principal's security stamp, returns the matching user if successful
/// </summary>
/// <param name="principal">The principal to verify.</param>
/// <returns>The verified user or null if verification fails.</returns>
protected virtual Task<TUser> VerifySecurityStamp(ClaimsPrincipal principal)
=> SignInManager.ValidateSecurityStampAsync(principal);
/// <summary>
/// Validates a security stamp of an identity as an asynchronous operation, and rebuilds the identity if the validation succeeds, otherwise rejects
/// the identity.
/// </summary>
/// <param name="context">The context containing the <see cref="System.Security.Claims.ClaimsPrincipal"/>
/// and <see cref="Http.Authentication.AuthenticationProperties"/> to validate.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous validation operation.</returns>
public virtual async Task ValidateAsync(CookieValidatePrincipalContext context)
{
var currentUtc = DateTimeOffset.UtcNow;
if (context.Options != null && Clock != null)
{
currentUtc = Clock.UtcNow;
}
var issuedUtc = context.Properties.IssuedUtc;
// Only validate if enough time has elapsed
var validate = (issuedUtc == null);
if (issuedUtc != null)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > Options.ValidationInterval;
}
if (validate)
{
var user = await VerifySecurityStamp(context.Principal);
if (user != null)
{
await SecurityStampVerified(user, context);
}
else
{
context.RejectPrincipal();
await SignInManager.SignOutAsync();
}
}
}
}
/// <summary>
/// Static helper class used to configure a CookieAuthenticationNotifications to validate a cookie against a user's security
/// stamp.
/// </summary>
public static class SecurityStampValidator
{
/// <summary>
/// Validates a principal against a user's stored security stamp.
/// </summary>
/// <param name="context">The context containing the <see cref="System.Security.Claims.ClaimsPrincipal"/>
/// and <see cref="AuthenticationProperties"/> to validate.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous validation operation.</returns>
public static Task ValidatePrincipalAsync(CookieValidatePrincipalContext context)
=> ValidateAsync<ISecurityStampValidator>(context);
/// <summary>
/// Used to validate the <see cref="IdentityConstants.TwoFactorUserIdScheme"/> and
/// <see cref="IdentityConstants.TwoFactorRememberMeScheme"/> cookies against the user's
/// stored security stamp.
/// </summary>
/// <param name="context">The context containing the <see cref="System.Security.Claims.ClaimsPrincipal"/>
/// and <see cref="AuthenticationProperties"/> to validate.</param>
/// <returns></returns>
public static Task ValidateAsync<TValidator>(CookieValidatePrincipalContext context) where TValidator : ISecurityStampValidator
{
if (context.HttpContext.RequestServices == null)
{
throw new InvalidOperationException("RequestServices is null.");
}
var validator = context.HttpContext.RequestServices.GetRequiredService<TValidator>();
return validator.ValidateAsync(context);
}
}
}