1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-18 14:47:03 +00:00

auth: Allow users without a domain

Some deployments already have users that authenticate without a domain.
Today, we refuse to even consider those, and reject them at parsing time.

However, it is a use-case worth supporting, at least with some
restrictions that make the complexity manageable.

This patch changes the auth package to support authenticating users
without an "@domain" part.

Those requests will always be directly passed on to the fallback
authenticator, if available.

The dovecot fallback authenticator can already handle this case just fine.
This commit is contained in:
Alberto Bertogli
2021-06-11 20:05:41 +01:00
parent 099e2e2269
commit cfe0e48c0a
7 changed files with 48 additions and 21 deletions

View File

@@ -31,8 +31,9 @@ accordingly.
This lets chasquid issue authentication requests to dovecot. This lets chasquid issue authentication requests to dovecot.
Authentication requests sent by chasquid to dovecot will use the Authentication requests sent by chasquid to dovecot will pass on the username
fully-qualified user form, `user@domain`. as specified by the client. This will usually be either `user@domain`, or just
`user`.
## Configuring chasquid ## Configuring chasquid

View File

@@ -42,7 +42,7 @@ type Authenticator struct {
// Fallback backend, to use when backends[domain] (which may not exist) // Fallback backend, to use when backends[domain] (which may not exist)
// did not yield a positive result. // did not yield a positive result.
// Note that this backend gets the user with the domain included, of the // Note that this backend gets the user with the domain included, of the
// form "user@domain". // form "user@domain" (if available).
Fallback Backend Fallback Backend
// How long Authenticate calls should last, approximately. // How long Authenticate calls should last, approximately.
@@ -90,7 +90,11 @@ func (a *Authenticator) Authenticate(user, domain, password string) (bool, error
} }
if a.Fallback != nil { if a.Fallback != nil {
ok, err := a.Fallback.Authenticate(user+"@"+domain, password) id := user
if domain != "" {
id = user + "@" + domain
}
ok, err := a.Fallback.Authenticate(id, password)
tr.Debugf("Fallback: %v %v", ok, err) tr.Debugf("Fallback: %v %v", ok, err)
return ok, err return ok, err
} }
@@ -113,7 +117,11 @@ func (a *Authenticator) Exists(user, domain string) (bool, error) {
} }
if a.Fallback != nil { if a.Fallback != nil {
ok, err := a.Fallback.Exists(user + "@" + domain) id := user
if domain != "" {
id = user + "@" + domain
}
ok, err := a.Fallback.Exists(id)
tr.Debugf("Fallback: %v %v", ok, err) tr.Debugf("Fallback: %v %v", ok, err)
return ok, err return ok, err
} }
@@ -158,9 +166,11 @@ func (a *Authenticator) Reload() error {
// //
// https://tools.ietf.org/html/rfc4954#section-4.1. // https://tools.ietf.org/html/rfc4954#section-4.1.
// //
// Either both ID match, or one of them is empty. // Either both IDs match, or one of them is empty.
// We expect the ID to be "user@domain", which is NOT an RFC requirement but //
// our own. // We split the id into user@domain, since in most cases we expect that to be
// the used form, and normalize them. If there is no domain, we just return
// "" for it. The rest of the stack will know how to handle it.
func DecodeResponse(response string) (user, domain, passwd string, err error) { func DecodeResponse(response string) (user, domain, passwd string, err error) {
buf, err := base64.StdEncoding.DecodeString(response) buf, err := base64.StdEncoding.DecodeString(response)
if err != nil { if err != nil {
@@ -201,17 +211,14 @@ func DecodeResponse(response string) (user, domain, passwd string, err error) {
return return
} }
// Identity must be in the form "user@domain". // Split identity into "user@domain", if possible.
// This is NOT an RFC requirement, it's our own. user = identity
idsp := strings.SplitN(identity, "@", 2) idsp := strings.SplitN(identity, "@", 2)
if len(idsp) != 2 { if len(idsp) >= 2 {
err = fmt.Errorf("identity must be in the form user@domain") user = idsp[0]
return domain = idsp[1]
} }
user = idsp[0]
domain = idsp[1]
// Normalize the user and domain. This is so users can write the username // Normalize the user and domain. This is so users can write the username
// in their own style and still can log in. For the domain, we use IDNA // in their own style and still can log in. For the domain, we use IDNA
// and relevant transformations to turn it to utf8 which is what we use // and relevant transformations to turn it to utf8 which is what we use

View File

@@ -19,6 +19,7 @@ func TestDecodeResponse(t *testing.T) {
{"dUBkAABwYXNz", "u", "d", "pass"}, // u@d\0\0pass {"dUBkAABwYXNz", "u", "d", "pass"}, // u@d\0\0pass
{"AHVAZABwYXNz", "u", "d", "pass"}, // \0u@d\0pass {"AHVAZABwYXNz", "u", "d", "pass"}, // \0u@d\0pass
{"dUBkAABwYXNz/w==", "u", "d", "pass\xff"}, // u@d\0\0pass\xff {"dUBkAABwYXNz/w==", "u", "d", "pass\xff"}, // u@d\0\0pass\xff
{"dQB1AHBhc3M=", "u", "", "pass"}, // u\0u\0pass
// "ñaca@ñeque\0\0clavaré" // "ñaca@ñeque\0\0clavaré"
{"w7FhY2FAw7FlcXVlAABjbGF2YXLDqQ==", "ñaca", "ñeque", "clavaré"}, {"w7FhY2FAw7FlcXVlAABjbGF2YXLDqQ==", "ñaca", "ñeque", "clavaré"},
@@ -42,7 +43,7 @@ func TestDecodeResponse(t *testing.T) {
failedCases := []string{ failedCases := []string{
"", "\x00", "\x00\x00", "\x00\x00\x00", "\x00\x00\x00\x00", "", "\x00", "\x00\x00", "\x00\x00\x00", "\x00\x00\x00\x00",
"a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass", "a\x00a\x00pass", "a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass",
"\xffa@b\x00\xffa@b\x00pass", "\xffa@b\x00\xffa@b\x00pass",
} }
for _, c := range failedCases { for _, c := range failedCases {

View File

@@ -5,10 +5,11 @@ ssl = no
default_internal_user = $USER default_internal_user = $USER
default_login_user = $USER default_login_user = $USER
# Before auth checks, rename "u@d" to "u-AT-d". This exercises that chasquid # Before auth checks, rename "u@d" to "u-x". This exercises that chasquid
# handles well the case where the returned user information does not match the # handles well the case where the returned user information does not match the
# requested user. # requested user.
auth_username_format = "%n-AT-%d" # We drop the domain, to exercise "naked" auth handling.
auth_username_format = "%n-x"
passdb { passdb {
driver = passwd-file driver = passwd-file

View File

@@ -1 +1,2 @@
user-AT-srv:{plain}password:1000:1000::/home/user user-x:{plain}password:1000:1000::/home/user
naked-x:{plain}gun:1001:1001::/home/naked

View File

@@ -26,3 +26,8 @@ password secretpassword
account badpasswd : default account badpasswd : default
user user@srv user user@srv
password badsecretpassword password badsecretpassword
account naked : default
from naked@srv
user naked
password gun

View File

@@ -51,11 +51,22 @@ mkdir -p .logs
chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config & chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config &
wait_until_ready 1025 wait_until_ready 1025
# Send an email as user@srv successfully. # Send an email as "user@srv" successfully.
run_msmtp user@srv < content run_msmtp user@srv < content
wait_for_file .mail/user@srv wait_for_file .mail/user@srv
mail_diff content .mail/user@srv mail_diff content .mail/user@srv
# Send an email as "naked" successfully.
rm .mail/user@srv
run_msmtp -a naked user@srv < content
wait_for_file .mail/user@srv
mail_diff content .mail/user@srv
# Send an email to the "naked" user successfully.
run_msmtp naked@srv < content
wait_for_file .mail/naked@srv
mail_diff content .mail/naked@srv
# Fail to send to nobody@srv (user does not exist). # Fail to send to nobody@srv (user does not exist).
if run_msmtp nobody@srv < content 2> /dev/null; then if run_msmtp nobody@srv < content 2> /dev/null; then
fail "successfuly sent an email to a non-existent user" fail "successfuly sent an email to a non-existent user"