// 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.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Identity { /// /// Provides the APIs for user sign in. /// /// The type encapsulating a user. public class SignInManager where TUser : class { private const string LoginProviderKey = "LoginProvider"; private const string XsrfKey = "XsrfId"; /// /// Creates a new instance of . /// /// An instance of used to retrieve users from and persist users. /// The accessor used to access the . /// The factory to use to create claims principals for a user. /// The accessor used to access the . /// The logger used to log messages, warnings and errors. public SignInManager(UserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger) { if (userManager == null) { throw new ArgumentNullException(nameof(userManager)); } if (contextAccessor == null) { throw new ArgumentNullException(nameof(contextAccessor)); } if (claimsFactory == null) { throw new ArgumentNullException(nameof(claimsFactory)); } UserManager = userManager; _contextAccessor = contextAccessor; ClaimsFactory = claimsFactory; Options = optionsAccessor?.Value ?? new IdentityOptions(); Logger = logger; } private readonly IHttpContextAccessor _contextAccessor; private HttpContext _context; /// /// Gets the used to log messages from the manager. /// /// /// The used to log messages from the manager. /// protected internal virtual ILogger Logger { get; set; } /// /// The used. /// protected internal UserManager UserManager { get; set; } internal IUserClaimsPrincipalFactory ClaimsFactory { get; set; } internal IdentityOptions Options { get; set; } internal HttpContext Context { get { var context = _context ?? _contextAccessor?.HttpContext; if (context == null) { throw new InvalidOperationException("HttpContext must not be null."); } return context; } set { _context = value; } } /// /// Creates a for the specified , as an asynchronous operation. /// /// The user to create a for. /// The task object representing the asynchronous operation, containing the ClaimsPrincipal for the specified user. public virtual async Task CreateUserPrincipalAsync(TUser user) => await ClaimsFactory.CreateAsync(user); /// /// Returns true if the principal has an identity with the application cookie identity /// /// The instance. /// True if the user is logged in with identity. public virtual bool IsSignedIn(ClaimsPrincipal principal) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } return principal?.Identities != null && principal.Identities.Any(i => i.AuthenticationType == Options.Cookies.ApplicationCookieAuthenticationScheme); } /// /// Returns a flag indicating whether the specified user can sign in. /// /// The user whose sign-in status should be returned. /// /// The task object representing the asynchronous operation, containing a flag that is true /// if the specified user can sign-in, otherwise false. /// public virtual async Task CanSignInAsync(TUser user) { if (Options.SignIn.RequireConfirmedEmail && !(await UserManager.IsEmailConfirmedAsync(user))) { Logger.LogWarning(0, "User {userId} cannot sign in without a confirmed email.", await UserManager.GetUserIdAsync(user)); return false; } if (Options.SignIn.RequireConfirmedPhoneNumber && !(await UserManager.IsPhoneNumberConfirmedAsync(user))) { Logger.LogWarning(1, "User {userId} cannot sign in without a confirmed phone number.", await UserManager.GetUserIdAsync(user)); return false; } return true; } /// /// Regenerates the user's application cookie, whilst preserving the existing /// AuthenticationProperties like rememberMe, as an asynchronous operation. /// /// The user whose sign-in cookie should be refreshed. /// The task object representing the asynchronous operation. public virtual async Task RefreshSignInAsync(TUser user) { var auth = new AuthenticateContext(Options.Cookies.ApplicationCookieAuthenticationScheme); await Context.Authentication.AuthenticateAsync(auth); var authenticationMethod = auth.Principal?.FindFirstValue(ClaimTypes.AuthenticationMethod); await SignInAsync(user, new AuthenticationProperties(auth.Properties), authenticationMethod); } /// /// Signs in the specified . /// /// The user to sign-in. /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// Name of the method used to authenticate the user. /// The task object representing the asynchronous operation. public virtual Task SignInAsync(TUser user, bool isPersistent, string authenticationMethod = null) { return SignInAsync(user, new AuthenticationProperties { IsPersistent = isPersistent }, authenticationMethod); } /// /// Signs in the specified . /// /// The user to sign-in. /// Properties applied to the login and authentication cookie. /// Name of the method used to authenticate the user. /// The task object representing the asynchronous operation. public virtual async Task SignInAsync(TUser user, AuthenticationProperties authenticationProperties, string authenticationMethod = null) { var userPrincipal = await CreateUserPrincipalAsync(user); // Review: should we guard against CreateUserPrincipal returning null? if (authenticationMethod != null) { userPrincipal.Identities.First().AddClaim(new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod)); } await Context.Authentication.SignInAsync(Options.Cookies.ApplicationCookieAuthenticationScheme, userPrincipal, authenticationProperties ?? new AuthenticationProperties()); } /// /// Signs the current user out of the application. /// public virtual async Task SignOutAsync() { await Context.Authentication.SignOutAsync(Options.Cookies.ApplicationCookieAuthenticationScheme); await Context.Authentication.SignOutAsync(Options.Cookies.ExternalCookieAuthenticationScheme); await Context.Authentication.SignOutAsync(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme); } /// /// Validates the security stamp for the specified against /// the persisted stamp for the current user, as an asynchronous operation. /// /// The principal whose stamp should be validated. /// The task object representing the asynchronous operation. The task will contain the /// if the stamp matches the persisted value, otherwise it will return false. public virtual async Task ValidateSecurityStampAsync(ClaimsPrincipal principal) { if (principal == null) { return null; } var user = await UserManager.GetUserAsync(principal); if (user != null && UserManager.SupportsUserSecurityStamp) { var securityStamp = principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType); if (securityStamp == await UserManager.GetSecurityStampAsync(user)) { return user; } } return null; } /// /// Attempts to sign in the specified and combination /// as an asynchronous operation. /// /// The user to sign in. /// The password to attempt to sign in with. /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// Flag indicating if the user account should be locked if the sign in fails. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public virtual async Task PasswordSignInAsync(TUser user, string password, bool isPersistent, bool lockoutOnFailure) { if (user == null) { throw new ArgumentNullException(nameof(user)); } var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure); return attempt.Succeeded ? await SignInOrTwoFactorAsync(user, isPersistent) : attempt; } /// /// Attempts to sign in the specified and combination /// as an asynchronous operation. /// /// The user name to sign in. /// The password to attempt to sign in with. /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// Flag indicating if the user account should be locked if the sign in fails. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public virtual async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure) { var user = await UserManager.FindByNameAsync(userName); if (user == null) { return SignInResult.Failed; } return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); } /// /// Attempts a password sign in for a user. /// /// The user to sign in. /// The password to attempt to sign in with. /// Flag indicating if the user account should be locked if the sign in fails. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. /// public virtual async Task CheckPasswordSignInAsync(TUser user, string password, bool lockoutOnFailure) { if (user == null) { throw new ArgumentNullException(nameof(user)); } var error = await PreSignInCheck(user); if (error != null) { return error; } if (await IsLockedOut(user)) { return await LockedOut(user); } if (await UserManager.CheckPasswordAsync(user, password)) { await ResetLockout(user); return SignInResult.Success; } Logger.LogWarning(2, "User {userId} failed to provide the correct password.", await UserManager.GetUserIdAsync(user)); if (UserManager.SupportsUserLockout && lockoutOnFailure) { // If lockout is requested, increment access failed count which might lock out the user await UserManager.AccessFailedAsync(user); if (await UserManager.IsLockedOutAsync(user)) { return await LockedOut(user); } } return SignInResult.Failed; } /// /// Returns a flag indicating if the current client browser has been remembered by two factor authentication /// for the user attempting to login, as an asynchronous operation. /// /// The user attempting to login. /// /// The task object representing the asynchronous operation containing true if the browser has been remembered /// for the current user. /// public virtual async Task IsTwoFactorClientRememberedAsync(TUser user) { var userId = await UserManager.GetUserIdAsync(user); var result = await Context.Authentication.AuthenticateAsync(Options.Cookies.TwoFactorRememberMeCookieAuthenticationScheme); return (result != null && result.FindFirstValue(ClaimTypes.Name) == userId); } /// /// Sets a flag on the browser to indicate the user has selected "Remember this browser" for two factor authentication purposes, /// as an asynchronous operation. /// /// The user who choose "remember this browser". /// The task object representing the asynchronous operation. public virtual async Task RememberTwoFactorClientAsync(TUser user) { var userId = await UserManager.GetUserIdAsync(user); var rememberBrowserIdentity = new ClaimsIdentity(Options.Cookies.TwoFactorRememberMeCookieAuthenticationScheme); rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId)); await Context.Authentication.SignInAsync(Options.Cookies.TwoFactorRememberMeCookieAuthenticationScheme, new ClaimsPrincipal(rememberBrowserIdentity), new AuthenticationProperties { IsPersistent = true }); } /// /// Clears the "Remember this browser flag" from the current browser, as an asynchronous operation. /// /// The task object representing the asynchronous operation. public virtual Task ForgetTwoFactorClientAsync() { return Context.Authentication.SignOutAsync(Options.Cookies.TwoFactorRememberMeCookieAuthenticationScheme); } /// /// Validates the two faction sign in code and creates and signs in the user, as an asynchronous operation. /// /// The two factor authentication provider to validate the code against. /// The two factor authentication code to validate. /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// Flag indicating whether the current browser should be remember, suppressing all further /// two factor authentication prompts. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public virtual async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) { var twoFactorInfo = await RetrieveTwoFactorInfoAsync(); if (twoFactorInfo == null || twoFactorInfo.UserId == null) { return SignInResult.Failed; } var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId); if (user == null) { return SignInResult.Failed; } var error = await PreSignInCheck(user); if (error != null) { return error; } if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code)) { // When token is verified correctly, clear the access failed count used for lockout await ResetLockout(user); // Cleanup external cookie if (twoFactorInfo.LoginProvider != null) { await Context.Authentication.SignOutAsync(Options.Cookies.ExternalCookieAuthenticationScheme); } // Cleanup two factor user id cookie await Context.Authentication.SignOutAsync(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme); if (rememberClient) { await RememberTwoFactorClientAsync(user); } await UserManager.ResetAccessFailedCountAsync(user); await SignInAsync(user, isPersistent, twoFactorInfo.LoginProvider); return SignInResult.Success; } // If the token is incorrect, record the failure which also may cause the user to be locked out await UserManager.AccessFailedAsync(user); return SignInResult.Failed; } /// /// Gets the for the current two factor authentication login, as an asynchronous operation. /// /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public virtual async Task GetTwoFactorAuthenticationUserAsync() { var info = await RetrieveTwoFactorInfoAsync(); if (info == null) { return null; } return await UserManager.FindByIdAsync(info.UserId); } /// /// Signs in a user via a previously registered third party login, as an asynchronous operation. /// /// The login provider to use. /// The unique provider identifier for the user. /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public virtual async Task ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent) { var user = await UserManager.FindByLoginAsync(loginProvider, providerKey); if (user == null) { return SignInResult.Failed; } var error = await PreSignInCheck(user); if (error != null) { return error; } return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider); } /// /// Gets a collection of s for the known external login providers. /// /// A collection of s for the known external login providers. public virtual IEnumerable GetExternalAuthenticationSchemes() { return Context.Authentication.GetAuthenticationSchemes().Where(d => !string.IsNullOrEmpty(d.DisplayName)); } /// /// Gets the external login information for the current login, as an asynchronous operation. /// /// Flag indication whether a Cross Site Request Forgery token was expected in the current request. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public virtual async Task GetExternalLoginInfoAsync(string expectedXsrf = null) { var auth = new AuthenticateContext(Options.Cookies.ExternalCookieAuthenticationScheme); await Context.Authentication.AuthenticateAsync(auth); if (auth.Principal == null || auth.Properties == null || !auth.Properties.ContainsKey(LoginProviderKey)) { return null; } if (expectedXsrf != null) { if (!auth.Properties.ContainsKey(XsrfKey)) { return null; } var userId = auth.Properties[XsrfKey] as string; if (userId != expectedXsrf) { return null; } } var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); var provider = auth.Properties[LoginProviderKey] as string; if (providerKey == null || provider == null) { return null; } return new ExternalLoginInfo(auth.Principal, provider, providerKey, new AuthenticationDescription(auth.Description).DisplayName) { AuthenticationTokens = new AuthenticationProperties(auth.Properties).GetTokens() }; } /// /// Stores any authentication tokens found in the external authentication cookie into the associated user. /// /// The information from the external login provider. /// The that represents the asynchronous operation, containing the of the operation. public virtual async Task UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin) { if (externalLogin == null) { throw new ArgumentNullException(nameof(externalLogin)); } if (externalLogin.AuthenticationTokens != null && externalLogin.AuthenticationTokens.Any()) { var user = await UserManager.FindByLoginAsync(externalLogin.LoginProvider, externalLogin.ProviderKey); if (user == null) { return IdentityResult.Failed(); } foreach (var token in externalLogin.AuthenticationTokens) { var result = await UserManager.SetAuthenticationTokenAsync(user, externalLogin.LoginProvider, token.Name, token.Value); if (!result.Succeeded) { return result; } } } return IdentityResult.Success; } /// /// Configures the redirect URL and user identifier for the specified external login . /// /// The provider to configure. /// The external login URL users should be redirected to during the login flow. /// The current user's identifier, which will be used to provide CSRF protection. /// A configured . public virtual AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null) { var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; properties.Items[LoginProviderKey] = provider; if (userId != null) { properties.Items[XsrfKey] = userId; } return properties; } /// /// Creates a claims principal for the specified 2fa information. /// /// The user whose is logging in via 2fa. /// The 2fa provider. /// A containing the user 2fa information. internal ClaimsPrincipal StoreTwoFactorInfo(string userId, string loginProvider) { var identity = new ClaimsIdentity(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme); identity.AddClaim(new Claim(ClaimTypes.Name, userId)); if (loginProvider != null) { identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); } return new ClaimsPrincipal(identity); } private ClaimsIdentity CreateIdentity(TwoFactorAuthenticationInfo info) { if (info == null) { return null; } var identity = new ClaimsIdentity(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme); identity.AddClaim(new Claim(ClaimTypes.Name, info.UserId)); if (info.LoginProvider != null) { identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, info.LoginProvider)); } return identity; } private async Task SignInOrTwoFactorAsync(TUser user, bool isPersistent, string loginProvider = null) { if (UserManager.SupportsUserTwoFactor && await UserManager.GetTwoFactorEnabledAsync(user) && (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0) { if (!await IsTwoFactorClientRememberedAsync(user)) { // Store the userId for use after two factor check var userId = await UserManager.GetUserIdAsync(user); await Context.Authentication.SignInAsync(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme, StoreTwoFactorInfo(userId, loginProvider)); return SignInResult.TwoFactorRequired; } } // Cleanup external cookie if (loginProvider != null) { await Context.Authentication.SignOutAsync(Options.Cookies.ExternalCookieAuthenticationScheme); } await SignInAsync(user, isPersistent, loginProvider); return SignInResult.Success; } private async Task RetrieveTwoFactorInfoAsync() { var result = await Context.Authentication.AuthenticateAsync(Options.Cookies.TwoFactorUserIdCookieAuthenticationScheme); if (result != null) { return new TwoFactorAuthenticationInfo { UserId = result.FindFirstValue(ClaimTypes.Name), LoginProvider = result.FindFirstValue(ClaimTypes.AuthenticationMethod) }; } return null; } private async Task IsLockedOut(TUser user) { return UserManager.SupportsUserLockout && await UserManager.IsLockedOutAsync(user); } private async Task LockedOut(TUser user) { Logger.LogWarning(3, "User {userId} is currently locked out.", await UserManager.GetUserIdAsync(user)); return SignInResult.LockedOut; } private async Task PreSignInCheck(TUser user) { if (!await CanSignInAsync(user)) { return SignInResult.NotAllowed; } if (await IsLockedOut(user)) { return await LockedOut(user); } return null; } private Task ResetLockout(TUser user) { if (UserManager.SupportsUserLockout) { return UserManager.ResetAccessFailedCountAsync(user); } return Task.FromResult(0); } internal class TwoFactorAuthenticationInfo { public string UserId { get; set; } public string LoginProvider { get; set; } } } }