Authentication helps our apps to identify users and specifies access rights. That's why authentication will be attacked by hackers first. Let's look at a few things.
The most common authentication attack is brute force. We can specify two types of brute force.
The first and the most common attack is hacked accounts attack.
Any data leak helps an attacker to hack user profiles on other websites. An attacker has pairs with login and password that were downloaded from hacked resources. Sometimes passwords in stolen databases are not hashed, sometimes hash is very weak. We have a lot of free databases such as this with logins or passwords as a login on the Internet. It’s a big market where you can buy or sell any database. Do you want to brute force a gambling website? Try an account from another hacked gambling website. It’s really easy to get logins and passwords for brute force.
The next attack is very old school. I'm talking about a dictionary attack.
You just need several logins like root, admin, manager, test, and dictionary with the most common password. We can use this technique because we have a small limit of admin account names. You will be surprised how many weak passwords people use. Even if a user is an admin. A new surprise is no protection for authentication. I mean sometimes I can send so many requests as I can, without any limit. I think at this time Amazon with its flexible price says “show me what you can” or stuff like that.
So what should we implement to prevent these attacks or to make hackers' jobs more complex?
1. Captcha. Sometimes we think that captcha can scare our users. But people are used to captcha. And captcha could be hidden and works in the background. The next thing is hackers that have to spend money on captcha solving services and time to solve.
2. Request Per Second Control. A lot of web servers (IIS) can control RPS. You have to be careful with cell phone users, sometimes cell phone operator has not so many IP addresses and dynamic addresses changes.
3. Account authentication freeze. After a number of attempts, the system has to freeze an account for a couple of minutes. It prevents valuable accounts from brute force.
4. Two-factor authentication. It really helps and it very hard to avoid. But be with Random class. You should use RNGCryptoServiceProvider
To generate a cryptographically secure random number, such as one that's suitable for creating a random password, use the RNGCryptoServiceProvider class or derive a class from System.Security.Cryptography.RandomNumberGenerator
5. Strong password hashing. We can't be 100% safe. But, we have to take care of users' data so we have to store passwords with strong hashes, like SHA-2. In this case, if we lose our database, hackers have to spend a lot of time guessing our hashes. I say "guess" because hashing is a one-way action and we have to hash some data to understand if it has what we need or not. That's why developers use salt and pepper while hashing, this is additional data that we add to a password string before hashing.
Sometimes developers use symmetric algorithms like AES. But keys could be stolen, so it’s not a good way.
Ok, we have small RPC, account freezing, and strong hashes. But users can lose sessions anyway due to injection or lost valid passwords. How can we protect sessions?
Out-of-the-box Form Authentication and the Identity Server have wild card authentication. It means that in token we have information about a user and when this token ends. Sometimes sliding is turned on and our token has auto-renewal. In this case, the user can’t control its sessions, because we can’t terminate this token. This token is valid if a token lifetime does not end. And if a user changes a password, it can’t help.
We have to use the Identity Server with session control. We can control sessions only if we save them to DB. That's why we have to implement ITicketStore Interface
1. Create database object
public class AuthenticationTicket
{
public Guid Id { get; set; }
public string UserId { get; set; }
public ApplicationUser User { get; set; }
public byte[] Value { get; set; }
public DateTimeOffset? LastActivity { get; set; }
public DateTimeOffset? Expires { get; set; }
}
2. Create DbTicketStore and implement ITicketStore
public class DbTicketStore : ITicketStore
{
private readonly DbContextOptionsBuilder<ApplicationDbContext> _optionsBuilder;
public CustomTicketStore(DbContextOptionsBuilder<ApplicationDbContext> optionsBuilder)
{
_optionsBuilder = optionsBuilder;
}
public async Task RemoveAsync(string key)
{
using (var context = new ApplicationDbContext(_optionsBuilder.Options))
{
if (Guid.TryParse(key, out var id))
{
var ticket = await context.AuthenticationTickets.SingleOrDefaultAsync(x => x.Id == id);
if (ticket != null)
{
context.AuthenticationTickets.Remove(ticket);
await context.SaveChangesAsync();
}
}
}
}
public async Task RenewAsync(string key, AuthenticationTicket ticket)
{
using (var context = new ApplicationDbContext(_optionsBuilder.Options))
{
if (Guid.TryParse(key, out var id))
{
var authenticationTicket = await context.AuthenticationTickets.FindAsync(id);
if (authenticationTicket != null)
{
authenticationTicket.Value = SerializeToBytes(ticket);
authenticationTicket.LastActivity = DateTimeOffset.UtcNow;
authenticationTicket.Expires = ticket.Properties.ExpiresUtc;
await context.SaveChangesAsync();
}
}
}
}
public async Task<AuthenticationTicket> RetrieveAsync(string key)
{
using (var context = new ApplicationDbContext(_optionsBuilder.Options))
{
if (Guid.TryParse(key, out var id))
{
var authenticationTicket = await context.AuthenticationTickets.FindAsync(id);
if (authenticationTicket != null)
{
authenticationTicket.LastActivity = DateTimeOffset.UtcNow;
await context.SaveChangesAsync();
return DeserializeFromBytes(authenticationTicket.Value);
}
}
}
return null;
}
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var userId = string.Empty;
var nameIdentifier = ticket.Principal.GetNameIdentifier();
using (var context = new ApplicationDbContext(_optionsBuilder.Options))
{
if (ticket.AuthenticationScheme == "Identity.Application")
{
userId = nameIdentifier;
}
// If using a external login provider like google we need to resolve the userid through the Userlogins
else if (ticket.AuthenticationScheme == "Identity.External")
{
userId = (await context.UserLogins.SingleAsync(x => x.ProviderKey == nameIdentifier)).UserId;
}
var authenticationTicket = new Entities.AuthenticationTicket()
{
UserId = userId,
LastActivity = DateTimeOffset.UtcNow,
Value = SerializeToBytes(ticket),
};
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc.HasValue)
{
authenticationTicket.Expires = expiresUtc.Value;
}
await context.AuthenticationTickets.AddAsync(authenticationTicket);
await context.SaveChangesAsync();
return authenticationTicket.Id.ToString();
}
}
private byte[] SerializeToBytes(AuthenticationTicket source)
=> TicketSerializer.Default.Serialize(source);
private AuthenticationTicket DeserializeFromBytes(byte[] source)
=> source == null ? null : TicketSerializer.Default.Deserialize(source);
}
3. Configure application cookie
services.ConfigureApplicationCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(14);
options.SlidingExpiration = true;
options.SessionStore = new CustomTicketStore(optionsBuilder);
});
After this, you can set business logic that controls:
1. Session max count. We have to track activity and a lot of session from different IP addresses is very suspiciously.
2. Terminate session on logout. Actually, it depends on your token lifetime. But if it's more than 10 minutes, I think you should terminate a session from DB. And of course, you have to terminate a renewal token.
3. Terminate all tokens on password change. In this case, an attacker will lose account access immediately and a user will be safe.
Nowadays hackers' attacks could cost a .netdolot for companies. Using these simple rules you can control users' sessions and protect their personal data and activity.?