-
Authentication
- Scenario
- As a sample application imagine an online shop.
- Some content areas can be viewed by any user, such as the products. It doesn’t matter who requests a page. The page (and the price) should look the same to all users.
- But some parts of the application should only be visible to certain users, e.g. a shopping cart or a personal preferences page. Each user has a different cart with different products as well as different preferences. Other users shouldn’t be able to access or modify these content areas.
- Such as shopping cart or user preferences should be persistent for longer than as long as this user interacts with the web page, e.g. when the user leaves the application and comes back several days later (with his or her computer turned off in the meantime) those preferences should still be the same as before.
- When accessing such a part of the application should be sure that this user is permitted to view the contents.
- This means that the user should authenticate.
- The online shop is just one example of an application that requires the user to log in. Many modern applications have private content areas. These applications require some form of authentication.
- User Authentication
- Is confirming the identity of a user.
- In the context of a web application it means confirming that a given request really comes from a particular user.
- There are various levels of authentication:
- 1 factor authentication: traditionally by using a user name and password
- Has 2 stages:
- first: registration
- subsequently: login
- Alternatives:
- Multi-factor authentication
- OAuth, an authorisation framework that can be used by users to grant access to their resources without having to share passwords. It works by using an authorisation token produced by an authorisation server.
-
Storage of User Details
- During registration the user provides a user name and a password.
- During registration the user provides a user name and a password.
- The details are persistently stored by the application, typically in a database.
- Subsequently these are retrieved to authenticate the user.
- Storing User Details - Threat Model
- The user database is accessed illegally and user names and passwords are retrieved by an unauthorized third party (the hacker).
- What hackers want: anything they can sell. In addition usernames with password attached have frequently appeared on pastebin.
- 7safe breach report (2010), an analysis of compromised data has identified the following main targets for stolen data:
- Payment data
- Sensitive company data
- Non-payment card data
- Intellectual property
- Therefore recording user names and passwords in a database should occur in such a way that makes it difficult for hackers to steal the data.
-
Password Storage
- Requirements:
- Store passwords in a form that protects the password even if the password file itself is compromised.
- When a user logs in we need to be able to verify that a user's password is correct.
- Mechanism:
Store the user name and the hashed password.
- Typical User Registration and Login Steps using Hashing
- The user creates an account.
- Their password is hashed and stored in the database. At no point is the plain-text (unencrypted) password ever written to the hard drive.
- When the user attempts to login, the hash of the password they entered is checked against the hash of their real password (retrieved from the database).
- When the user attempts to login, the hash of the password they entered is checked against the hash of their real password (retrieved from the database).
- Steps 3 and 4 repeat every time someone tries to login to their account.
-
Hash Algorithms
- Are one way functions that turn any amount of data into a fixed-length "fingerprint" (the hash) that cannot be reversed.
- If the input changes by even a tiny bit, the resulting hash is completely different.
IMPORTANT WARNING: if you are thinking of writing your own password hashing code, please don't!. It's too easy to screw up. No, that cryptography course you took in university doesn't make you exempt from this warning. This applies to everyone: DO NOT WRITE YOUR OWN CRYPTO! The problem of storing passwords has already been solved. Use either use either phppass, the PHP, C#, Java, and Ruby Implementation in defuse/password-hashing, or libsodium.- Example hash
- A one-way function that turns any amount of data into a fixed-length "fingerprint" (the hash) that cannot be reversed.
- If two users have the same password, they'll have the same password hashes.
- but you can't get the password back given the hash
- If the input changes by even a tiny bit, the resulting hash is completely different.
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 hash("hbllo") = 58756879c05c68dfac9866712fad6a93f8146f337a69aef7dd238f3364946366 hash("waltz") = c0e81794384491161f1777c232bc6bd9ec38f616560b120fda8e90f383853542
-
Possible Attack Methods - Guess the Password
- Even when the password is hashed hackers might still try and guess the password.
- Hash any given guess.
- Check if the guess's hash equals the hash being cracked.
- If the hashes are equal, the guess is the password.
- Forms:
- Brute force attacks...
- Any possible combination of a certain set of characters up to a certain length
- Dictionary attacks
Use a file of likely passwords (words, phrases, leet-speak, passwords, etc.)
- Possible Attack Methods - Lookup Tables
- Pre-compute the hashes of the passwords in a password dictionary and store them, and their corresponding password, in a lookup table data structure.
- Try CrackStation's 🔗 free hash cracker (https://crackstation.net/)
- Many "versions":
- Reverse lookup tables
- Creates a lookup table that maps each password hash from the compromised user account database to a list of users who had that hash. The attacker then hashes each password guess and uses the lookup table to get a list of users whose password was the attacker's guess.
- Effective because it is common for many users to have the same password.
- Rainbow tables
- Smaller than lookup tables,
- More solutions can be stored in the same amount of space.
-
Prevention: (Add a Little) Salt
- If two users have the same password, they would have the same password hash
- Randomize each hash, so that when the same password is hashed twice, the hashes are not the same.
- If we append or prepend a random string, called a salt, to the password before hashing...
- ... this makes the same password hash into a completely different string every time.
- To check if a password is correct, we need the salt, so it is usually stored in the user account database along with the hash, or as part of the hash string itself.
password + salt = hash
hash("hello") 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1 hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9eo79b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007
-
Storing the Salt
- To check if any password is correct, we need to re-compute the hash with the same salt.
- The salt is often stored in the user database with the hash, or as part of the hash string itself.
- The salt does not need to be secret. Just by randomizing the hashes, lookup tables, reverse lookup tables, and rainbow tables become ineffective.
- Don't use the same salt twice because if two users have the same password it will return the same (salted) hash.
- A new random salt must be generated each time a user creates an account or changes their password.
- Prevention
- The attackers won't know in advance what the salt will be, so they can't pre-compute a lookup table or rainbow table.
- If each user's password is hashed with a different salt, the reverse lookup table attack won't work either.
-
Salt Generation
- Salt should be generated using a Cryptographically Secure Pseudo-Random Number Generator (CSPRNG).
- These implementations provide a high level of randomness and are completely unpredictable.
- Example:
In Java: java.security.SecureRandom
- Prevention: Key Stretching
- This doesn't prevent hackers from running dictionary or brute-force attacks on each hash individually.
- High-end graphics cards (GPUs) and custom hardware can compute billions of hashes per second, so these attacks are still very effective.
- Key stretching makes the hash function very slow, so that even with a fast GPU or custom hardware, dictionary and brute-force attacks are too slow to be worthwhile.
- An argument value determines how slow the hash function will be.
- The denial of service threat can be eliminated by making the user solve a CAPTCHA every time they log in.
- Prevention: Keeping a Secret Key
- The next level of security is to keep the key secret.
- Only someone who knows the key can use the hash to validate a password.
- The hash can be encrypted using a cipher like AES, or the secret key can be included in the hash using a keyed hash algorithm like HMAC.
- In case of a breach an attacker gains full access to the system.
- In order to keep the key secret it has to be stored in an external system, such as a physically separate server dedicated to password validation, or a special hardware device attached to the server such as the YubiHSM.
- Practically hard to achieve.
-
Store the Password
1. Generate a long random salt using a CSPRNG.
2. Prepend the salt to the password and hash it with a standard password hashing function like Argon2, bcrypt, scrypt, or PBKDF2.
3. Save both the salt and the hash in the user's database record.
Validate the Password
1. Retrieve the user's salt and hash from the database.
2. Prepend the salt to the given password and hash it using the same hash function.
3. Compare the hash of the given password with the hash from the database. If they match, the password is correct. Otherwise, the password is incorrect.
-
Login Code Example
- Basic Login Functions
- An interface defines functionality that can be expected from implementing classes
- Register with user name and password for new users.
- Login with user name and password for returning users.
/* Simple as possible to avoid leaking information */ public interface IUserLogin { boolean login(String userName, String password); boolean register(String userName, String password); }
Implementation in Memory – Login
public synchronized boolean login(@NonNull String userName, @NonNull String password) { try { String storedHash = passwords.get(userName); return storedHash != null && PasswordHash.validatePassword(password, passwords.get(userName)); } catch (NoSuchAlgorithmException|InvalidKeySpecException e) { LOG.error("Can't validate password: " + e.getMessage()); return false; } }
Implementation in Memory – Registration
public synchronized boolean register(@NonNull String userName, @NonNull String password) { String current = passwords.get(userName); if (current != null) { return false; } try { String hash = PasswordHash.createHash(password); passwords.put(userName, hash); return true; } catch (Exception e) { LOG.error("Can't hash password <" + password + ">: " + e.getMessage()); return false; } }
NB: The following functions are from a text book. Due to the complexity of writing hashing algorithms they can be used for individual implementations.
This is an example only. You do not need to write such code.
Hash Generation – Overview of Methods and Constants
Hash Generation - The Class and Constants
/* * Hashes a password using a slow algorithm and a random salt to make * dictionary attacks as difficult as possible. */ class PasswordHash { private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; // The following constants may be changed without breaking existing hashes. private static final int SALT_BYTES = 24; private static final int HASH_BYTES = 24; private static final int PBKDF2_ITERATIONS = 1000; private static final int ITERATION_INDEX = 0; private static final int SALT_INDEX = 1; private static final int PBKDF2_INDEX = 2;
Creating a Password Hash
/** * Returns a salted PBKDF2 hash of the password. * * @param password the password to hash * @return a salted PBKDF2 hash of the password */ static String createHash(String password) throws NoSuchAlgorithmException, InvalidKeySpecException { return createHash(password.toCharArray()); } private static String createHash(char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException { // Generate a random salt SecureRandom random = new SecureRandom(); byte[] salt = new byte[SALT_BYTES]; random.nextBytes(salt); // Hash the password byte[] hash = pbkdf2(password, salt, PBKDF2_ITERATIONS,HASH_BYTES); // format iterations:salt:hash return PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash); }
Password Validation
/** * Validates a password using a hash. * * @param password the password to check * @param goodHash the hash of the valid password * @return true if the password is correct, false if not */ static boolean validatePassword(String password, String goodHash) throws NoSuchAlgorithmException, InvalidKeySpecException { return validatePassword(password.toCharArray(), goodHash); } private static boolean validatePassword(char[] password, String goodHash) throws NoSuchAlgorithmException, InvalidKeySpecException { // Decode the hash into its parameters String[] params = goodHash.split(":"); int iterations = Integer.parseInt(params[ITERATION_INDEX]); byte[] salt = fromHex(params[SALT_INDEX]); byte[] hash = fromHex(params[PBKDF2_INDEX]); // Compute the hash of the provided password, using the same salt, iteration count, and hash //length byte[] testHash = pbkdf2(password, salt, iterations, hash.length); // Compare the hashes in constant time. The password is correct if both hashes match. return slowEquals(hash, testHash); }
Compute the Hash
/** * Computes the PBKDF2 hash of a password. * * @param password the password to hash. * @param salt the salt * @param iterations the iteration count (slowness factor) * @param bytes the length of the hash to compute in bytes * @return the PBDKF2 hash of the password */ private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes) throws NoSuchAlgorithmException, InvalidKeySpecException { PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8); SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); return skf.generateSecret(spec).getEncoded(); }
Compare Hashes
/* * Compares two byte arrays in length-constant time. * This comparison method is used so that password hashes cannot be extracted from an on-line * system using a timing attack and then attacked off-line. */ private static boolean slowEquals(byte[] a, byte[] b) { int diff = a.length ^ b.length; for (int i = 0; i < a.length && i < b.length; i++) diff |= a[i] ^ b[i]; return diff == 0; }
Hex -> Byte Array
/** * Converts a string of hexadecimal characters into a byte array. * * @param hex the hex string * @return the hex string decoded into a byte array */ private static byte[] fromHex(String hex) { byte[] binary = new byte[hex.length() / 2]; for (int i = 0; i < binary.length; i++) { binary[i] = (byte) Integer.parseInt( hex.substring(2 * i, 2 * i + 2), 16); } return binary; }
And back to Byte Array
/** * Converts a byte array into a hexadecimal string. * * @param array the byte array to convert * @return a length*2 character string encoding the byte array */ private static String toHex(byte[] array) { BigInteger bi = new BigInteger(1, array); String hex = bi.toString(16); int paddingLength = (array.length * 2) - hex.length(); if (paddingLength > 0) { return String.format("%0" + paddingLength + "d", 0) + hex; } else { return hex; } }
- Calling the Login - Login Servlet
- Define a request URI that should produce the login screen.
- In Runner.java setup a (new) servlet to respond to that request.
- Define a login form with user name and password.
- In the new servlet, render that form in the doGet() method.
- Define the URI to which the user name and password are submitted (the method and action attributes of a form).
- Write a servlet to process the request from this URI.
- For parameter submission the http post method is better, so implement this method.
- Extract the user name and password from the request, hash the new key and compare with the existing hash in the database.
- If both hashes match, the user should be directed to the protected page. If not, the user should be re-directed to the login screen or a public page.
- Simple Forwarding of Processing on the Server
- If a method has been set up to process a request and produce a response but you want the actual processing to be done in a different method you can re-direct the response.
- In response method, e.g. doPost(...){...}
send
response.sendRedirect(response.encodeRedirectURL("/newURL")); - This creates another request for URL:
localhost:9000/newURL
Which will then be passed back to Runner.
Depending on which Servlet has been set up for the request for "/newURL " using ContextHandler, that servlet will provide the response.
-
Alternatives: OAuth
- OAuth is an authentication protocol that allows you to approve one application interacting with another on your behalf without giving away your password.
- Participants: the user, the consumer, and the service provider.
- Example
- Step 1 – The user shows intent
- Joe (User): “Hey, Bitly, I would like you to be able to post links directly to my Twitter stream.”
- Bitly (Consumer): “Great! Let me go ask for permission.”
- Step 2 – The consumer gets permission
- Bitly: “I have a user that would like me to post to his stream. Can I have a request token?”
- Twitter (Service Provider): “Sure. Here’s a token and a secret.”
- The secret is used to prevent request forgery. The consumer uses the secret to sign each request so that the service provider can verify it is actually coming from the consumer application.
- Step 3 – The user is redirected to the service provider
- Bitly: “OK, Joe. I’m sending you over to Twitter so you can approve. Take this token with you.”
- Joe: “OK!”
- <Bitly directs Joe to Twitter for authorization>
- Note: This is the scary part. If Bitly were super-shady Evil Co. it could pop up a window that looked like Twitter but was really phishing for your username and password. Always be sure to verify that the URL you’re directed to is actually the service provider (Twitter, in this case).
- Step 4 – The user gives permission
- Joe: “Twitter, I’d like to authorize this request token that Bitly gave me.”
- Twitter: “OK, just to be sure, you want to authorize Bitly to do X, Y, and Z with your Twitter account?”
- Joe: “Yes!”
- Twitter: “OK, you can go back to Bitly and tell them they have permission to use their request token.”
- Twitter marks the request token as “good-to-go,” so when the consumer requests access, it will be accepted (so long as it’s signed using their shared secret).
- Step 5 – The consumer obtains an access token
- Bitly: “Twitter, can I exchange this request token for an access token?”
- Twitter: “Sure. Here’s your access token and secret.”
- Step 6 – The consumer accesses the protected resource
- Bitly: “I’d like to post this link to Joe’s stream. Here’s my access token!”
- Twitter: “Done!”
Alternatives: Multiple Authentication Factors
Authenticate with:
- Example 2-factor Authentication
- Providing user name and password is an example of single-factor authentication (SFA).
- The user provides one factor, often a password.
- With 2 factor the user provides two authentication factors to verify they are who they say they are.
- Process
- The user logs in to a service with the first factor, e.g. a password.
- The service then issues another authentication via a different channel, e.g. SMS.
- The user provides that second authentication factor.
- Not completely secure: e.g. account recovery is a potential weakness.
XXX