/* * 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.Linq; using System.Security.Claims; using System.Threading.Tasks; namespace Amazon.AspNetCore.Identity.Cognito { public class CognitoUserManager : UserManager where TUser : CognitoUser { // This specific type is needed to accomodate all the interfaces it implements. private readonly CognitoUserStore _userStore; private IHttpContextAccessor _httpContextAccessor; public CognitoUserManager(IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, CognitoKeyNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger, IHttpContextAccessor httpContextAccessor) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { var userStore = store as CognitoUserStore; if (userStore == null) { throw new ArgumentException("The store must be of type CognitoUserStore", nameof(store)); } else { _userStore = userStore; } _httpContextAccessor = httpContextAccessor ?? throw new ArgumentException(nameof(httpContextAccessor)); } /// /// Gets the user, if any, associated with the normalized value of the specified email address. /// /// The email address to return the user for. /// /// The task object containing the results of the asynchronous lookup operation, the user, if any, associated with a normalized value of the specified email address. /// public override async Task FindByEmailAsync(string email) { ThrowIfDisposed(); if (email == null) { throw new ArgumentNullException(nameof(email)); } #if NETCOREAPP3_1 email = NormalizeEmail(email); #endif #if NETSTANDARD2_0 email = NormalizeKey(email); #endif var user = await _userStore.FindByEmailAsync(email, CancellationToken).ConfigureAwait(false); if (user != null) { await PopulateTokens(user, ClaimTypes.Email, email).ConfigureAwait(false); } return user; } /// /// Finds and returns a user, if any, who has the specified . /// /// The user ID to search for. /// /// The that represents the asynchronous operation, containing the user matching the specified if it exists. /// public override async Task FindByIdAsync(string userId) { ThrowIfDisposed(); if (userId == null) { throw new ArgumentNullException(nameof(userId)); } var user = await _userStore.FindByIdAsync(userId, CancellationToken).ConfigureAwait(false); if (user != null) { await PopulateTokens(user, ClaimTypes.Name, userId).ConfigureAwait(false); } return user; } /// /// Finds and returns a user, if any, who has the specified user name. /// /// The user name to search for. /// /// The that represents the asynchronous operation, containing the user matching the specified if it exists. /// public override async Task FindByNameAsync(string userName) { ThrowIfDisposed(); if (userName == null) { throw new ArgumentNullException(nameof(userName)); } #if NETCOREAPP3_1 userName = NormalizeName(userName); #endif #if NETSTANDARD2_0 userName = NormalizeKey(userName); #endif var user = await _userStore.FindByNameAsync(userName, CancellationToken).ConfigureAwait(false); if (user != null) { await PopulateTokens(user, ClaimTypes.Name, userName).ConfigureAwait(false); } return user; } /// /// Populates the user SessionToken object if they satisfy the claimType and claimValue parameters /// /// The user to populate tokens for. /// The claim type to check. /// The claim value to check. private async Task PopulateTokens(TUser user, string claimType, string claimValue) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } // First check if the current user is authenticated before calling AuthenticateAsync() or the call may hang. if (_httpContextAccessor?.HttpContext?.User?.Identity?.IsAuthenticated == true) { var result = await _httpContextAccessor.HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme).ConfigureAwait(false); if (result?.Principal?.Claims != null) { if (result.Principal.Claims.Any(claim => claim.Type == claimType && claim.Value == claimValue)) { var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken).ConfigureAwait(false); var refreshToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken).ConfigureAwait(false); var idToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken).ConfigureAwait(false); user.SessionTokens = new CognitoUserSession(idToken, accessToken, refreshToken, result.Properties.IssuedUtc.Value.UtcDateTime, result.Properties.ExpiresUtc.Value.UtcDateTime); } } } } /// /// Returns an AuthFlowResponse representing an authentication workflow for the specified /// and the specified . /// /// The user whose password should be validated. /// The password to validate /// The that represents the asynchronous operation, containing the AuthFlowResponse object /// if the specified matches the one store for the , /// otherwise null. public virtual Task CheckPasswordAsync(TUser user, string password) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return _userStore.StartValidatePasswordAsync(user, password, CancellationToken); } /// /// Checks if the can log in with the specified 2fa code challenge . /// /// The user try to log in with. /// The 2fa code to check /// The ongoing Cognito authentication workflow id. /// The that represents the asynchronous operation, containing the AuthFlowResponse object linked to that authentication workflow. public virtual Task RespondToTwoFactorChallengeAsync(TUser user, string code, string authWorkflowSessionId) { return RespondToTwoFactorChallengeAsync(user, code, ChallengeNameType.SMS_MFA, authWorkflowSessionId); } /// /// Checks if the can log in with the specified 2fa code challenge . /// /// The user try to log in with. /// The 2fa code to check /// The ongoing Cognito challenge name type. /// The ongoing Cognito authentication workflow id. /// The that represents the asynchronous operation, containing the AuthFlowResponse object linked to that authentication workflow. public virtual Task RespondToTwoFactorChallengeAsync(TUser user, string code, ChallengeNameType challengeNameType, string authWorkflowSessionId) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return _userStore.RespondToTwoFactorChallengeAsync(user, code, challengeNameType, authWorkflowSessionId, CancellationToken); } /// /// Sets a flag indicating whether the specified has two factor authentication enabled or not, /// as an asynchronous operation. /// /// The user whose two factor authentication enabled status should be set. /// A flag indicating whether the specified has two factor authentication enabled. /// /// The that represents the asynchronous operation, the of the operation /// public override async Task SetTwoFactorEnabledAsync(TUser user, bool enabled) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } await _userStore.SetTwoFactorEnabledAsync(user, enabled, CancellationToken).ConfigureAwait(false); return IdentityResult.Success; } /// /// Changes a user's password after confirming the specified is correct, /// as an asynchronous operation. /// /// The user whose password should be set. /// The current password to validate before changing. /// The new password to set for the specified . /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override Task ChangePasswordAsync(TUser user, string currentPassword, string newPassword) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return _userStore.ChangePasswordAsync(user, currentPassword, newPassword, CancellationToken); } /// /// 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. public virtual Task IsPasswordChangeRequiredAsync(TUser user) { ThrowIfDisposed(); return _userStore.IsPasswordChangeRequiredAsync(user, CancellationToken); } /// /// 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. public virtual Task IsPasswordResetRequiredAsync(TUser user) { ThrowIfDisposed(); return _userStore.IsPasswordResetRequiredAsync(user, CancellationToken); } /// /// Resets the 's password to the specified after /// validating the given password reset . /// /// The user whose password should be reset. /// The password reset token to verify. /// The new password to set if reset token verification succeeds. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override Task ResetPasswordAsync(TUser user, string token, string newPassword) { ThrowIfDisposed(); return _userStore.ChangePasswordWithTokenAsync(user, token, newPassword, CancellationToken); } /// /// Resets the 's password and sends the confirmation token to the user /// via email or sms depending on the user pool policy. /// /// The user whose password should be reset. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public Task ResetPasswordAsync(TUser user) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return _userStore.ResetPasswordAsync(user, CancellationToken); } /// /// Creates the specified in Cognito with a generated password sent to the user, /// as an asynchronous operation. /// /// The user to create. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override Task CreateAsync(TUser user) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return _userStore.CreateAsync(user, CancellationToken); } /// /// Creates the specified in Cognito with the given password, /// as an asynchronous operation. /// /// The user to create. /// The password for the user. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override Task CreateAsync(TUser user, string password) { ThrowIfDisposed(); return CreateAsync(user, password, null); } /// /// Creates the specified in Cognito with the given password and validation data, /// as an asynchronous operation. /// /// The user to create. /// The password for the user /// The validation data to be sent to the pre sign-up lambda triggers. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public async Task CreateAsync(TUser user, string password, IDictionary validationData) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } if (password == null) { throw new ArgumentNullException(nameof(password)); } var validate = await ValidatePasswordInternal(user, password).ConfigureAwait(false); if (!validate.Succeeded) { return validate; } var result = await _userStore.CreateAsync(user, password, validationData, CancellationToken).ConfigureAwait(false); return result; } /// /// Validates the given password against injected IPasswordValidator password validators, /// as an asynchronous operation. /// /// The user to validate the password for. /// The password to validate. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// private async Task ValidatePasswordInternal(TUser user, string password) { var errors = new List(); foreach (var v in PasswordValidators) { var result = await v.ValidateAsync(this, user, password).ConfigureAwait(false); if (!result.Succeeded) { errors.AddRange(result.Errors); } } if (errors.Count > 0) { Logger.LogWarning(14, "User {userId} password validation failed: {errors}.", await GetUserIdAsync(user).ConfigureAwait(false), string.Join(";", errors.Select(e => e.Code))); return IdentityResult.Failed(errors.ToArray()); } return IdentityResult.Success; } /// /// Generates an email confirmation token for the specified user. /// /// The user to generate an email confirmation token for. /// /// The that represents the asynchronous operation, an email confirmation token. /// public override Task GenerateEmailConfirmationTokenAsync(TUser user) { throw new NotSupportedException("Cognito does not support directly retrieving the token value. Use SendEmailConfirmationTokenAsync() instead."); } /// /// Generates a telephone number change token for the specified user. /// /// The user to generate a telephone number token for. /// The new phone number the validation token should be sent to. /// /// The that represents the asynchronous operation, containing the telephone change number token. /// public override Task GenerateChangePhoneNumberTokenAsync(TUser user, string phoneNumber) { throw new NotSupportedException("Cognito does not support directly retrieving the token value. Use SendPhoneConfirmationTokenAsync() instead."); } /// /// Generates and sends an email confirmation token for the specified user. /// /// The user to generate and send an email confirmation token for. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public virtual Task SendEmailConfirmationTokenAsync(TUser user) { ThrowIfDisposed(); return _userStore.GetUserAttributeVerificationCodeAsync(user, CognitoAttribute.Email.AttributeName, CancellationToken); } /// /// Generates and sends a phone confirmation token for the specified user. /// /// The user to generate and send a phone confirmation token for. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public virtual Task SendPhoneConfirmationTokenAsync(TUser user) { ThrowIfDisposed(); return _userStore.GetUserAttributeVerificationCodeAsync(user, CognitoAttribute.PhoneNumber.AttributeName, CancellationToken); } /// /// Confirms the email of an user by validating that an email confirmation token is valid for the specified . /// This operation requires a logged in user. /// /// The user to validate the token against. /// The email confirmation code to validate. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override Task ConfirmEmailAsync(TUser user, string confirmationCode) { ThrowIfDisposed(); return _userStore.VerifyUserAttributeAsync(user, CognitoAttribute.Email.AttributeName, confirmationCode, CancellationToken); } /// /// Confirms the phone number of an user by validating that an email confirmation token is valid for the specified . /// This operation requires a logged in user. /// /// The user to validate the token against. /// The phone number confirmation code to validate. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public Task ConfirmPhoneNumberAsync(TUser user, string confirmationCode) { ThrowIfDisposed(); return _userStore.VerifyUserAttributeAsync(user, CognitoAttribute.PhoneNumber.AttributeName, confirmationCode, CancellationToken); } /// /// Confirms the specified account with the specified /// they were sent by email or sms, /// as an asynchronous operation. /// When a new user is confirmed, the user's attribute through which the /// confirmation code was sent (email address or phone number) is marked as verified. /// If this attribute is also set to be used as an alias, then the user can sign in with /// that attribute (email address or phone number) instead of the username. /// /// The user to confirm. /// The confirmation code that was sent by email or sms. /// If set to true, this resolves potential alias conflicts by marking the attribute email or phone number verified. /// If set to false and an alias conflict exists, then the user confirmation will fail. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public virtual Task ConfirmSignUpAsync(TUser user, string confirmationCode, bool forcedAliasCreation) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } if (string.IsNullOrWhiteSpace(confirmationCode)) { throw new ArgumentException("The confirmation code can not be null or blank", nameof(confirmationCode)); } return _userStore.ConfirmSignUpAsync(user, confirmationCode, forcedAliasCreation, CancellationToken); } /// /// Admin confirms the specified /// as an asynchronous operation. /// /// The user to confirm. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public virtual Task AdminConfirmSignUpAsync(TUser user) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return _userStore.AdminConfirmSignUpAsync(user, CancellationToken); } /// /// Resends the account signup confirmation code for the specified /// as an asynchronous operation. /// /// The user to resend the account signup confirmation code for. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public virtual Task ResendSignupConfirmationCodeAsync(TUser user) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return _userStore.ResendSignupConfirmationCodeAsync(user, CancellationToken); } /// /// Sets the phone number for the specified . /// /// The user whose phone number to set. /// The phone number to set. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override async Task SetPhoneNumberAsync(TUser user, string phoneNumber) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } await _userStore.SetPhoneNumberAsync(user, phoneNumber, CancellationToken).ConfigureAwait(false); return await UpdateUserAsync(user).ConfigureAwait(false); } /// /// Sets the address for a . /// /// The user whose email should be set. /// The email to set. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override async Task SetEmailAsync(TUser user, string email) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } await _userStore.SetEmailAsync(user, email, CancellationToken).ConfigureAwait(false); return await UpdateUserAsync(user).ConfigureAwait(false); } /// /// Updates a users emails if the specified email change is valid for the user. /// /// The user whose email should be updated. /// The new email address. /// The change email token to be verified. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override Task ChangeEmailAsync(TUser user, string newEmail, string token) { throw new NotSupportedException("Cognito does not support changing and confirming the email simultaneously, use SetEmailAsync() and ConfirmEmailAsync()"); } /// /// Updates the user attributes. /// /// The user with the new attributes values changed. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// protected override Task UpdateUserAsync(TUser user) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } return _userStore.UpdateAsync(user, CancellationToken); } /// /// Not supported: Returns an IQueryable of users if the store is an IQueryableUserStore /// public override IQueryable Users => throw new NotSupportedException("This property is not supported. Use GetUsersAsync() instead."); /// /// Queries Cognito and returns the users in the pool. Optional filters can be applied on the users to retrieve based on their attributes. /// Providing an empty attributeFilterName parameter returns all the users in the pool. /// /// The attribute name to filter your search on. You can only search for the following standard attributes: /// username (case-sensitive) /// email /// phone_number /// name /// given_name /// family_name /// preferred_username /// cognito:user_status (called Status in the Console) (case-insensitive) /// status (called Enabled in the Console) (case-sensitive) /// sub /// Custom attributes are not searchable. /// For more information, see Searching for Users Using the ListUsers API and Examples /// of Using the ListUsers API in the Amazon Cognito Developer Guide. /// The type of filter to apply: /// For an exact match, use = /// For a prefix ("starts with") match, use ^= /// /// The filter value for the specified attribute. /// /// The that represents the asynchronous operation, containing a IEnumerable of CognitoUser. /// public virtual Task> GetUsersAsync(CognitoAttribute filterAttribute = null, CognitoAttributeFilterType filterType = null, string filterValue = "") { ThrowIfDisposed(); return _userStore.GetUsersAsync(filterAttribute, filterType, filterValue, CancellationToken); } /// /// Adds the specified to the . /// /// The user to add the claim to. /// The claims to add. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override async Task AddClaimsAsync(TUser user, IEnumerable claims) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } if (claims == null) { throw new ArgumentNullException(nameof(claims)); } await _userStore.AddClaimsAsync(user, claims, CancellationToken).ConfigureAwait(false); return IdentityResult.Success; } /// /// Removes the specified from the given . /// /// The user to remove the specified from. /// A collection of s to remove. /// /// The that represents the asynchronous operation, containing the /// of the operation. /// public override async Task RemoveClaimsAsync(TUser user, IEnumerable claims) { ThrowIfDisposed(); if (user == null) { throw new ArgumentNullException(nameof(user)); } if (claims == null) { throw new ArgumentNullException(nameof(claims)); } await _userStore.RemoveClaimsAsync(user, claims, CancellationToken).ConfigureAwait(false); return IdentityResult.Success; } } }