Books / Go - Secure Coding Practices / Chapter 4
Authentication and Password Management
OWASP Secure Coding Practices is a valuable document for programmers to help them to validate if all best practices were followed during project implementation. Authentication and Password Management are critical parts of any system and they are covered in detail from user signup, to credentials storage, password reset and private resources access.
Some guidelines may be grouped for more in-depth details. Plus, source code examples are provided to illustrate the topics.
Rules of Thumb
Let’s start with the rules of thumb: “all authentication controls must be enforced on a trusted system” which usually is the server where the application’s backend is running.
For the sake of system’s simplicity, and to reduce the points of failure, you should utilize standard and tested authentication services. Usually frameworks already have such a module and you’re encouraged to use them as they are developed, maintained, and used by many people behaving as a centralized authentication mechanism. Nevertheless, you should “inspect the code carefully to ensure it is not affected by any malicious code”, and be sure that it follows the best practices.
Resources which require authentication should not perform it themselves. Instead, “redirection to and from the centralized authentication control” should be used. Be careful handling redirection: you should redirect only to local and/or safe resources.
Authentication should not be used only by the application’s users, but also by your own application when it requires “connection to external systems that involve sensitive information or functions”. In these cases, “authentication credentials for accessing services external to the application should be encrypted and stored in a protected location on a trusted system (e.g., the server). The source code is NOT a secure location”.
Communicating authentication data
In this section, “communication” is used in a broader sense, encompassing User Experience (UX) and client-server communication.
Not only is it true that “password entry should be obscured on user’s screen”,
but also the “remember me functionality should be disabled”.
You can accomplish both by using an input field with type="password"
, and
setting the autocomplete
attribute to off
.
<input type="password" name="passwd" autocomplete="off" />
Authentication credentials should be sent only through encrypted connections (HTTPS). An exception to the encrypted connection may be the temporary passwords associated with email resets.
Remember that requested URLs are usually logged by the HTTP server
(access_log
), which include the query string. To prevent authentication
credentials leakage to HTTP server logs, data should be sent to the server using
the HTTP POST
method.
xxx.xxx.xxx.xxx - - [27/Feb/2017:01:55:09 +0000] "GET /?username=user&password=70pS3cure/oassw0rd HTTP/1.1" 200 235 "-" "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0"
A well-designed HTML form for authentication would look like:
<form method="post" action="https://somedomain.com/user/signin" autocomplete="off">
<input type="hidden" name="csrf" value="CSRF-TOKEN" />
<label>Username <input type="text" name="username" /></label>
<label>Password <input type="password" name="password" /></label>
<input type="submit" value="Submit" />
</form>
When handling authentication errors, your application should not disclose which part of the authentication data was incorrect. Instead of “Invalid username” or “Invalid password”, just use “Invalid username and/or password” interchangeably:
<form method="post" action="https://somedomain.com/user/signin" autocomplete="off">
<input type="hidden" name="csrf" value="CSRF-TOKEN" />
<div class="error">
<p>Invalid username and/or password</p>
</div>
<label>Username <input type="text" name="username" /></label>
<label>Password <input type="password" name="password" /></label>
<input type="submit" value="Submit" />
</form>
Using a generic message you do not disclose:
- Who is registered: “Invalid password” means that the username exists.
- How your system works: “Invalid password” may reveal how your application
works, first querying the database for the
username
and then comparing passwords in-memory.
An example of how to perform authentication data validation (and storage) is available at Validation and Storage section.
After a successful login, the user should be informed about the last successful
or unsuccessful access date/time so that he can detect and report suspicious
activity. Further information regarding logging can be found in the
Error Handling and Logging section of the document. Additionally, it is
also recommended to use a constant time comparison function while checking
passwords in order to prevent a timing attack. The latter consists of analyzing
the difference of time between multiple requests with different inputs. In this
case, a standard comparison of the form record == password
would return false
at the first character that does not match. The closer the submitted password
is, the longer the response time. By exploiting that, an attacker could guess
the password. Note that even if the record doesn’t exist, we always force the
execution of subtle.ConstantTimeCompare
with an empty value to compare it to
the user input.
Validation and storing authentication data
Validation
The key subject of this section is the “authentication data storage”, since more often than desirable, user account databases are leaked on the Internet. Of course, this is not guaranteed to happen. But in the case of such an event, collateral damages can be avoided if authentication data, especially passwords, are stored properly.
First, let’s be clear that “all authentication controls should fail securely”. We recommend you read all other “Authentication and Password Management” sections, since they cover recommendations about reporting back wrong authentication data and how to handle logging.
One other preliminary recommendation is as follow: for sequential authentication implementations (like Google does nowadays), validation should happen only on the completion of all data input, on a trusted system (e.g. the server).
Storing password securely: the theory
Now let’s discuss storing passwords.
You really don’t need to store passwords, since they are provided by the users (plaintext). But you will need to validate on each authentication whether users are providing the same token.
So, for security reasons, what you need is a “one way” function H
, so that for
every password p1
and p2
, p1
is different from p2
, H(p1)
is also
different from H(p2)
1.
Does this sound, or look like math?
Pay attention to this last requirement: H
should be such a function that
there’s no function H⁻¹
so that H⁻¹(H(p1))
is equal to p1
. This means
that there’s no way back to the original p1
, unless you try all possible
values of p
.
If H
is one-way only, what’s the real problem about account leakage?
Well, if you know all possible passwords, you can pre-compute their hashes and then run a rainbow table attack.
Certainly you were already told that passwords are hard to manage from user’s point of view, and that users are not only able to re-use passwords, but they also tend to use something that’s easy to remember, hence somehow guessable.
How can we avoid this?
The point is: if two different users provide the same password p1
, we should
store a different hashed value.
It may sound impossible, but the answer is salt
: a pseudo-random unique per
user password value which is prepended to p1
, so that the resulting hash is
computed as follows: H(salt + p1)
.
So each entry on a passwords store should keep the resulting hash, and the
salt
itself in plaintext: salt
is not required to remain private.
Last recommendations.
- Avoid using deprecated hashing algorithms (e.g. SHA-1, MD5, etc)
- Read the Pseudo-Random Generators section.
The following code-sample shows a basic example of how this works:
package main
import (
"crypto/rand"
"crypto/sha256"
"database/sql"
"context"
"fmt"
)
const saltSize = 32
func main() {
ctx := context.Background()
email := []byte("[email protected]")
password := []byte("47;u5:B(95m72;Xq")
// create random word
salt := make([]byte, saltSize)
_, err := rand.Read(salt)
if err != nil {
panic(err)
}
// let's create SHA256(salt+password)
hash := sha256.New()
hash.Write(salt)
hash.Write(password)
h := hash.Sum(nil)
// this is here just for demo purposes
//
// fmt.Printf("email : %s\n", string(email))
// fmt.Printf("password: %s\n", string(password))
// fmt.Printf("salt : %x\n", salt)
// fmt.Printf("hash : %x\n", h)
// you're supposed to have a database connection
stmt, err := db.PrepareContext(ctx, "INSERT INTO accounts SET hash=?, salt=?, email=?")
if err != nil {
panic(err)
}
result, err := stmt.ExecContext(ctx, h, salt, email)
if err != nil {
panic(err)
}
}
However, this approach has several flaws and should not be used. It is shown here only to illustrate the theory with a practical example. The next section explains how to correctly salt passwords in real life.
Storing password securely: the practice
One of the most important sayings in cryptography is: never roll your own crypto. By doing so, one can put the entire application at risk. It is a sensitive and complex topic. Hopefully, cryptography provides tools and standards reviewed and approved by experts. It is therefore important to use them instead of trying to re-invent the wheel.
In the case of password storage, the hashing algorithms recommended by
OWASP are bcrypt
, PDKDF2
, Argon2
and scrypt
.
Those take care of hashing and salting passwords in a robust way. Go authors
provide an extended package for cryptography, that is not part of the standard
library. It provides robust implementations for most of the aforementioned
algorithms. It can be downloaded using go get
:
go get golang.org/x/crypto
The following example shows how to use bcrypt, which should be good enough for most of the situations. The advantage of bcrypt is that it is simpler to use, and is therefore less error-prone.
package main
import (
"database/sql"
"context"
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
ctx := context.Background()
email := []byte("[email protected]")
password := []byte("47;u5:B(95m72;Xq")
// Hash the password with bcrypt
hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
panic(err)
}
// this is here just for demo purposes
//
// fmt.Printf("email : %s\n", string(email))
// fmt.Printf("password : %s\n", string(password))
// fmt.Printf("hashed password: %x\n", hashedPassword)
// you're supposed to have a database connection
stmt, err := db.PrepareContext(ctx, "INSERT INTO accounts SET hash=?, email=?")
if err != nil {
panic(err)
}
result, err := stmt.ExecContext(ctx, hashedPassword, email)
if err != nil {
panic(err)
}
}
Bcrypt also provides a simple and secure way to compare a plaintext password with an already hashed password:
ctx := context.Background()
// credentials to validate
email := []byte("[email protected]")
password := []byte("47;u5:B(95m72;Xq")
// fetch the hashed password corresponding to the provided email
record := db.QueryRowContext(ctx, "SELECT hash FROM accounts WHERE email = ? LIMIT 1", email)
var expectedPassword string
if err := record.Scan(&expectedPassword); err != nil {
// user does not exist
// this should be logged (see Error Handling and Logging) but execution
// should continue
}
if bcrypt.CompareHashAndPassword(password, []byte(expectedPassword)) != nil {
// passwords do not match
// passwords mismatch should be logged (see Error Handling and Logging)
// error should be returned so that a GENERIC message "Sign-in attempt has
// failed, please check your credentials" can be shown to the user.
}
If you’re not comfortable with password hashing and comparison options/parameters, better delegating the task to specialized third-party package with safe defaults. Always opt for an actively maintained package and remind to check for known issues.
- passwd - A Go package that provides a safe default abstraction for password hashing and comparison. It has support for original go bcrypt implementation, argon2, scrypt, parameters masking and key’ed (uncrackable) hashes.
Password Policies
Passwords are a historical asset, part of most authentication systems, and are the number one target of attackers.
Quite often some service leaks its users’ database, and despite the leak of email addresses and other personal data, the biggest concern are passwords. Why? Because passwords are not easy to manage and remember. Users not only tend to use weak passwords (e.g. “123456”) they can easily remember, they can also re-use the same password for different services.
If your application sign-in requires a password, the best you can do is to “enforce password complexity requirements, (…) requiring the use of alphabetic as well as numeric and/or special characters)”. Password length should also be enforced: “eight characters is commonly used, but 16 is better or consider the use of multi-word pass phrases”.
Of course, none of the previous guidelines will prevent users from re-using the same password. The best you can do to reduce this bad practice is to “enforce password changes”, and preventing password re-use. “Critical systems may require more frequent changes. The time between resets must be administratively controlled”.
Reset
Even if you’re not applying any extra password policy, users still need to be able to reset their password. Such a mechanism is as critical as signup or sign-in, and you’re encouraged to follow the best practices to be sure your system does not disclose sensitive data and become compromised.
“Passwords should be at least one day old before they can be changed”. This way you’ll prevent attacks on password re-use. Whenever using “email based resets, only send email to a pre-registered address with a temporary link/password” which should have a short expiration period.
Whenever a password reset is requested, the user should be notified. The same way, temporary passwords should be changed on the next usage.
A common practice for password reset is the “Security Question”, whose answer was previously configured by the account owner. “Password reset questions should support sufficiently random answers”: asking for “Favorite Book?” may lead to “The Bible” which makes this reset questions undesirable in most cases.
Other guidelines
Authentication is a critical part of any system, therefore you should always employ correct and safe practices. Below are some guidelines to make your authentication system more resilient:
- “Re-authenticate users prior to performing critical operations”
- “Use Multi-Factor Authentication for highly sensitive or high value transactional accounts”
- “Implement monitoring to identify attacks against multiple user accounts, utilizing the same password. This attack pattern is used to bypass standard lockouts, when user IDs can be harvested or guessed”
- “Change all vendor-supplied default passwords and user IDs or disable the associated accounts”
- “Enforce account disabling after an established number of invalid login attempts (e.g., five attempts is common). The account must be disabled for a period of time sufficient to discourage brute force guessing of credentials, but not so long as to allow for a denial-of-service attack to be performed”
-
Hashing functions are the subject of Collisions but recommended hashing functions have a really low collisions probability ↩