We are building a system that will have a number of WCF services hosted in IIS sitting on an enterprise domain. A presentation tier server running in the DMZ will call these services. The calls to the WCF services need to be secured (i.e. require authentication). This system is a COTS system and will be deployed to a number of client sites.
WCF supports authenticating a caller using Windows authentication and x.509 certificates out-of-the-box. Windows authentication will not work for securing the WCF services in this scenario due to the fact that the DMZ presentation tier server will be in a different domain.
x.509 certificate security is an option and has been mentioned on other SO posts like the one below:
Accessing WCF Service using TCP from the DMZ (not on network or domain)
I have two concerns about x.509 certs:
Performance. I have yet to do performance analysis myself, but have heard from others that the overhead for validating x.509 certificates may make the solution a non-starter. My next task is to do performance analysis on this point.
Ease-of-deployment. I have found in the past that anytime x.509 certificates come into the picture for anything other than SSL that they cause problems for customer IT staff (procuring, generating, managing). This, in turn, causing a support issue for our product.
I'm considering using username/password security for securing the WCF calls for the reasons mentioned above. The solution would use a custom username/password validator.
https://msdn.microsoft.com/en-us/library/aa702565(v=vs.110).aspx
Credentials would be stored in a custom section of the web.config file on the presentation tier server in the DMZ. The same credentials would be stored in the web.config file on the application tier server. The sections containing the credentials would be encrypted on both servers.
Any other suggestions? Any thoughts on the custom username/password validator approach?
We did a lot of testing of various options. The solution that we ended up implementing was one that was configurable. It allows us to deploy username/password security as an option or to fall back to standard security approaches like x.509 certs for those clients that are comfortable with certs and can manage them.
There are four primary components to the solution:
A ServiceClientBase class that the web tier uses for making calls to services on the app tier.
A custom configuration section on the web tier for holding username/password credentials for authenticating to the services on the app tier.
A custom UserNamePasswordValidator class on the app tier for validating credentials.
A custom configuration section on the app tier for holding the list of username/password combinations that can be used for authentication.
The abridged ServiceClientBase class is shown below. The if/else blocks can be modified to include support for whatever bindings you desire to support. The main thing to point out about this class is that if security is used and the client credential type is "username", then we will load the username/password from the .config file. Otherwise, we fallback to using standard WCF security configuration.
public class ServiceClientBase<TChannel> : ClientBase<TChannel>, IDisposable where TChannel : class
{
public const string AppTierServiceCredentialKey = "credentialKey";
public ServiceClientBase()
{
bool useUsernameCredentials = false;
Binding binding = this.Endpoint.Binding;
if (binding is WSHttpBinding)
{
WSHttpBinding wsHttpBinding = (WSHttpBinding)binding;
if (wsHttpBinding.Security != null && wsHttpBinding.Security.Mode == SecurityMode.TransportWithMessageCredential)
{
if (wsHttpBinding.Security.Message != null && wsHttpBinding.Security.Message.ClientCredentialType == MessageCredentialType.UserName)
{
useUsernameCredentials = true;
}
}
}
else if (binding is BasicHttpBinding)
{
BasicHttpBinding basicHttpBinding = (BasicHttpBinding)binding;
if (basicHttpBinding.Security != null && basicHttpBinding.Security.Mode == BasicHttpSecurityMode.TransportWithMessageCredential)
{
if (basicHttpBinding.Security.Message != null && basicHttpBinding.Security.Message.ClientCredentialType == BasicHttpMessageCredentialType.UserName)
{
useUsernameCredentials = true;
}
}
}
...
if (useUsernameCredentials)
{
ServiceCredentialsSection section = (ServiceCredentialsSection)ConfigurationManager.GetSection(ServiceCredentialsSection.SectionName);
CredentialsElement credentials = section.Credentials[AppTierServiceCredentialKey];
this.ClientCredentials.UserName.UserName = credentials.UserName;
this.ClientCredentials.UserName.Password = credentials.Password;
}
}
// http://blogs.msdn.com/b/jjameson/archive/2010/03/18/avoiding-problems-with-the-using-statement-and-wcf-service-proxies.aspx
void IDisposable.Dispose()
{
if (this.State == CommunicationState.Faulted)
{
this.Abort();
}
else if (this.State != CommunicationState.Closed)
{
this.Close();
}
}
}
The custom configuration section class for credentials is shown below.
public class ServiceCredentialsSection : ConfigurationSection
{
public const string SectionName = "my.serviceCredentials";
public const string CredentialsTag = "credentials";
[ConfigurationProperty(CredentialsTag, IsDefaultCollection = false)]
[ConfigurationCollection(typeof(CredentialsCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
public CredentialsCollection Credentials
{
get
{
return (CredentialsCollection)this[CredentialsTag];
}
}
}
In addition to the ServiceCredentialsSection class, there is also a CredentialsCollection class (extending ConfigurationElementCollection) and a CredentialsElement class (extending ConfigurationElement). I won't include the CredentialsCollection class here because it's a long class and mainly full of stock code. You can find references implementations for ConfigurationElementCollection on the Internet, like at https://msdn.microsoft.com/en-us/library/system.configuration.configurationelementcollection(v=vs.110).aspx. The CredentialsElement class is shown below.
public class CredentialsElement : ConfigurationElement
{
[ConfigurationProperty("serviceName", IsKey = true, DefaultValue = "", IsRequired = true)]
public string ServiceName
{
get { return base["serviceName"] as string; }
set { base["serviceName"] = value; }
}
[ConfigurationProperty("username", DefaultValue = "", IsRequired = true)]
public string UserName
{
get { return base["username"] as string; }
set { base["username"] = value; }
}
[ConfigurationProperty("password", DefaultValue = "", IsRequired = true)]
public string Password
{
get { return base["password"] as string; }
set { base["password"] = value; }
}
}
The classes mentioned above supports a .config section like the one shown below. This section can be encrypted to secure the credentials. See Encrypting custom sections of a web.config for tips on encrypting a section of a .config file.
<my.serviceCredentials>
<credentials>
<add serviceName="credentialKey" username="myusername" password="mypassword" />
</credentials>
</my.serviceCredentials>
The third piece of the puzzle is the custom UserNamePasswordValidator. The code for this class is shown below.
public class PrivateServiceUserNamePasswordValidator : UserNamePasswordValidator
{
private IPrivateServiceAccountCache _accountsCache;
public IPrivateServiceAccountCache AccountsCache
{
get
{
if (_accountsCache == null)
{
_accountsCache = ServiceAccountsCache.Instance;
}
return _accountsCache;
}
}
public override void Validate(string username, string password)
{
if (!(AccountsCache.Validate(username, password)))
{
throw new FaultException("Unknown Username or Incorrect Password");
}
}
}
For performance reasons, we cache the sets of credentials against which the username/password pairs contained in service calls will be validated. The cache class is shown below.
public class ServiceAccountsCache : IPrivateServiceAccountCache
{
private static ServiceAccountsCache _instance = new ServiceAccountsCache();
private Dictionary<string, ServiceAccount> _accounts = new Dictionary<string, ServiceAccount>();
private ServiceAccountsCache() { }
public static ServiceAccountsCache Instance
{
get
{
return _instance;
}
}
public void Add(ServiceAccount account)
{
lock (_instance)
{
if (account == null) throw new ArgumentNullException("account");
if (String.IsNullOrWhiteSpace(account.Username)) throw new ArgumentException("Username cannot be null for a service account. Set the username attribute for the service account in the my.serviceAccounts section in the web.config file.");
if (String.IsNullOrWhiteSpace(account.Password)) throw new ArgumentException("Password cannot be null for a service account. Set the password attribute for the service account in the my.serviceAccounts section in the web.config file.");
if (_accounts.ContainsKey(account.Username.ToLower())) throw new ArgumentException(String.Format("The username '{0}' being added to the service accounts cache already exists. Verify that the username exists only once in the my.serviceAccounts section in the web.config file.", account.Username));
_accounts.Add(account.Username.ToLower(), account);
}
}
public bool Validate(string username, string password)
{
if (username == null) throw new ArgumentNullException("username");
string key = username.ToLower();
if (_accounts.ContainsKey(key) && _accounts[key].Password == password)
{
return true;
}
else
{
return false;
}
}
}
The cache above is initialized at application startup in the Global.Application_Start method as shown below.
// Cache service accounts.
ServiceAccountsSection section = (ServiceAccountsSection)ConfigurationManager.GetSection(ServiceAccountsSection.SectionName);
if (section != null)
{
foreach (AccountElement account in section.Accounts)
{
ServiceAccountsCache.Instance.Add(new ServiceAccount() { Username = account.UserName, Password = account.Password, AccountType = (ServiceAccountType)Enum.Parse(typeof(ServiceAccountType), account.AccountType, true) });
}
}
The last piece of the puzzle is the custom configuration section on the app tier for holding the list of username/password combinations. The code for this section is shown below.
public class ServiceAccountsSection : ConfigurationSection
{
public const string SectionName = "my.serviceAccounts";
public const string AccountsTag = "accounts";
[ConfigurationProperty(AccountsTag, IsDefaultCollection = false)]
[ConfigurationCollection(typeof(AccountsCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
public AccountsCollection Accounts
{
get
{
return (AccountsCollection)this[AccountsTag];
}
}
}
As before, there is a custom ConfigurationElementCollection class and a custom ConfigurationElement class. The ConfigurationElement class is shown below.
public class AccountElement : ConfigurationElement
{
[ConfigurationProperty("username", IsKey = true, DefaultValue = "", IsRequired = true)]
public string UserName
{
get { return base["username"] as string; }
set { base["username"] = value; }
}
[ConfigurationProperty("password", DefaultValue = "", IsRequired = true)]
public string Password
{
get { return base["password"] as string; }
set { base["password"] = value; }
}
[ConfigurationProperty("accountType", DefaultValue = "", IsRequired = true)]
public string AccountType
{
get { return base["accountType"] as string; }
set { base["accountType"] = value; }
}
}
These configuration classes support a .config file XML snippet as shown below. As before, this section can be encrypted.
<my.serviceAccounts>
<accounts>
<add username="myusername" password="mypassword" accountType="development" />
</accounts>
</my.serviceAccounts>
Hope this may help someone.
Related
I am attempting to use a PostgreSQL database managed by Heroku in an ASP.NET MVC app running on .NET core 2.0. I would like to be able to use the Entity Framework to easily read and write to the database. I am extremely new to all of these things except for ASP.NET, which is likely obvious, and having previously used a local SQLite server for this same purpose, I have almost no understanding of how PostgreSQL works with Heroku and the Entity Framework.
I have installed the Npgsql extension to the Entity Framework. I am stuck at the Entity Framework's connection string for this particular setup and how to use it with Heroku. Heroku supplies a DATABASE_URL variable (documented here), which is necessary to use because the database connection credentials are subject to change and Heroku automatically updates the variable when they change.
//This method gets called by the runtime. Use this method to add services to
//the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
string connection = "???";
services.AddDbContext<MyDbContext>(options => options.UseNpgsql(connection));
}
What value do I use for connection such that it will connect to the database through Heroku's URL independently of the current credentials? Additionally, how can I ensure that a table will be created in the database matching MyDbContext's model?
My goal is simply to have a database accessible (read and write) from a deployed Heroku website. It would be nice if I could also access the database locally for development purposes, but my only requirement is that this work on the hosted website and that the database is managed by Heroku (I went with PostgreSQL, Kafka and Redis are also available through Heroku).
The solution here is to "parse" the content of the DATABASE_URLenvironment variable provided by Heroku and use it to build the connection string in the format that the Npgsql expects.
For a quick and dirty solution, you can just follow this solution: .net core - database url parser.
For my project I decided to go a little further and created a class for that, based in the other connection string builders (for MS SQL, Mongo and etc):
public enum SslMode
{
Require,
Disable,
Prefer
}
public class PostgreSqlConnectionStringBuilder : DbConnectionStringBuilder
{
private string _database;
private string _host;
private string _password;
private bool _pooling;
private int _port;
private string _username;
private bool _trustServerCertificate;
private SslMode _sslMode;
public PostgreSqlConnectionStringBuilder(string uriString)
{
ParseUri(uriString);
}
public string Database
{
get => _database;
set
{
base["database"] = value;
_database = value;
}
}
public string Host
{
get => _host;
set
{
base["host"] = value;
_host = value;
}
}
public string Password
{
get => _password;
set
{
base["password"] = value;
_password = value;
}
}
public bool Pooling
{
get => _pooling;
set
{
base["pooling"] = value;
_pooling = value;
}
}
public int Port
{
get => _port;
set
{
base["port"] = value;
_port = value;
}
}
public string Username
{
get => _username;
set
{
base["username"] = value;
_username = value;
}
}
public bool TrustServerCertificate
{
get => _trustServerCertificate;
set
{
base["trust server certificate"] = value;
_trustServerCertificate= value;
}
}
public SslMode SslMode
{
get => _sslMode;
set
{
base["ssl mode"] = value.ToString();
_sslMode = value;
}
}
public override object this[string keyword]
{
get
{
if (keyword == null) throw new ArgumentNullException(nameof(keyword));
return base[keyword.ToLower()];
}
set
{
if (keyword == null) throw new ArgumentNullException(nameof(keyword));
switch (keyword.ToLower())
{
case "host":
Host = (string) value;
break;
case "port":
Port = Convert.ToInt32(value);
break;
case "database":
Database = (string) value;
break;
case "username":
Username = (string) value;
break;
case "password":
Password = (string) value;
break;
case "pooling":
Pooling = Convert.ToBoolean(value);
break;
case "trust server certificate":
TrustServerCertificate = Convert.ToBoolean(value);
break;
case "sslmode":
SslMode = (SslMode) value;
break;
default:
throw new ArgumentException(string.Format("Invalid keyword '{0}'.", keyword));
}
}
}
public override bool ContainsKey(string keyword)
{
return base.ContainsKey(keyword.ToLower());
}
private void ParseUri(string uriString)
{
var isUri = Uri.TryCreate(uriString, UriKind.Absolute, out var uri);
if (!isUri) throw new FormatException(string.Format("'{0}' is not a valid URI.", uriString));
Host = uri.Host;
Port = uri.Port;
Database = uri.LocalPath.Substring(1);
Username = uri.UserInfo.Split(':')[0];
Password = uri.UserInfo.Split(':')[1];
}
}
And then, in my Startup.cs, in the Configuration method, I have:
var builder = new PostgreSqlConnectionStringBuilder(Configuration["DATABASE_URL"])
{
Pooling = true,
TrustServerCertificate = true,
SslMode = SslMode.Require
};
services.AddEntityFrameworkNpgsql()
.AddDbContext<TTRDbContext>(options => options.UseNpgsql(builder.ConnectionString));
If you are accessing your DB from outside Heroku network (e.g. your local environment), you need to add the SSL Mode and Trust Server Certificate.
Hope it helps
I'm integrating our asp.net MVC application with SAML2 Authentication. And using Kentor.AuthServices as module as described at kentor.AuthServices Configuration
Everithing works fine. But next step is to add usage of second service provider (which configured to use another auth mechanisms on server side) only for specified range of pages.
First, how to configure it via web.config to add second SP (not the second IdP in scope of first SP) with different entityId.
And Second, how to switch programmatically to second SP? I assume that it should happend in global.asax file in method Application_BeginRequest, but how?
Using two different SP instances in the same application is a quite rare scenario. But if you are really sure you need it, it can be achieved.
You will have to use the Kentor.AuthServices.Owin package and do the configuration in code - web.config won't do. Register two instances of the middleware. Each one will have their own configuration, including their own SP EntityID. Also make sure to change the ModulePath of at least one of them so that they get different endpoint addresses.
To challenge an authentication from either one, set the right authentication scheme in the challenge (typically in a ChallengeResult returned from a controller)
Self-answering.
Here is a workaround for multiple SP for MVC or HttpModule package, switching is based on specified range of URLs. In my case different SP realize different amount of security factors.
First, implementing custom IOptions and CookieHandler, with ability to switch to correct instance. In the web.config file, two kentor.authServices sections must be defined. In my case only "entityId" attribute differs.
public class CustomOptions : IOptions
{
private IOptions options1Factor;
private IOptions options2Factor;
private Func<bool> _checkIsSecure;
public CustomOptions(Func<bool> checkIsSecure)
{
_checkIsSecure = checkIsSecure;
AddOption(out options2Factor, "kentor.authServices1");
AddOption(out options1Factor, "kentor.authServices");
}
private void AddOption(out IOptions options, string sectionName)
{
var sp = new SPOptions((KentorAuthServicesSection)ConfigurationManager.GetSection(sectionName));
options = new Options(sp);
KentorAuthServicesSection.Current.IdentityProviders.RegisterIdentityProviders(options);
KentorAuthServicesSection.Current.Federations.RegisterFederations(options);
}
public SPOptions SPOptions
{
get
{
if (_checkIsSecure())
return options2Factor.SPOptions;
return options1Factor.SPOptions;
}
}
public IdentityProviderDictionary IdentityProviders
{
get
{
if (_checkIsSecure())
return options2Factor.IdentityProviders;
return options1Factor.IdentityProviders;
}
}
public KentorAuthServicesNotifications Notifications
{
get
{
if (_checkIsSecure())
return options2Factor.Notifications;
return options1Factor.Notifications;
}
}
}
public class CustomCookieHandler : CookieHandler
{
private Func<bool> _checkIsSecure;
private CookieHandler _originalCookieHandler1Factor;
private CookieHandler _originalCookieHandler2Factor;
public CustomCookieHandler(Func<bool> checkIsSecure)
{
_checkIsSecure = checkIsSecure;
_originalCookieHandler1Factor = new ChunkedCookieHandler()
{
Name = "commonAuth",
RequireSsl = false
};
_originalCookieHandler2Factor = new ChunkedCookieHandler()
{
Name = "securedAuth",
RequireSsl = false
};
}
public override string MatchCookiePath(Uri baseUri, Uri targetUri)
{
if (_checkIsSecure())
return _originalCookieHandler2Factor.MatchCookiePath(baseUri, targetUri);
return _originalCookieHandler1Factor.MatchCookiePath(baseUri, targetUri);
}
protected override void DeleteCore(string name, string path, string domain, HttpContext context)
{
if (_checkIsSecure())
_originalCookieHandler2Factor.Delete();
else
_originalCookieHandler1Factor.Delete();
}
protected override byte[] ReadCore(string name, HttpContext context)
{
if (_checkIsSecure())
return _originalCookieHandler2Factor.Read();
return _originalCookieHandler1Factor.Read();
}
protected override void WriteCore(byte[] value, string name, string path, string domain, DateTime expirationTime, bool secure, bool httpOnly, HttpContext context)
{
if (_checkIsSecure())
_originalCookieHandler2Factor.Write(value, true, expirationTime);
else
_originalCookieHandler1Factor.Write(value, true, expirationTime);
}
}
In Global.asax file setting static properties to custom implementations. No more modifications needed.
protected void Application_Start()
{
FederatedAuthentication.FederationConfiguration.CookieHandler = new CustomCookieHandler(CheckIsSecure);
Kentor.AuthServices.Mvc.AuthServicesController.Options = new CustomOptions(CheckIsSecure);
}
private bool CheckIsSecure()
{
if (HttpContext.Current == null)
return false;
var mainHost = "http://host.local"; // host url
var sp = new [] { "/Home/Secure" }; // array of URLs which must be secured with other SP
var request = HttpContext.Current.Request;
var isSecured = sp.Any(x => x.Equals(request.Path, StringComparison.InvariantCultureIgnoreCase));
if (!isSecured && request.Path.Equals("/AuthServices/SignIn", StringComparison.InvariantCultureIgnoreCase))
{
var returnUrl = request.QueryString["ReturnUrl"];
isSecured = !string.IsNullOrEmpty(returnUrl) &&
sp.Any(x => x.Equals(returnUrl, StringComparison.InvariantCultureIgnoreCase));
}
if (!isSecured && request.Path.Equals("/AuthServices/Acs", StringComparison.InvariantCultureIgnoreCase))
{
var _r = new HttpRequestWrapper(request).ToHttpRequestData();
isSecured = _r != null && _r.StoredRequestState != null && _r.StoredRequestState.ReturnUrl != null
&& sp.Any(x => x.Equals(_r.StoredRequestState.ReturnUrl.ToString(),
StringComparison.InvariantCultureIgnoreCase));
}
if (!isSecured && !string.IsNullOrEmpty(request.Headers["Referer"]))
{
var referer = request.Headers["Referer"];
isSecured = sp
.Select(x => string.Format("{0}/{1}", mainHost.TrimEnd('/'), x.TrimStart('/')))
.Any(x => x.Equals(referer, StringComparison.InvariantCultureIgnoreCase));
}
return isSecured;
}
I'm porting a website to dnx core/aspnet5/mvc6. I need to store passwords to 3rd party sites in the database (it's essentially an aggregator).
In earlier versions of mvc, I did this using classes like RijndaelManaged. But those don't appear to exist in dnx core. In fact, I haven't been able to find much documentation on any general purpose encryption/decryption stuff in dnx core.
What's the recommended approach for encrypting/decrypting single field values in an mvc6 site? I don't want to encrypt the entire sql server database.
Or should I be looking at a different approach for storing the credentials necessary to access a password-protected 3rd party site?
See the DataProtection API documentation
Their guidance on using it for persistent data protection is a little hedgy but they say there is no technical reason you can't do it. Basically to store protected data persistently you need to be willing to allow unprotecting it with expired keys since the keys could expire after you protect it.
To me it seems reasonable to use it and I am using it in my own project.
Since the IPersistedDataProtector only provides methods with byte arrays I made a couple of extension methods to convert the bytes back and forth from string.
public static class DataProtectionExtensions
{
public static string PersistentUnprotect(
this IPersistedDataProtector dp,
string protectedData,
out bool requiresMigration,
out bool wasRevoked)
{
bool ignoreRevocation = true;
byte[] protectedBytes = Convert.FromBase64String(protectedData);
byte[] unprotectedBytes = dp.DangerousUnprotect(protectedBytes, ignoreRevocation, out requiresMigration, out wasRevoked);
return Encoding.UTF8.GetString(unprotectedBytes);
}
public static string PersistentProtect(
this IPersistedDataProtector dp,
string clearText)
{
byte[] clearBytes = Encoding.UTF8.GetBytes(clearText);
byte[] protectedBytes = dp.Protect(clearBytes);
string result = Convert.ToBase64String(protectedBytes);
return result;
}
}
I also created a helper class specifically for protecting certain properties on my SiteSettings object before it gets persisted to the db.
using cloudscribe.Core.Models;
using Microsoft.AspNet.DataProtection;
using Microsoft.Extensions.Logging;
using System;
namespace cloudscribe.Core.Web.Components
{
public class SiteDataProtector
{
public SiteDataProtector(
IDataProtectionProvider dataProtectionProvider,
ILogger<SiteDataProtector> logger)
{
rawProtector = dataProtectionProvider.CreateProtector("cloudscribe.Core.Models.SiteSettings");
log = logger;
}
private ILogger log;
private IDataProtector rawProtector = null;
private IPersistedDataProtector dataProtector
{
get { return rawProtector as IPersistedDataProtector; }
}
public void Protect(ISiteSettings site)
{
if (site == null) { throw new ArgumentNullException("you must pass in an implementation of ISiteSettings"); }
if (site.IsDataProtected) { return; }
if (dataProtector == null) { return; }
if (site.FacebookAppSecret.Length > 0)
{
try
{
site.FacebookAppSecret = dataProtector.PersistentProtect(site.FacebookAppSecret);
}
catch (System.Security.Cryptography.CryptographicException ex)
{
log.LogError("data protection error", ex);
}
}
// ....
site.IsDataProtected = true;
}
public void UnProtect(ISiteSettings site)
{
bool requiresMigration = false;
bool wasRevoked = false;
if (site == null) { throw new ArgumentNullException("you must pass in an implementation of ISiteSettings"); }
if (!site.IsDataProtected) { return; }
if (site.FacebookAppSecret.Length > 0)
{
try
{
site.FacebookAppSecret = dataProtector.PersistentUnprotect(site.FacebookAppSecret, out requiresMigration, out wasRevoked);
}
catch (System.Security.Cryptography.CryptographicException ex)
{
log.LogError("data protection error", ex);
}
catch (FormatException ex)
{
log.LogError("data protection error", ex);
}
}
site.IsDataProtected = false;
if (requiresMigration || wasRevoked)
{
log.LogWarning("DataProtection key wasRevoked or requires migration, save site settings for " + site.SiteName + " to protect with a new key");
}
}
}
}
If the app will need to migrate to other machines after data has been protected then you also want to take control of the key location, the default would put the keys on the OS keyring of the machine as I understand it so a lot like machinekey in the past where you would override it in web.config to be portable.
Of course protecting the keys is on you at this point. I have code like this in the startup of my project
//If you change the key persistence location, the system will no longer automatically encrypt keys
// at rest since it doesn’t know whether DPAPI is an appropriate encryption mechanism.
services.ConfigureDataProtection(configure =>
{
string pathToCryptoKeys = appBasePath + Path.DirectorySeparatorChar
+ "dp_keys" + Path.DirectorySeparatorChar;
// these keys are not encrypted at rest
// since we have specified a non default location
// that also makes the key portable so they will still work if we migrate to
// a new machine (will they work on different OS? I think so)
// this is a similar server migration issue as the old machinekey
// where we specified a machinekey in web.config so it would not change if we
// migrate to a new server
configure.PersistKeysToFileSystem(new DirectoryInfo(pathToCryptoKeys));
});
So my keys are stored in appRoot/dp_keys in this example.
If you want to do things manually;
Add a reference to System.Security.Cryptography.Algorithms
Then you can create instances of each algorithm type via the create method. For example;
var aes = System.Security.Cryptography.Aes.Create();
I've been trying to understand how the reset password & account confirmation works in ASP.NET Identity. I'd just like to know if the Tokens are being stored and if so, where?
The links I receive when I'm using the password reset feature look something like this
http://localhost:1470/Account/ResetPassword?userId=a8b1389c-df93-4dfc-b463-541507c1a4bc&code=yhUegXIM9SZBpPVbBtv22kg7NO7F96B8MJi9MryAadUY5XYjz8srVkS5UL8Lx%2BLPYTU6a6jhqOrzMUkkMyPbEHPY3Ul6%2B%2F0s0qQvtM%2FLLII3s29FgkcK0OnjX46Bmj9JlFCUx53rOH%2FXMacwnKDzoJ1rbrUyypZiJXloIE50Q6iPuMTUHbX9O%2B3JMZtCVXjhhsHLkTOn9IVoN6uVAOMWNQ%3D%3D
My guess is that the tokens are stored in the link itself since I cannot find any trace of it anywhere else. Maybe someone knows for sure?
As I mentioned in the comment
"Tokens are generated using the SecurityStamp and validating against the SecurityStamp and not storing anywhere in database or local file storage. If you update the SecurityStamp, then previous tokens are no longer valid."
#DSR is correct but I would like to add some information to this as well.
If you have set up a Web project with Individual User Accounts go to:
App_Start -> IdentityConfig.cs
There you will see code like this:
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
The description for DataProtectorTokenProvider<TUser, TKey> gives the information:
Represents a token provider that uses an IDataProtector to generate
encrypted tokens based off of the security stamp.
https://learn.microsoft.com/en-us/previous-versions/aspnet/dn613280(v%3dvs.108)
We can however try to dig a bit deeper how it really works. The token verification will fail if different Application Pool Identities are used for creating and validating a token on a single server. This points to that the actual protection mechanism would look something like this:
System.Security.Cryptography.ProtectedData.Protect(userData, entropy, DataProtectionScope.CurrentUser);
Given that it works if all sites use the same Application Pool Identity points to this as well. Could also be DataProtectionProvider with protectionDescriptor "LOCAL=user". It should have worked with different Application Pool Identities if LOCAL=machine was set.
new DataProtectionProvider("LOCAL=user")
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.dataprotector?view=netframework-4.7.2
https://learn.microsoft.com/en-us/uwp/api/windows.security.cryptography.dataprotection.dataprotectionprovider
dataProtectionProvider is of type IDataProtectionProvider.
It is injected in Startup.Auth.cs like this:
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
CreatePerOwinContext is located in the assembly Microsoft.AspNet.Identity.Owin -> AppBuilderExtensions.cs. Both ASP.NET Identity and ASP.NET Core Identity are open source and can be viewed at GitHub.
public static IAppBuilder CreatePerOwinContext<T>(this IAppBuilder app,
Func<IdentityFactoryOptions<T>, IOwinContext, T> createCallback,
Action<IdentityFactoryOptions<T>, T> disposeCallback) where T : class, IDisposable
{
if (app == null)
{
throw new ArgumentNullException("app");
}
if (createCallback == null)
{
throw new ArgumentNullException("createCallback");
}
if (disposeCallback == null)
{
throw new ArgumentNullException("disposeCallback");
}
app.Use(typeof (IdentityFactoryMiddleware<T, IdentityFactoryOptions<T>>),
new IdentityFactoryOptions<T>
{
DataProtectionProvider = app.GetDataProtectionProvider(),
Provider = new IdentityFactoryProvider<T>
{
OnCreate = createCallback,
OnDispose = disposeCallback
}
});
return app;
}
https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Owin/Extensions/AppBuilderExtensions.cs
https://archive.codeplex.com/?p=aspnetidentity#src/Microsoft.AspNet.Identity.Owin/Extensions/AppBuilderExtensions.cs
app.GetDataProtectionProvider() is in turn located in assembly Microsoft.Owin.Security that is also Open Source.
public static IDataProtectionProvider GetDataProtectionProvider(this IAppBuilder app)
{
if (app == null)
{
throw new ArgumentNullException("app");
}
object value;
if (app.Properties.TryGetValue("security.DataProtectionProvider", out value))
{
var del = value as DataProtectionProviderDelegate;
if (del != null)
{
return new CallDataProtectionProvider(del);
}
}
return null;
}
https://github.com/aspnet/AspNetKatana/blob/release/src/Microsoft.Owin.Security/DataProtection/AppBuilderExtensions.cs
We can also see that CreateDataProtector has a fallback to the implementation DpapiDataProtectionProvider.
private static IDataProtectionProvider FallbackDataProtectionProvider(IAppBuilder app)
{
return new DpapiDataProtectionProvider(GetAppName(app));
}
When reading about DpapiDataProtectionProvider(DPAPI stands for Data Protection Application Programming Interface) the description says:
Used to provide the data protection services that are derived from the
Data Protection API. It is the best choice of data protection when you
application is not hosted by ASP.NET and all processes are running as
the same domain identity.
The Create method purposes are described as:
Additional entropy used to ensure protected data may only be
unprotected for the correct purposes.
The protector class itself then looks like this:
using System.Security.Cryptography;
namespace Microsoft.Owin.Security.DataProtection
{
internal class DpapiDataProtector : IDataProtector
{
private readonly System.Security.Cryptography.DpapiDataProtector _protector;
public DpapiDataProtector(string appName, string[] purposes)
{
_protector = new System.Security.Cryptography.DpapiDataProtector(appName, "Microsoft.Owin.Security.IDataProtector", purposes)
{
Scope = DataProtectionScope.CurrentUser
};
}
public byte[] Protect(byte[] userData)
{
return _protector.Protect(userData);
}
public byte[] Unprotect(byte[] protectedData)
{
return _protector.Unprotect(protectedData);
}
}
}
https://learn.microsoft.com/en-us/previous-versions/aspnet/dn253784(v%3dvs.113)
I am having a hard time implementing "Remember Me" functionality in an MVC application with a custom principal. I have boiled it down to ASP.NET not retrieving the authentication cookie for me. I have included a snapshot below from Google Chrome.
Shows the results of Request.Cookies that is set within the controller action and placed in ViewData for the view to read. Notice that it is missing the .ASPXAUTH cookie
Shows the results from the Chrome developer tools. You can see that .ASPXAUTH is included here.
What may be the issue here? Why does ASP.NET not read this value from the cookie collection?
My application uses a custom IPrincipal. BusinessPrincipalBase is a CSLA object that ust implements IPrincipal. Here is the code for that:
[Serializable()]
public class MoralePrincipal : BusinessPrincipalBase
{
private User _user;
public User User
{
get
{
return _user;
}
}
private MoralePrincipal(IIdentity identity) : base(identity)
{
if (identity is User)
{
_user = (User)identity;
}
}
public override bool Equals(object obj)
{
MoralePrincipal principal = obj as MoralePrincipal;
if (principal != null)
{
if (principal.Identity is User && this.Identity is User)
{
return ((User)principal.Identity).Equals(((User)this.Identity));
}
}
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool Login(string username, string password)
{
User identity = User.Fetch(username, password);
if (identity == null || !identity.IsAuthenticated)
{
identity = (User)User.UnauthenicatedIdentity;
}
MoralePrincipal principal = new MoralePrincipal(identity);
Csla.ApplicationContext.User = principal;
Context.Current.User = identity;
return identity != null && identity.IsAuthenticated;
}
public static void Logout()
{
IIdentity identity = User.UnauthenicatedIdentity;
MoralePrincipal principal = new MoralePrincipal(identity);
ApplicationContext.User = principal;
Context.Current.User = identity as User;
}
public override bool IsInRole(string role)
{
if (Context.Current.User == null || Context.Current.Project == null)
{
return false;
}
string userRole = Context.Current.User.GetRole(Context.Current.Project.Id);
return string.Compare(role, userRole, true) == 0;
}
The application also uses a custom membership provider. Here is the code for that.
public class MoraleMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
bool result = MoralePrincipal.Login(username, password);
HttpContext.Current.Session["CslaPrincipal"] = ApplicationContext.User;
return result;
}
#region Non-Implemented Properties/Methods
public override string ApplicationName
{
get
{
return "Morale";
}
set
{
throw new NotImplementedException();
}
}
// Everything else just throws a NotImplementedException
#endregion
}
I do not think that any of this is related because the bottom line is that the Request.Cookies does not return the authentication cookie. Is it related to the size of the cookie? I heard there are issues to the size of the cookie.
UPDATE: It seems that the issue revolves around subdomains. This site was being hosted with a subdomain and the cookie domain was left blank. Does anyone have any pointers on how I can get the auth cookie to work with all domains (e.g. http://example.com, http://www.example.com, and http://sub.example.com)?
If you are trying to store the actual User object in the cookie itself, it is probably too big to store as a cookie. I am not too familiar with the MVC authentication stuff, but in web forms I generally do the following:
FormsAuthentication.RedirectFromLoginPage(user_unique_id_here, false);
The second parameter is for the persistency you are looking for.
From there I create a custom context (UserContext) that I populate via HttpModule that gives me access to all the user and role information.
Since I do not develop in MVC (yet) or CSLA, I'm not sure how much more help I can be. If I were you, I would also ditch the custom membership provider. You might as well just call MoralePrincipal.Login directly in your Authentication controller.
The rememberMe stuff should be set by the FormsAuthenticationService (in MVC2) or the FormsAuthentication static class in MVC1, if you're using the 'regular' AccountController's code. If you changed that code, did you remember to add in the (optional) boolean param indicating whether to use a persistent cookie or not?
It sounds to me like you're getting a session cookie, but not a persistent cookie.