How to securely store service passwords with ASP.NET Membership? - asp.net

I am using ASP.NET MVC 3 membership for my site with defaults. Thus the membership password for the user is stored securely.
My service requires the user to enter username/passwords for other web services they use. I access those services from my service. If I save the users' service passwords I need to ensure that those usernames/passwords are stored securely as well so that if someone hacks my server they will not be exposed.
I understand the general concepts of how this might be done (encrypt the username/pw using the hash of the ASP.NET membership pw they've provided as the key). But I don't know the specific APIs or correct patterns.
I also think it's basically impossible to really do this because if someone hacks my service they could simply use the hash themselves to decrypt the passwords. Am I right about that.
Assuming i'm wrong, and it is possible to do what I want, assume my model contains something like this:
public class MSExchangeSettings
{
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Email address for your Exchange account")]
public string EmailAddress { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password for your Exchange account")]
public string Password { get; set; }
...
}
Can someone please provide an example of how to do this correctly?
If, this is not possible I'll have to resort to asking the user for the PW every every time which I want to avoid. Of course, for the services I use that support OpenID or OAuth I have other alternatives, but for this specific example (Exchange) I need a username/pw.

Take a look at this
private static string GetPasswordHashed(string password) {
var saltBytes = new byte[0x10];
using (var random = new RNGCryptoServiceProvider()) {
random.GetBytes(saltBytes);
}
var passwordBytes = Encoding.Unicode.GetBytes(password);
var combinedBytes = saltBytes.Concat(passwordBytes).ToArray();
byte[] hashBytes;
using (var hashAlgorithm = HashAlgorithm.Create("HashAlgorithm")) {
hashBytes = hashAlgorithm.ComputeHash(combinedBytes);
}
var PasswordHashed = Convert.ToBase64String(hashBytes);
return PasswordHashed;
}

Related

Assign Delegated permission for System Assigned Managed Identity to Graph Api

I am trying to setup Managed Identity (system assigned), to assign delegated permission (like Tasks.ReadWrite) and then to use it to call Graph Api.
I have identified object Id using following code:
$app = Get-AzureADServicePrincipal -All $true -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"
$role = $app.Oauth2Permissions | where-Object { $_.AdminConsentDisplayName -eq "Create, read, update, and delete user’s tasks and task lists" }
but when I run following command:
New-AzureADServiceAppRoleAssignment -Id $role.Id -ObjectId $miObjectID -PrincipalId $miObjectID -ResourceId $app.ObjectId
where $miObjectID is my managed identity Id, I am getting following error message:
New-AzureADServiceAppRoleAssignment : Error occurred while executing NewServicePrincipalAppRoleAssignment
Code: Request_BadRequest
Message: Permission being assigned was not found on application
I also tried to do the same. My understanding is, that this is not intended to be.
Typically, user-delegated permissions are supposed to be used interactively, involving user interaction, e.g. on a web-app accessing user resources on his behalf. If, like me, you're developing a backend service, this won't fly. For this scenario the supported way is to just use application permissions and be done with it.
However, imho application permissions are often too broad, granting access to ressources that are irrelevant. E.g. if your app needs to have access to one specific Sharepoint site, you have to authorize access for all sites in your tenant. Application permission cannot be scoped.
Due to compliance reasons this is not acceptable. Especially if you work in a LOB org.
Still, I did find a workaround to have the best of both worlds, i.e. having really scoped permissions and have the ability to leverage these unattended in a backend service. But there is one caveat: I did not get it working with a managed identity I had to use a regular service principal. If that's a compromise you can accept, the following may be helpful to you.
Dedicate a user principal for this scenario. Authorise that user as needed. Choose a password with max length, i.e. 256 chars. Enable MFA.
Create an app/service principal in Azure AD, generate client/app credentials
Create a demo web-app locally using available templates and the MSAL lib. Have the above app request the required user-delegated permissions from the user
Then, in the app code, use the Resource-owner password credential, ROPC flow to authenticate the app and assume the permissions from the user
public class RessourceOwnerPasswordCredentialFlow
{
public static async Task<AccessToken> GetToken(HttpClient http, string credJson, Guid tenantId)
{
var ropc = credJson.Deserialize<GraphROPC>();
var dict = new Dictionary<string, string>();
dict.Add("grant_type", "password"); // ROPC
dict.Add("username", ropc.User);
dict.Add("password", ropc.Password);
dict.Add("client_id", ropc.ClientId);
dict.Add("client_secret", ropc.ClientSecret);
dict.Add("scope", ".default");
var url = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = new FormUrlEncodedContent(dict) };
var res = await http.SendAsync(req);
var content = await res.Content.ReadAsStreamAsync();
var authResp = await content.DeserializeAsync<GraphAuthResponse>();
return new AccessToken(authResp.access_token, DateTimeOffset.UtcNow.AddSeconds(authResp.expires_in));
}
}
public class GraphROPC
{
public string User { get; set; }
public string Password { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}
public class GraphAuthResponse
{
public string token_type { get; set; }
public string scope { get; set; }
public int expires_in { get; set; }
public string access_token { get; set; }
}
Note: Using the ROPC is not recommended by Microsoft.
However, I have found most of the objections to be not applicable in my case.
There is really no alternative to use delegated-permissions within a backend service app
I do have absolute trust in the app, because it's developed by me
The app does have to know the user credentials, these are stored in a KeyVault to which only the app has access (this time using MSI)
Also, one could argue that using two sets of credentials, user credentials plus app credentials is superior to using just the client credentials one would use if working with (more excessive) application permissions.
The ROPC doc says, that user accounts with MFA are not supported. However, I can confirm it is possible to work-around this restriction if conditional access policies are used. In our case the app has a fixed outbound public IP address which can be added as a trusted location. In fact, if the ability to whitelist a trusted location from the MFA requirement were missing, this would have been a blocker for the above steps.

Remove User from Directory Role using Graph API

I am trying to add and remove users from a directory role (Guest Inviter) based on a user's ID. My client ID has Directory.AccessAsUserAll for the Microsoft Graph application. I am using the ID for the directory role and the ID for the user. Using an HTTP Client call (verb is DELETE) I use the format suggested by Microsoft and get an "Insufficient privileges to complete the operation." error. I can perform other functions successfully
It seems clear to me that I am missing something. I would think that you still log in with the Client ID and Client Secret then do something with an admin type id and password rather than just create a new token based these credentials (because then why would you link them) similar to impersonation code but I don't know how and cannot seem to find an example of how.
Using HTTPClient
Verb DELETE
following this pattern
DELETE /directoryRoles/{id}/members/{id}/$ref
https://learn.microsoft.com/en-us/graph/api/directoryrole-delete-member?view=graph-rest-1.0&tabs=cs
Using C# creating bearer token (with client id and client secret) then using an HTTPCLient I call DeleteAsync using a url string based on the recommended pattern.
I see references to needing to pass user credential for a user in an admin role.
I think the issue is the absence of something important. This is called once the bearer token is obtained using client id and client secret for out tenant.
string delURL = $"{settings.RestUrl.value}{settings.RestVersion.value}/directoryRoles/{settings.GuestInviterRoleObjectID.value}/members/{user.id}/$ref";
HttpResponseMessage payload = await client.DeleteAsync(delURL);
Task<string> json = payload.Content.ReadAsStringAsync();
JObject o = new JObject();
if (json.Result.Length > 0)
{
o = JObject.Parse(json.Result);
}
I would like to remove the user from the Guest Inviter directory role. I get however
error: code:"authorization_requestDenied",
messsage: "Insufficient privileges to complete the operation" ....
Update: I was following this example https://dzone.com/articles/getting-access-token-for-microsoft-graph-using-oau-2
I built a class to contain the properties so after getting my original token using Client ID and Client secret then feeding in what I was told was a global admin credentials and now I get a 401 unauthorized error.
string tURL = $"https://login.microsoftonline.com/{settings.TenantID.value}/oauth2/token";
using (System.Net.WebClient c = new System.Net.WebClient())
{
c.Headers["Authorization"] = $"Bearer {token}";
c.Headers["Content-Type"] = "application/x-www-form-urlencoded";
System.Collections.Specialized.NameValueCollection data = new System.Collections.Specialized.NameValueCollection();
body.GetType().GetProperties().ToList().ForEach(delegate (System.Reflection.PropertyInfo item)
{
data.Add(item.Name, item.GetValue(body) == null ? string.Empty : item.GetValue(body).ToString());
});
var res = await Task.Run(() => c.UploadValues(tURL, data));
Task.WaitAll();
if(res != null)
{
string response = System.Text.Encoding.UTF8.GetString(res);
}
}
Data object
public class JSONBody
{
public string grant_type { get; set; }
public string client_id { get; set; }
public string client_secret { get; set; }
public string resource { get; set; }
public string username { get; set; }
public string password { get; set; }
public JSONBody()
{
this.grant_type = "password";
this.resource = "https://graph.microsoft.com";
}
}
I cannot prove or disprove the 401 error because I cannot prove my code works (or doesn't).
According to the documentation https://learn.microsoft.com/en-us/graph/api/directoryrole-delete-member
You’ll need an application with the delegated Directory.AccessAsUser.All permission. The you’ll need an admin to login to that application (with the correct permissions).
The application credentials (or client credentials flow) is unsupported, by design.
This could result in privilege elevation, if some admin would create an application with these permissions. If that admin would then be removed from the admin role he would be able to use his application to make himself admin again

Plain password in .net core SignInManager Identity

I have user table with plain passwords. I need connect the table into .net core web's AspNetUsers table.How to hash my user table's plain passwords like AspNetUsers's PasswordHash.
How can login with SignInManger plain-text password?
I recently did something like this. Our legacy system had its own password hashing method. I needed to covert everything over to asp.net users.
First thing I did was add two new columns to the Application user. These contain my legacy user password and that hash that was used to create it.
public string LegacyPasswordHash { get; set; }
public string LegacyPasswordSalt { get; set; }
Then i ran my sql script that copied all of the users in including their legacy password hash and salt.
Then i created a custom SignInManager.
public class ApplicationSignInManager : SignInManager<ApplicationUser> {}
In the password check method I test if its a legacy password user and if it is i covert the password that they just sent me over to a asp.net users password and delete their legacy password. Tip: is to remember to set the user security token on the user table as well this can not be null. You will have major issues with resting password if it is. As there is a bug in the token validation 2022
This is the section of the code i use for testing and resetting the password.
if (_password.EncodePassword(_user.LegacyPasswordSalt) == _user.LegacyPasswordHash)
{
_logger.LogInformation(LoggingEvents.LegacyUserCommand, "Legacy User {_user.Id} migrating password.", _user.Id);
await _userManager.AddPasswordAsync(_user, _password);
_user.SecurityStamp = Guid.NewGuid().ToString();
_user.LegacyPasswordHash = null;
_user.LegacyPasswordSalt = null;
await _userManager.UpdateAsync(_user);
return await new CheckTwoFactorCommand(_logger, _userManager, _user).Execute();
}
if (_shouldLockout)
{
_user.SecurityStamp = Guid.NewGuid().ToString();
await _userManager.UpdateAsync(_user);
_logger.LogInformation(LoggingEvents.LegacyUserCommand, "Login failed for Legacy user {_user.Id} invalid password. (LockoutEnabled)", _user.Id);
await _userManager.AccessFailedAsync(_user);
if (await _userManager.IsLockedOutAsync(_user))
return SignInResult.LockedOut;
}
_logger.LogInformation(LoggingEvents.LegacyUserCommand, "Login failed for Legacy user {_user.Id} invalid password", _user.Id);
return SignInResult.Failed;

How to implement Asp.net membership in my developed web application

I am working on a web application. I already completed it. But when I gone through security audit, I came to know that I should use asp.net membership for, login, password change, creating user, reset password etc.
So I stared using asp.net membership. I created a login page and it's working fine. I also got a database in my App_Data folder.
The problem is that I already have a database for my application where I have a user table which is having more fields than the table aspnet_Users. see the image below.
So please suggest me how to implement asp.net membership in my web application, as I need more fields in user table,
how to insert data along with my fields with the fields above mentioned in the above image, because I didn't fine any code through using asp.net membership. If I could got, I would surely make changes accordingly.
I mean how to merge this database and mine without any code.
I would also recommend that you use ASP.Net Identity as it is the framework for handling security.
However, if you are going to use simple membership, here is some code to help you "add fields" to your user table.
First, create your user model with all needed properties like so:
[Table("User")]
public class User
{
public int UserId { get; set; }
[Display(Name = "User name")]
[Required(ErrorMessage = "Usernname is required.")]
[StringLength(80, ErrorMessage = "Please enter a value no more than 80 characters.")]
public string UserName { get; set; }
[Display(Name = "First Name")]
[Required(ErrorMessage = "First name is required.")]
[StringLength(80, ErrorMessage = "Please enter a value no more than 80 characters.")]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
[Required(ErrorMessage = "Last name is required.")]
[StringLength(80, ErrorMessage = "Please enter a value no more than 80 characters.")]
public string LastName { get; set; }
[Required]
[Display(Name = "Email")]
[DataType(DataType.EmailAddress)]
[StringLength(254, ErrorMessage = "Please enter a value no more than 254 characters.")]
public string Email { get; set; }
//Add other properties... etc
}
Next, write the code to initialize your database. You probably have this code somewhere in the InitializeSimpleMembershipAttribute.cs file. Or, you can take it out from there and put in on your Application Start method. Here is the piece you need to modify:
WebSecurity.InitializeDatabaseConnection("YourDBConnectionName", "User", "UserId", "UserName", autoCreateTables: true);
//Modify the above properties if you change your user model to be a different table name
See reference to InitializeDatabaseConnection.
Now, when you want to create a user, call the following method:
WebSecurity.CreateUserAndAccount(user.UserName, user.Password, new { user.Email, user.FirstName, user.LastName });
//user is the User object that has all your data populated from some type of input
Reference to create user account: CreateUserAndAccount
The one you are trying to implement is legacy ASP.Net Membership Provider created using aspnet_regsql.exe. Do not use it, because it has been deprecated long time ago.
Currently, Microsoft offers 3 types of Memberships -
ASP.NET Universal Providers
Simple Membership Provider
ASP.NET Identity
Since your application is new and doesn't have existing users, you want to use -
ASP.NET Identity 2
The problem is that I already have a database for my application where
I have a user table which is having more fields than the table
aspnet_Users.
ASP.Net Identity lets you create custom fields in User table.
FYI: Make sure you use Version 2.

Can ServiceStack routes not handle special characters in their values?

I'm developing an API for our business.
Requests should require authTokens that require a POST http verb to retrieve. The flow should work like this-
User's client POSTS username and password (ssl protected) to the GetAuthToken service. Service returns auth token.
User's client can use token in any other request, no longer requiring POST.
I've written a convenience rest function, CheckAuthToken, to allow users to debug whether their working auth token is correct. It requires the email address of the user and the auth token to check. This works fine in SOAP and via POST, but the route doesn't seem to work via GET.
Here's the DTO:
[Route("/auth/CheckAuthToken/{EmailAddress}/{AuthToken}")]
public class CheckAuthToken
{
public string EmailAddress { get; set; }
public string AuthToken { get; set; }
}
If I request, for example (note the %2E, at the recommendation of this post: ServiceStack Handler Not Found When Periods Present in Path):
GET /auth/CheckAuthToken/aaron%40meemailsite%2Ecom/Hnot0real0auth0token4mzSBhKwFXY6xQcgX6XqsE%3D HTTP/1.1\r\n
I still get a 404. It seems like the period is being decoded before handing off to ServiceStack.
I recognize that I could use query string variables instead, making my request this:
GET /auth/CheckAuthToken?EmailAddress=aaronb%40bluebookinc%2Ecom&AuthToken=HwjQoKiHD2HSngFeeCH1k4mzSBhKwFXY6xQcgX6XqsE%3D HTTP/1.1\r\n
But, I was hoping to be more flexible than that, especially in my business layer REST services, which will also require user identification via email on certain functions.
Any ideas?
What version of ServiceStack are you running?
This is a passing test in the latest version (3.9.55). I also tested with a simple API with your endpoints and was having no problems passing those values in the url.
[Route("/auth/CheckAuthToken/{EmailAddress}/{AuthToken}")]
public class CheckAuthToken : IReturn
{
public string EmailAddress { get; set; }
public string AuthToken { get; set; }
}
[Test]
public void Test()
{
var url = new CheckAuthToken() {
EmailAddress = "me#test.com",
AuthToken = "fake"
}.ToUrl("GET");
Assert.That(url, Is.EqualTo("/auth/CheckAuthToken/me%40test.com/fake"));
}

Resources