maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package auth
 20
 21import (
 22	"context"
 23	"errors"
 24	"fmt"
 25	"net"
 26
 27	"github.com/emersion/go-sasl"
 28	"github.com/foxcpp/maddy/framework/config"
 29	modconfig "github.com/foxcpp/maddy/framework/config/module"
 30	"github.com/foxcpp/maddy/framework/log"
 31	"github.com/foxcpp/maddy/framework/module"
 32	"github.com/foxcpp/maddy/internal/auth/sasllogin"
 33	"github.com/foxcpp/maddy/internal/authz"
 34)
 35
 36var (
 37	ErrUnsupportedMech = errors.New("Unsupported SASL mechanism")
 38	ErrInvalidAuthCred = errors.New("auth: invalid credentials")
 39)
 40
 41// SASLAuth is a wrapper that initializes sasl.Server using authenticators that
 42// call maddy module objects.
 43//
 44// It also handles username translation using auth_map and auth_map_normalize
 45// (AuthMap and AuthMapNormalize should be set).
 46//
 47// It supports reporting of multiple authorization identities so multiple
 48// accounts can be associated with a single set of credentials.
 49type SASLAuth struct {
 50	Log         log.Logger
 51	OnlyFirstID bool
 52	EnableLogin bool
 53
 54	AuthMap       module.Table
 55	AuthNormalize authz.NormalizeFunc
 56
 57	Plain []module.PlainAuth
 58}
 59
 60func (s *SASLAuth) SASLMechanisms() []string {
 61	var mechs []string
 62
 63	if len(s.Plain) != 0 {
 64		mechs = append(mechs, sasl.Plain)
 65		if s.EnableLogin {
 66			mechs = append(mechs, sasl.Login)
 67		}
 68	}
 69
 70	return mechs
 71}
 72
 73func (s *SASLAuth) usernameForAuth(ctx context.Context, saslUsername string) (string, error) {
 74	if s.AuthNormalize != nil {
 75		var err error
 76		saslUsername, err = s.AuthNormalize(saslUsername)
 77		if err != nil {
 78			return "", err
 79		}
 80	}
 81
 82	if s.AuthMap == nil {
 83		return saslUsername, nil
 84	}
 85
 86	mapped, ok, err := s.AuthMap.Lookup(ctx, saslUsername)
 87	if err != nil {
 88		return "", err
 89	}
 90	if !ok {
 91		return "", ErrInvalidAuthCred
 92	}
 93
 94	if saslUsername != mapped {
 95		s.Log.DebugMsg("using mapped username for authentication", "username", saslUsername, "mapped_username", mapped)
 96	}
 97
 98	return mapped, nil
 99}
100
101func (s *SASLAuth) AuthPlain(username, password string) error {
102	if len(s.Plain) == 0 {
103		return ErrUnsupportedMech
104	}
105
106	var lastErr error
107	for _, p := range s.Plain {
108		mappedUsername, err := s.usernameForAuth(context.TODO(), username)
109		if err != nil {
110			return err
111		}
112
113		s.Log.DebugMsg("attempting authentication",
114			"mapped_username", mappedUsername, "original_username", username,
115			"module", p)
116
117		lastErr = p.AuthPlain(mappedUsername, password)
118		if lastErr == nil {
119			return nil
120		}
121	}
122
123	return fmt.Errorf("no auth. provider accepted creds, last err: %w", lastErr)
124}
125
126type ContextData struct {
127	// Authentication username. May be different from identity.
128	Username string
129
130	// Password used for password-based mechanisms.
131	Password string
132}
133
134// CreateSASL creates the sasl.Server instance for the corresponding mechanism.
135func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func(identity string, data ContextData) error) sasl.Server {
136	switch mech {
137	case sasl.Plain:
138		return sasl.NewPlainServer(func(identity, username, password string) error {
139			if identity == "" {
140				identity = username
141			}
142			if identity != username {
143				return ErrInvalidAuthCred
144			}
145
146			err := s.AuthPlain(username, password)
147			if err != nil {
148				s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)
149				return ErrInvalidAuthCred
150			}
151
152			return successCb(identity, ContextData{
153				Username: username,
154				Password: password,
155			})
156		})
157	case sasl.Login:
158		if !s.EnableLogin {
159			return FailingSASLServ{Err: ErrUnsupportedMech}
160		}
161
162		return sasllogin.NewLoginServer(func(username, password string) error {
163			username, err := s.usernameForAuth(context.Background(), username)
164			if err != nil {
165				return err
166			}
167
168			err = s.AuthPlain(username, password)
169			if err != nil {
170				s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)
171				return ErrInvalidAuthCred
172			}
173
174			return successCb(username, ContextData{
175				Username: username,
176				Password: password,
177			})
178		})
179	}
180	return FailingSASLServ{Err: ErrUnsupportedMech}
181}
182
183// AddProvider adds the SASL authentication provider to its mapping by parsing
184// the 'auth' configuration directive.
185func (s *SASLAuth) AddProvider(m *config.Map, node config.Node) error {
186	var any interface{}
187	if err := modconfig.ModuleFromNode("auth", node.Args, node, m.Globals, &any); err != nil {
188		return err
189	}
190
191	hasAny := false
192	if plainAuth, ok := any.(module.PlainAuth); ok {
193		s.Plain = append(s.Plain, plainAuth)
194		hasAny = true
195	}
196
197	if !hasAny {
198		return config.NodeErr(node, "auth: specified module does not provide any SASL mechanism")
199	}
200	return nil
201}
202
203type FailingSASLServ struct{ Err error }
204
205func (s FailingSASLServ) Next([]byte) ([]byte, bool, error) {
206	return nil, true, s.Err
207}