74 lines
2.2 KiB
Go
74 lines
2.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// BCrypt hashed passwords have a 72 character limit
|
|
const MaxPasswordLength = 72
|
|
|
|
// WeakPasswordError encodes an error that a password does not meet strength
|
|
// requirements. It is handled specially in errors.go as it gets transformed to
|
|
// a HTTPError with a special weak_password field that encodes the Reasons
|
|
// slice.
|
|
type WeakPasswordError struct {
|
|
Message string `json:"message,omitempty"`
|
|
Reasons []string `json:"reasons,omitempty"`
|
|
}
|
|
|
|
func (e *WeakPasswordError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
func (a *API) checkPasswordStrength(ctx context.Context, password string) error {
|
|
config := a.config
|
|
|
|
if len(password) > MaxPasswordLength {
|
|
return badRequestError(ErrorCodeValidationFailed, fmt.Sprintf("Password cannot be longer than %v characters", MaxPasswordLength))
|
|
}
|
|
|
|
var messages, reasons []string
|
|
|
|
if len(password) < config.Password.MinLength {
|
|
reasons = append(reasons, "length")
|
|
messages = append(messages, fmt.Sprintf("Password should be at least %d characters.", config.Password.MinLength))
|
|
}
|
|
|
|
for _, characterSet := range config.Password.RequiredCharacters {
|
|
if characterSet != "" && !strings.ContainsAny(password, characterSet) {
|
|
reasons = append(reasons, "characters")
|
|
|
|
messages = append(messages, fmt.Sprintf("Password should contain at least one character of each: %s.", strings.Join(config.Password.RequiredCharacters, ", ")))
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if config.Password.HIBP.Enabled {
|
|
pwned, err := a.hibpClient.Check(ctx, password)
|
|
if err != nil {
|
|
if config.Password.HIBP.FailClosed {
|
|
return internalServerError("Unable to perform password strength check with HaveIBeenPwned.org.").WithInternalError(err)
|
|
} else {
|
|
logrus.WithError(err).Warn("Unable to perform password strength check with HaveIBeenPwned.org, pwned passwords are being allowed")
|
|
}
|
|
} else if pwned {
|
|
reasons = append(reasons, "pwned")
|
|
messages = append(messages, "Password is known to be weak and easy to guess, please choose a different one.")
|
|
}
|
|
}
|
|
|
|
if len(reasons) > 0 {
|
|
return &WeakPasswordError{
|
|
Message: strings.Join(messages, " "),
|
|
Reasons: reasons,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|