/* * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using Amazon.CognitoIdentityProvider; using Amazon.Extensions.CognitoAuthentication; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using System; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; namespace Amazon.AspNetCore.Identity.Cognito { public class CognitoSignInManager : SignInManager where TUser : CognitoUser { private readonly CognitoUserManager _userManager; private readonly CognitoUserClaimsPrincipalFactory _claimsFactory; private readonly IHttpContextAccessor _contextAccessor; private const string Cognito2FAAuthWorkflowKey = "Cognito2FAAuthWorkflowId"; private const string Cognito2FAChallengeNameType = "Cognito2FAChallengeNameType"; private const string Cognito2FAProviderKey = "Amazon Cognito 2FA"; #if NETCOREAPP3_1 public CognitoSignInManager(UserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) { if (userManager == null) throw new ArgumentNullException(nameof(userManager)); if (claimsFactory == null) throw new ArgumentNullException(nameof(claimsFactory)); if (userManager is CognitoUserManager) _userManager = userManager as CognitoUserManager; else throw new ArgumentException("The userManager must be of type CognitoUserManager", nameof(userManager)); if (claimsFactory is CognitoUserClaimsPrincipalFactory) _claimsFactory = claimsFactory as CognitoUserClaimsPrincipalFactory; else throw new ArgumentException("The claimsFactory must be of type CognitoUserClaimsPrincipalFactory", nameof(claimsFactory)); _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); } #endif #if NETSTANDARD2_0 public CognitoSignInManager(UserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes) { if (userManager == null) throw new ArgumentNullException(nameof(userManager)); if (claimsFactory == null) throw new ArgumentNullException(nameof(claimsFactory)); if (userManager is CognitoUserManager) _userManager = userManager as CognitoUserManager; else throw new ArgumentException("The userManager must be of type CognitoUserManager", nameof(userManager)); if (claimsFactory is CognitoUserClaimsPrincipalFactory) _claimsFactory = claimsFactory as CognitoUserClaimsPrincipalFactory; else throw new ArgumentException("The claimsFactory must be of type CognitoUserClaimsPrincipalFactory", nameof(claimsFactory)); _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); } #endif /// /// Attempts to sign in the specified and combination /// as an asynchronous operation. /// /// The user id to sign in with. This can be a username, an email, or a phone number depending on the user pool policy. /// The password to attempt to sign in with. /// Flag indicating whether the sign-in cookie should persist after the browser is closed. /// Cognito does not handle account lock out. This parameter must be set to false, or a NotSupportedException will be thrown. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public override async Task PasswordSignInAsync(string userId, string password, bool isPersistent, bool lockoutOnFailure) { if (lockoutOnFailure) { throw new NotSupportedException("Lockout is not enabled for the CognitoUserManager."); } var user = await _userManager.FindByIdAsync(userId).ConfigureAwait(false); if (user == null) { return SignInResult.Failed; } return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure).ConfigureAwait(false); } /// /// 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. /// Cognito does not handle account lock out. This parameter must be set to false, or a NotSupportedException will be thrown. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public override async Task PasswordSignInAsync(TUser user, string password, bool isPersistent, bool lockoutOnFailure) { if (lockoutOnFailure) { throw new NotSupportedException("Lockout is not enabled for the CognitoUserManager."); } if (user == null) { throw new ArgumentNullException(nameof(user)); } var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure).ConfigureAwait(false); if (attempt.Succeeded) await SignInAsync(user, isPersistent).ConfigureAwait(false); return attempt; } /// /// 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 override Task SignInAsync(TUser user, bool isPersistent, string authenticationMethod = null) { // Populating the tokens to be retrieved when calling Context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken). var authenticationProperties = new AuthenticationProperties { IsPersistent = isPersistent, AllowRefresh = true, ExpiresUtc = user.SessionTokens?.ExpirationTime, IssuedUtc = user.SessionTokens?.IssuedTime }; AddUserTokensToAuthenticationProperties(user, authenticationProperties); return SignInAsync(user, authenticationProperties, authenticationMethod); } /// /// Adds a user tokens to the authentication properties /// /// The user to update tokens for /// The authentication properties to update private void AddUserTokensToAuthenticationProperties(TUser user, AuthenticationProperties authenticationProperties) { authenticationProperties.StoreTokens(new List() { new AuthenticationToken() { Name = OpenIdConnectParameterNames.AccessToken, Value = user.SessionTokens?.AccessToken }, new AuthenticationToken() { Name = OpenIdConnectParameterNames.RefreshToken, Value = user.SessionTokens?.RefreshToken }, new AuthenticationToken() { Name = OpenIdConnectParameterNames.IdToken, Value = user.SessionTokens?.IdToken } }); } /// /// 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 override async Task RefreshSignInAsync(TUser user) { var result = await user.StartWithRefreshTokenAuthAsync( new InitiateRefreshTokenAuthRequest() { AuthFlowType = AuthFlowType.REFRESH_TOKEN } ).ConfigureAwait(false); var auth = await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme); var authenticationMethod = auth?.Principal?.FindFirstValue(ClaimTypes.AuthenticationMethod); var properties = auth?.Properties; if (properties != null) { AddUserTokensToAuthenticationProperties(user, properties); properties.ExpiresUtc = user.SessionTokens?.ExpirationTime; properties.IssuedUtc = user.SessionTokens?.IssuedTime; } await SignInAsync(user, properties, authenticationMethod); } /// /// Attempts a password sign in for a user. /// /// The user to sign in. /// The password to attempt to sign in with. /// Cognito does not handle account lock out. This parameter must be set to false, or a NotSupportedException will be thrown. /// The task object representing the asynchronous operation containing the /// for the sign-in attempt. public override async Task CheckPasswordSignInAsync(TUser user, string password, bool lockoutOnFailure) { if (lockoutOnFailure) { throw new NotSupportedException("Lockout is not enabled for the CognitoUserManager."); } if (user == null) { throw new ArgumentNullException(nameof(user)); } // Prechecks if the user password needs to be changed or reset var error = await PreSignInCheck(user).ConfigureAwait(false); if (error != null) { return error; } var checkPasswordResult = await _userManager.CheckPasswordAsync(user, password).ConfigureAwait(false); SignInResult signinResult; if (checkPasswordResult == null) { signinResult = SignInResult.Failed; } else if (checkPasswordResult.ChallengeName == ChallengeNameType.SMS_MFA || checkPasswordResult.ChallengeName == ChallengeNameType.SOFTWARE_TOKEN_MFA) { signinResult = SignInResult.TwoFactorRequired; var userPrincipal = new ClaimsPrincipal(); userPrincipal.AddIdentity(new ClaimsIdentity(new List() { new Claim(ClaimTypes.Name, user.UserID), new Claim(Cognito2FAAuthWorkflowKey, checkPasswordResult.SessionID), new Claim(ClaimTypes.AuthenticationMethod, Cognito2FAProviderKey), new Claim(Cognito2FAChallengeNameType, checkPasswordResult.ChallengeName), }, IdentityConstants.ApplicationScheme)); // This signs in the user in the context of 2FA only. await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, userPrincipal).ConfigureAwait(false); } else if (user.SessionTokens != null && user.SessionTokens.IsValid()) { signinResult = SignInResult.Success; } else { signinResult = SignInResult.Failed; } return signinResult; } /// /// Used to ensure that a user is allowed to sign in. /// /// The user /// Null if the user should be allowed to sign in, otherwise the SignInResult why they should be denied. protected override async Task PreSignInCheck(TUser user) { // Checks for email/phone number confirmation status if (!await CanSignInAsync(user).ConfigureAwait(false)) { return SignInResult.NotAllowed; } if (await IsPasswordChangeRequiredAsync(user).ConfigureAwait(false)) { return CognitoSignInResult.PasswordChangeRequired; } if (await IsPasswordResetRequiredAsync(user).ConfigureAwait(false)) { return CognitoSignInResult.PasswordResetRequired; } return null; } /// /// Checks if the password needs to be changed for the specified . /// /// The user to check if the password needs to be changed. /// The that represents the asynchronous operation, containing a boolean set to true if the password needs to be changed, false otherwise. protected Task IsPasswordChangeRequiredAsync(TUser user) { return _userManager.IsPasswordChangeRequiredAsync(user); } /// /// Checks if the password needs to be reset for the specified . /// /// The user to check if the password needs to be reset. /// The that represents the asynchronous operation, containing a boolean set to true if the password needs to be reset, false otherwise. protected Task IsPasswordResetRequiredAsync(TUser user) { return _userManager.IsPasswordResetRequiredAsync(user); } /// /// Validates the two factor sign in code and creates and signs in the user, as an asynchronous operation. /// /// 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 async Task RespondToTwoFactorChallengeAsync(string code, bool isPersistent, bool rememberClient) { var twoFactorInfo = await RetrieveTwoFactorInfoAsync().ConfigureAwait(false); if (twoFactorInfo == null || string.IsNullOrWhiteSpace(twoFactorInfo.UserId)) { return SignInResult.Failed; } var user = await _userManager.FindByIdAsync(twoFactorInfo.UserId).ConfigureAwait(false); if (user == null) { return SignInResult.Failed; } // Responding to the Cognito challenge. await _userManager.RespondToTwoFactorChallengeAsync(user, code, twoFactorInfo.ChallengeNameType, twoFactorInfo.CognitoAuthenticationWorkflowId).ConfigureAwait(false); if (user.SessionTokens == null || !user.SessionTokens.IsValid()) { return SignInResult.Failed; } else { // Cleanup external cookie if (twoFactorInfo.LoginProvider != null) { await Context.SignOutAsync(IdentityConstants.ExternalScheme).ConfigureAwait(false); } // Cleanup two factor user id cookie await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme).ConfigureAwait(false); if (rememberClient) { await RememberTwoFactorClientAsync(user).ConfigureAwait(false); } // This creates the ClaimPrincipal and signs in the user in the IdentityConstants.ApplicationScheme await SignInAsync(user, isPersistent, twoFactorInfo.LoginProvider).ConfigureAwait(false); return SignInResult.Success; } } /// /// 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 override async Task GetTwoFactorAuthenticationUserAsync() { var info = await RetrieveTwoFactorInfoAsync().ConfigureAwait(false); if (info == null) { return null; } return await UserManager.FindByIdAsync(info.UserId).ConfigureAwait(false); } #region 2FA /// /// Retrieves the information related to the authentication workflow. /// /// private async Task RetrieveTwoFactorInfoAsync() { var result = await Context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme).ConfigureAwait(false); if (result?.Principal != null) { return new TwoFactorAuthenticationInfo { UserId = result.Principal.FindFirstValue(ClaimTypes.Name), LoginProvider = result.Principal.FindFirstValue(ClaimTypes.AuthenticationMethod), CognitoAuthenticationWorkflowId = result.Principal.FindFirstValue(Cognito2FAAuthWorkflowKey), ChallengeNameType = result.Principal.FindFirstValue(Cognito2FAChallengeNameType) }; } return null; } /// /// Utility class to model information related to the ongoing authentication workflow. /// internal class TwoFactorAuthenticationInfo { public string UserId { get; set; } public string LoginProvider { get; set; } public string CognitoAuthenticationWorkflowId { get; set; } public ChallengeNameType ChallengeNameType { get; set; } } #endregion /// /// Validates the security stamp for the specified . Will always return false /// if the userManager does not support security stamps. /// /// The user whose stamp should be validated. /// The expected security stamp value. /// True if the stamp matches the persisted value, otherwise it will return false. public override async Task ValidateSecurityStampAsync(TUser user, string securityStamp) => user != null && // Only validate the security stamp if the store supports it (!UserManager.SupportsUserSecurityStamp || securityStamp == await UserManager.GetSecurityStampAsync(user).ConfigureAwait(false)); // Preventing the cookies from expiring every 30 minutes. This fix was only added to Identity 2.2. // https://github.com/aspnet/Identity/pull/1941 } }