1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2025-12-17 14:37:02 +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.
Authentication requests sent by chasquid to dovecot will use the
fully-qualified user form, `user@domain`.
Authentication requests sent by chasquid to dovecot will pass on the username
as specified by the client. This will usually be either `user@domain`, or just
`user`.
## Configuring chasquid

View File

@@ -42,7 +42,7 @@ type Authenticator struct {
// Fallback backend, to use when backends[domain] (which may not exist)
// did not yield a positive result.
// Note that this backend gets the user with the domain included, of the
// form "user@domain".
// form "user@domain" (if available).
Fallback Backend
// 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 {
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)
return ok, err
}
@@ -113,7 +117,11 @@ func (a *Authenticator) Exists(user, domain string) (bool, error) {
}
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)
return ok, err
}
@@ -158,9 +166,11 @@ func (a *Authenticator) Reload() error {
//
// https://tools.ietf.org/html/rfc4954#section-4.1.
//
// Either both ID match, or one of them is empty.
// We expect the ID to be "user@domain", which is NOT an RFC requirement but
// our own.
// Either both IDs match, or one of them is empty.
//
// 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) {
buf, err := base64.StdEncoding.DecodeString(response)
if err != nil {
@@ -201,17 +211,14 @@ func DecodeResponse(response string) (user, domain, passwd string, err error) {
return
}
// Identity must be in the form "user@domain".
// This is NOT an RFC requirement, it's our own.
// Split identity into "user@domain", if possible.
user = identity
idsp := strings.SplitN(identity, "@", 2)
if len(idsp) != 2 {
err = fmt.Errorf("identity must be in the form user@domain")
return
if len(idsp) >= 2 {
user = idsp[0]
domain = idsp[1]
}
user = idsp[0]
domain = idsp[1]
// 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
// 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
{"AHVAZABwYXNz", "u", "d", "pass"}, // \0u@d\0pass
{"dUBkAABwYXNz/w==", "u", "d", "pass\xff"}, // u@d\0\0pass\xff
{"dQB1AHBhc3M=", "u", "", "pass"}, // u\0u\0pass
// "ñaca@ñeque\0\0clavaré"
{"w7FhY2FAw7FlcXVlAABjbGF2YXLDqQ==", "ñaca", "ñeque", "clavaré"},
@@ -42,7 +43,7 @@ func TestDecodeResponse(t *testing.T) {
failedCases := []string{
"", "\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",
}
for _, c := range failedCases {

View File

@@ -5,10 +5,11 @@ ssl = no
default_internal_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
# requested user.
auth_username_format = "%n-AT-%d"
# We drop the domain, to exercise "naked" auth handling.
auth_username_format = "%n-x"
passdb {
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
user user@srv
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 &
wait_until_ready 1025
# Send an email as user@srv successfully.
# Send an email as "user@srv" successfully.
run_msmtp user@srv < content
wait_for_file .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).
if run_msmtp nobody@srv < content 2> /dev/null; then
fail "successfuly sent an email to a non-existent user"