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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user