I am trying to integrate AWS Cognito into a web site. I am trying to force a user to change their own password. I already had a method for voluntary password resets and I tried to use it for forced password reset. Here is the method:
internal async Task<bool> ResetPassword(string username, string oldPassword, string newPassword) {
AmazonCognitoIdentityProviderClient provider = new AmazonCognitoIdentityProviderClient(new Amazon.Runtime.AnonymousAWSCredentials());
CognitoUserPool userPool = new CognitoUserPool(CognitoHelper.POOL_ID, CognitoHelper.CLIENTAPP_ID, provider);
CognitoUser user = new CognitoUser(username, CognitoHelper.CLIENTAPP_ID, userPool, provider);
InitiateSrpAuthRequest authRequest = new InitiateSrpAuthRequest() {
Password = oldPassword
};
AuthFlowResponse authResponse = await user.StartWithSrpAuthAsync(authRequest).ConfigureAwait(false);
await user.ChangePasswordAsync(oldPassword, newPassword);
return true;
} // ResetPassword
When I call this method on a voluntary password reset, it works fine. On a forced password reset, the "StartWithSrpAuthAsync" throws an exception complaining "Password reset required for the user". No kidding - that is why I am trying to change the password.
The problem is that the "ChangePasswordAsync" method requires the user be authenticated before it is called. I can't authenticate the user because the password needs to be reset, but I can't change the password because the user needs to be authenticated first.
I tried a hack to solve my issue by catching the "Password reset required for the user" exception hoping the user was authenticated anyway. Unfortunately no luck:
internal async Task<bool> ResetPassword(string username, string oldPassword, string newPassword) {
AmazonCognitoIdentityProviderClient provider = new AmazonCognitoIdentityProviderClient(new Amazon.Runtime.AnonymousAWSCredentials());
CognitoUserPool userPool = new CognitoUserPool(CognitoHelper.POOL_ID, CognitoHelper.CLIENTAPP_ID, provider);
CognitoUser user = new CognitoUser(username, CognitoHelper.CLIENTAPP_ID, userPool, provider);
InitiateSrpAuthRequest authRequest = new InitiateSrpAuthRequest() {
Password = oldPassword
};
try {
AuthFlowResponse authResponse = await user.StartWithSrpAuthAsync(authRequest).ConfigureAwait(false);
await user.ChangePasswordAsync(oldPassword, newPassword);
} catch (Exception exp) {
if (exp.Message == "Password reset required for the user") {
await user.ChangePasswordAsync(oldPassword, newPassword);
} else {
throw exp;
} // if else
} // try catch
return true;
} // ResetPassword
Any thoughts?
I tried treating a forced reset like a forgot password case and it worked! Specifically to send a new verification code to the user's email:
internal async Task<ForgotPasswordResponse> ForgotPassword(string username) {
ForgotPasswordRequest forgotPasswordRequest = new ForgotPasswordRequest();
forgotPasswordRequest.Username = username;
forgotPasswordRequest.ClientId = CLIENTAPP_ID;
ForgotPasswordResponse forgotPasswordResponse = await provider.ForgotPasswordAsync(forgotPasswordRequest).ConfigureAwait(false);
return forgotPasswordResponse;
} // ForgotPassword
and:
internal async Task<ConfirmForgotPasswordResponse> ConfirmForgotPassword(string validationCode, string username, string newPassword) {
ConfirmForgotPasswordRequest confirmForgotPasswordRequest = new ConfirmForgotPasswordRequest();
confirmForgotPasswordRequest.Username = username;
confirmForgotPasswordRequest.ClientId = CLIENTAPP_ID;
confirmForgotPasswordRequest.Password = newPassword;
confirmForgotPasswordRequest.ConfirmationCode = validationCode;
ConfirmForgotPasswordResponse confirmForgotPasswordResponse = await provider.ConfirmForgotPasswordAsync(confirmForgotPasswordRequest).ConfigureAwait(false);
return confirmForgotPasswordResponse;
} // ConfirmForgotPassword
to "reset" the new password. From what I can see in the documentation, this is not spelled out anywhere.
I tried your code (in the first question) and got the same error
Changing the provider declaration as follows fixed it
static Amazon.RegionEndpoint region = Amazon.RegionEndpoint.APSoutheast2;
AmazonCognitoIdentityProviderClient provider = new AmazonCognitoIdentityProviderClient(new Amazon.Runtime.AnonymousAWSCredentials(), region);
Of course the region should be changed to whatever region you are on
Related
I have been working on a program that scans an exchange inbox for specific emails from a specified address. Currently the program reads the inbox, downloads the attachment, and moves the email to another folder. However, after about 15 pulls from the EWS server, the connection starts giving a 401 Unauthorized error until I restart the program. The program is setup to login via OAuth as basic auth is disabled by the system administrator. Below is the code that I am using to obtain the exchange connection and read the emails from the inbox.
Exchange Connection Code:
public static async Task<ExchangeService> GetExchangeConnection()
{
var pcaOptions = new PublicClientApplicationOptions
{
ClientId = AppID,
TenantId = TenantID,
};
var pca = PublicClientApplicationBuilder.CreateWithApplicationOptions(pcaOptions).Build();
var ewsScopes = new string[] { "https://outlook.office365.com/EWS.AccessAsUser.All" };
var securePassword = new SecureString();
foreach (char c in Pasword)
securePassword.AppendChar(c);
try
{
var authResult = await pca.AcquireTokenByUsernamePassword(ewsScopes, Username, securePassword).ExecuteAsync();
ExchangeService exchangeService = new ExchangeService()
{
Credentials = new OAuthCredentials(authResult.AccessToken),
Url = new Uri("https://outlook.office365.com/ews/exchange.asmx"),
};
return exchangeService;
}
catch
{
return null;
}
}
Email Retriever
public static List<Email> RetreiveEmails()
{
ExchangeService exchangeConnection = GetExchangeConnection().Result;
try
{
List<Email> Emails = new List<Email>();
TimeSpan ts = new TimeSpan(0, -5, 0, 0);
DateTime date = DateTime.Now.Add(ts);
SearchFilter.IsGreaterThanOrEqualTo EmailTimeFilter = new SearchFilter.IsGreaterThanOrEqualTo(ItemSchema.DateTimeReceived, date);
if (exchangeConnection != null)
{
FindItemsResults<Item> findResults = exchangeConnection.FindItems(WellKnownFolderName.Inbox, EmailTimeFilter, new ItemView(10));
foreach (Item item in findResults)
{
if (item.Subject != null)
{
EmailMessage message = EmailMessage.Bind(exchangeConnection, item.Id);
message.Load(new PropertySet(BasePropertySet.FirstClassProperties, ItemSchema.TextBody));
Emails.Add(new Email(message.DateTimeReceived, message.From.Name.ToString(), message.Subject, message.TextBody.ToString(), (message.HasAttachments) ? "Yes" : "No", message.Id.ToString()));
}
}
}
exchangeConnection = null;
return Emails;
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
return null;
}
}
The error occurs when the email retriever tries to either create the exchange connection or when requesting the emails from the folder. In either case the code will error out and give me 401 unauthorized while using credentials that work for the first dozen times and then fails after so many attempts. I have tried it with multiple different accounts and the issue persists with all of them and I have made sure that the application is authorized to access the exchange inbox. Any suggestions or help is much appreciated.
After doing further tracing regarding the 401 error it resulted in an issue with the token reaching the end of it's 1 hour lifespan. This is due to the original OAuth token having an initial life of 1 hour. This however was able to be fixed by setting up code to automatically refresh the token when needed. Here is the code to address this issue for anyone else who comes across this problem.
Authentication Manager:
class AuthenticationManager
{
protected IPublicClientApplication App { get; set; }
public AuthenticationManager(IPublicClientApplication app)
{
App = app;
}
public async Task<AuthenticationResult> AcquireATokenFromCacheOrUsernamePasswordAsync(IEnumerable<String> scopes, string username, SecureString password)
{
AuthenticationResult result = null;
var accounts = await App.GetAccountsAsync();
if (accounts.Any())
{
try
{
result = await (App as PublicClientApplication).AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
}
catch (MsalUiRequiredException)
{ }
}
if (result == null)
{
result = await (App as PublicClientApplication).AcquireTokenByUsernamePassword(scopes, username, password).ExecuteAsync();
}
return result;
}
}
I am using direct username and password authentication but the line of code can be switched to getting the user authentication via interactive methods as well. The code essentially creates a new instance of the authentication manager with a PublicClientApplication used to initialize it which houses the appID and tenantID. After initializing, you can call the AquireATokenFromCacheOrUsernamePasswordAsync which will attempt to see if there is an account present to get a token against. Next it will attempt to retrieve the previously cached token or refresh the token if it expires in less than 5 minutes. If there is a token available it will return that to the main application. If there isn't a token available, it will acquire a new token using the username and password supplied. Implementation of this code looks something like this,
class ExchangeServices
{
AuthenticationManager Manager = null;
public ExchangeServices(String AppId, String TenantID)
{
var pcaOptions = new PublicClientApplicationOptions
{
ClientId = AppID,
TenantId = TenantID,
};
var pca = PublicClientApplicationBuilder.CreateWithApplicationOptions(pcaOptions).Build();
Manager = new AuthenticationManager(pca);
}
public static async Task<ExchangeService> GetExchangeService()
{
var ewsScopes = new string[] { "https://outlook.office365.com/EWS.AccessAsUser.All" }
var securePassword = new SecureString();
foreach(char c in Password)
securePassword.AppendChar(c);
var authResult = await Manager.AquireATokenFromCacheOrUsernamePasswordAsync(ewsScopes, Username, securePassword);
ExchangeService exchangeService = new ExchangeService()
{
Credentials = new OAuthCredentials(authResult.AccessToken),
Url = new Uri("https://outlook.office365.com/ews/exchange.asmx");
};
return exchangeService;
}
}
The code above is everything laid out that is needed to create a new authentication manager and use it to get and update new tokens while using EWS services through OAuth. This is the solution that I found to fix the issue described above.
How can I create a Cognito user with the account status confirmed using c#? After a user is created the account status displays FORCE_CHANGE_PASSWORD. Another thing is I need to create user without email address.
AmazonCognitoIdentityProviderClient cognitoProvider =
new AmazonCognitoIdentityProviderClient(region);
string userName = "user";
string tempPassword = "Temp#3434";
string newPassword = "RealPass#2019";
AdminCreateUserRequest adminUserCreateRequest = new AdminCreateUserRequest()
{
UserPoolId = poolId,
Username = userName,
TemporaryPassword = tempPassword
};
AdminCreateUserResponse signUpResponse = await cognitoProvider.AdminCreateUserAsync(adminUserCreateRequest);
Admin InitiateRequest
Dictionary<string, string> initialParams = new Dictionary<string, string>();
initialParams.Add("USERNAME", userName);
initialParams.Add("PASSWORD", tempPassword);
AdminInitiateAuthRequest initialRequest = new AdminInitiateAuthRequest()
{
AuthFlow = AuthFlowType.ADMIN_NO_SRP_AUTH,
AuthParameters = initialParams,
ClientId = appClientId_tenantApi,
UserPoolId = poolId
};
AdminInitiateAuthResponse resInitAuth = await cognitoProvider.AdminInitiateAuthAsync(initialRequest);
InitiateAuthRresponse has email as a required attribute.
{[requiredAttributes, ["userAttributes.email"]]}
But the documentation doesn't say so.
For ADMIN_NO_SRP_AUTH: USERNAME (required), SECRET_HASH (if app client is configured with client secret), PASSWORD (required), DEVICE_KEY
Admin Respond to challenge
var authParameters = new Dictionary<string, string>();
authParameters.Add("USERNAME", userName);
authParameters.Add("NEW_PASSWORD", newPassword);
AdminRespondToAuthChallengeRequest adminAuthRequest = new AdminRespondToAuthChallengeRequest()
{
UserPoolId = poolId,
ClientId = appClientId_tenantApi,
ChallengeName = ChallengeNameType.NEW_PASSWORD_REQUIRED,
ChallengeResponses = authParameters,
Session = session
};
cognitoProvider.AdminRespondToAuthChallengeAsync(adminAuthRequest);
I am thinking I may missed some user settings in Cognito to avoid email. Any one have similar experience ? or is this not possible to create user without email ?
During the creation of the user pool, under general settings;attributes as in the photocognito creation on aws one is required to choose the attributes that must be present, i believe in your case the email was selected by default hence the challenge request response you got.
The admin create user request requires the client to confirm the email for purposes of verification that the user owns the email.
A hack for the same would be to allow users to sign themselves up on your cognito configuration, then sign someone up then follow with a username and password, then proceed to confirm them as an admin
var signup = await cognitoClient.SignUpAsync(new SignUpRequest
{
Username = person.Username,
ClientId = cognitoOptions.ClientId,
Password = person.IdNumber,
});
var confirm = await cognitoClient.AdminConfirmSignUpAsync(new AdminConfirmSignUpRequest
{
Username = person.Username,
UserPoolId = cognitoOptions.UserPoolId
});
In case if anyone still looking for answer
Initalize Provider.
AmazonCognitoIdentityProviderClient provider = new AmazonCognitoIdentityProviderClient("*************", "************", Amazon.RegionEndpoint.USWest);
Create user
AdminCreateUserResponse adminCreateUserResponse = await provider.AdminCreateUserAsync(new AdminCreateUserRequest
{
Username = "TestUser",
TemporaryPassword = "TempPassword#1",
UserPoolId = "us-west-**********"
});
Authenticate user
CognitoUserPool userPool = new CognitoUserPool("us-west-***", "***", provider);
CognitoUser user = new CognitoUser("TestUser", "******", userPool, provider, "**********");
InitiateSrpAuthRequest authRequest = new InitiateSrpAuthRequest()
{
Password = "TempPassword#1"
};
AuthFlowResponse authResponse = await user.StartWithSrpAuthAsync(authRequest).ConfigureAwait(false);
Vaidate user authentication result and get the user AccessToken
if (authResponse.AuthenticationResult == null)
{
if (authResponse.ChallengeName == ChallengeNameType.NEW_PASSWORD_REQUIRED)
{
//Console.WriteLine("Enter your desired new password:");
string newPassword = "NewPWD#1";// Console.ReadLine();
Dictionary<string, string> att = new Dictionary<string, string>();
att.Add("userAttributes.email", "testemail#xyz.com");
user.Attributes.Add("preferred_username", "TestUser1");
And update the new password using Accesstoken ( post update the User status will be confirmed)
authResponse = await user.RespondToNewPasswordRequiredAsync(new RespondToNewPasswordRequiredRequest()
{
SessionID = authResponse.SessionID,
NewPassword = newPassword,
},att);
accessToken = authResponse.AuthenticationResult.AccessToken;
}
I'm working on a bot using bot framework. With active directory authentication I managed to get the username . Now I want to get the phone number and logged in user Email ID after authenticated using active directory ?
Below is the code I'm working with.
Authentication
AuthenticationOptions options = new AuthenticationOptions()
{
UseMagicNumber = false,
Authority = Convert.ToString(ConfigurationManager.AppSettings["aad:Authority"]),
ClientId = Convert.ToString(ConfigurationManager.AppSettings["aad:ClientId"]),
ClientSecret = Convert.ToString(ConfigurationManager.AppSettings["aad:ClientSecret"]),
ResourceId = Convert.ToString(ConfigurationManager.AppSettings["aad:ResourceId"]),
RedirectUrl = Convert.ToString(ConfigurationManager.AppSettings["aad:Callback"])
};
await context.Forward(new AuthDialog(new ADALAuthProvider(), options), ResumeAfterLogin, message, context.CancellationToken);
Extracting the data
private async Task ResumeAfterLogin(IDialogContext authContext, IAwaitable<AuthResult> authResult)
{
string tokenstring = string.Empty;
string userName = string.Empty;
var resultToken = await authResult;
string email = string.Empty;
try
{
tokenstring = resultToken.AccessToken;
userName = resultToken.UserName;
MyGlobalVariables.EmailID = "";
MyGlobalVariables.username = userName;
if (null != tokenstring && string.Empty != tokenstring)
{
authContext.UserData.SetValue<string>("AccessToken", tokenstring);
authContext.UserData.SetValue<string>("userName", userName);
await authContext.PostAsync($"*info: you are logged in as {userName}*");
authContext.Call(new RootDialog(), this.ResumeAfterOptionDialog);
}
}
catch (Exception ex)
{
authContext.Wait(MessageReceivedAsync);
throw ex;
}
finally
{
}
}
You can get phone numbers and emails of logged in users by accessing the Microsoft AAD Graph API. For example:
public async Task<User> GetMe()
{
var graphClient = GetAuthenticatedClient();
var me = await graphClient.Me.Request().GetAsync();
return me;
}
A full sample can be found here.
In our project we have a user management system build on the ASP.NET Identity framework.
When a user registers by providing an email, username and password, everything works fine. We are able to get the users ID in the method in every controller that inherit "ApiController".
However, now we are trying to implement external log in providers, and we are starting off with Facebook. The registration is going smooth, and the user is created in our database, just any other user, but without a PasswordHash of cause, and an access token is retured back to the client for further authorization.
All of that is working as it should, but when it comes to the part, where the programmer should be able to receive the users id with "User.Identity.GetUserId", we are having a little problem. The "User.Identity" is containing the right "userName" but the "GetUserId" is always returning "null".
The following is our registration method, and the generation of the access token
[OverrideAuthentication]
[AllowAnonymous]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("RegisterExternal")]
public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var verifiedAccessToken = await VerifyExternalAccessToken(model.Provider, model.AccessToken);
if (verifiedAccessToken == null)
{
return BadRequest("Invalid Provider or External Access Token");
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
var info = new ExternalLoginInfo()
{
DefaultUserName = model.UserName,
Login = new UserLoginInfo(model.Provider, verifiedAccessToken.user_id)
};
var accessTokenResponse = GenerateLocalAccessTokenResponse(model.UserName, user.Id, model.Provider);
return Ok(accessTokenResponse);
}
catch (Exception e)
{
return null;
}
}
private JObject GenerateLocalAccessTokenResponse(string userName, string userid, string provider)
{
var tokenExpiration = TimeSpan.FromDays(1);
ClaimsIdentity identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Sid, userid));
identity.AddClaim(new Claim(ClaimTypes.Name, userName));
identity.AddClaim(new Claim("role", "user"));
var props = new AuthenticationProperties()
{
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.Add(tokenExpiration),
};
var ticket = new AuthenticationTicket(identity, props);
var accessToken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket);
JObject tokenResponse = new JObject(
new JProperty("userName", userName),
new JProperty("access_token", accessToken),
new JProperty("token_type", "bearer"),
new JProperty("external_provider", provider),
new JProperty("expires_in", tokenExpiration.TotalSeconds),
new JProperty(".issued", ticket.Properties.IssuedUtc.ToString()),
new JProperty(".expires", ticket.Properties.ExpiresUtc.ToString())
);
return tokenResponse;
}
So all in all, every part of the registration is working as it should, we are just not able to receive the user id of the current user, when it uses an access token for a user created by an external provider.
Well, in case someone in the future needs the answer, it was because the "GetUserId" looked for the claim called "NameIdentifier", so changeing it, made it work.
I have this code for capturing user credentials:
string domain = Domain.GetComputerDomain().ToString();
Console.WriteLine(domain);
string username =
new System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent())
.Identity.Name;
Console.WriteLine(username);
Console.Write("Password: ");
//there are far better ways to get a hidden password this was just an easy way as it's irrelevant to the point of the application, will improve
string password = null;
while (true)
{
var key = Console.ReadKey(true);
if (key.Key == ConsoleKey.Enter)
break;
password += key.KeyChar;
}
And this method for authenticating with Kerberos:
private static bool ValidateCredentialsKerberos(string username, string password, string domain)
{
var credentials
= new NetworkCredential(username, password, domain);
var id = new LdapDirectoryIdentifier(domain);
using (var connection = new LdapConnection(id, credentials, AuthType.Kerberos))
{
connection.SessionOptions.Sealing = true;
connection.SessionOptions.Signing = true;
try
{
connection.Bind();
}
catch (LdapException lEx)
{
if (ERROR_LOGON_FAILURE == lEx.ErrorCode)
{
return false;
}
throw;
}
}
return true;
}
It always throws false as incorrect credentials despite the credentials being correct. The output into the console is as follows:
Domain.net
Domain/user
Password
Any thoughts?
The problem is that new System.Security.Principal.WindowsPrincipal(System.Security.Principal.WindowsIdentity.GetCurrent()).Identity.Name; returns the username in DOMAIN\username format, whereas LdapConnection expects to see just the username (you are already sending the domain as another parameter).
You can use Environment.UserName to get just the username.
Another issue is that the ErrorCode you are checking against isn't correct. You'll get "The supplied credential is invalid." message from the DC (error code 49).
(By the way, you didn't have to create a new WindowsPrincipal, you could have just use System.Security.Principal.WindowsIdentity.GetCurrent().Name)