fix: login refuses a not-activated account while require_activation is on
The refusal is the same 401 invalid-login answer as a wrong password and is counted by the login throttle. The activation is checked after the password and before the deactivation restore, so a refused account stays deactivated. require_activation false keeps the previous behaviour.
This commit is contained in:
@@ -44,6 +44,17 @@ var errNoAvatar = errors.New("golem15.user: no avatar")
|
||||
const invalidLoginBody = "Invalid email or password"
|
||||
|
||||
// Login authenticates email and password and returns a JWT plus the user payload.
|
||||
//
|
||||
// It checks in this order: a ban or an active suspension
|
||||
// (classes.RejectIfThrottled), the password (bcrypt), the activation, then
|
||||
// restores a deactivated (deleted_at) account and sends it the reactivate
|
||||
// mail, records the successful attempt and mints the token. With
|
||||
// golem15.user.activation.require_activation on (the default) the correct
|
||||
// password of a not-activated account gets the same 401 invalid-login answer
|
||||
// as a wrong password and counts as a failed attempt for the throttle; an
|
||||
// account that is also deactivated is refused before the restore, so it
|
||||
// stays deactivated and gets no mail. With the key off such an account signs
|
||||
// in as before. Activation gates login, deactivation does not.
|
||||
func Login(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, secret, ok := sessionDeps(w, app)
|
||||
@@ -75,13 +86,14 @@ func Login(app *backpack.App) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
if user == nil || !bouncer.CheckPassword(user.Password, password) {
|
||||
if user != nil {
|
||||
if err := classes.CheckAndRecordLogin(ctx, gdb, user, ip, false); err != nil && !classes.IsLoginRejected(err) {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeInvalidLogin(w, r, app)
|
||||
refuseLogin(ctx, w, r, app, gdb, user, ip)
|
||||
return
|
||||
}
|
||||
// The activation gate runs after the password check, so a wrong
|
||||
// password and a not-activated account cost the same and answer the
|
||||
// same, and before the restore, so a refused account is not revived.
|
||||
if _, _, _, requireActivation := registrationSettings(app); requireActivation && !user.IsActivated {
|
||||
refuseLogin(ctx, w, r, app, gdb, user, ip)
|
||||
return
|
||||
}
|
||||
if user.DeletedAt.Valid {
|
||||
@@ -1174,6 +1186,18 @@ func writeInvalidLogin(w http.ResponseWriter, r *http.Request, app *backpack.App
|
||||
})
|
||||
}
|
||||
|
||||
// refuseLogin is the one refusal /login gives after the throttle check: it
|
||||
// records a failed attempt for user (a nil user, an unknown email, records
|
||||
// nothing) and answers the 401 invalid-login body. The wrong-password and
|
||||
// the not-activated branches share it, so their answers cannot be told apart.
|
||||
func refuseLogin(ctx context.Context, w http.ResponseWriter, r *http.Request, app *backpack.App, gdb *gorm.DB, user *models.User, ip string) {
|
||||
if err := classes.CheckAndRecordLogin(ctx, gdb, user, ip, false); err != nil && !classes.IsLoginRejected(err) {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
writeInvalidLogin(w, r, app)
|
||||
}
|
||||
|
||||
func writeUnauthorized(w http.ResponseWriter) {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": true, "message": "Unauthorized"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user