1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package auth2021import (22 "context"23 "errors"24 "fmt"25 "net"2627 "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)3536var (37 ErrUnsupportedMech = errors.New("Unsupported SASL mechanism")38 ErrInvalidAuthCred = errors.New("auth: invalid credentials")39)4041// SASLAuth is a wrapper that initializes sasl.Server using authenticators that42// call maddy module objects.43//44// It also handles username translation using auth_map and auth_map_normalize45// (AuthMap and AuthMapNormalize should be set).46//47// It supports reporting of multiple authorization identities so multiple48// accounts can be associated with a single set of credentials.49type SASLAuth struct {50 Log log.Logger51 OnlyFirstID bool52 EnableLogin bool5354 AuthMap module.Table55 AuthNormalize authz.NormalizeFunc5657 Plain []module.PlainAuth58}5960func (s *SASLAuth) SASLMechanisms() []string {61 var mechs []string6263 if len(s.Plain) != 0 {64 mechs = append(mechs, sasl.Plain)65 if s.EnableLogin {66 mechs = append(mechs, sasl.Login)67 }68 }6970 return mechs71}7273func (s *SASLAuth) usernameForAuth(ctx context.Context, saslUsername string) (string, error) {74 if s.AuthNormalize != nil {75 var err error76 saslUsername, err = s.AuthNormalize(saslUsername)77 if err != nil {78 return "", err79 }80 }8182 if s.AuthMap == nil {83 return saslUsername, nil84 }8586 mapped, ok, err := s.AuthMap.Lookup(ctx, saslUsername)87 if err != nil {88 return "", err89 }90 if !ok {91 return "", ErrInvalidAuthCred92 }9394 if saslUsername != mapped {95 s.Log.DebugMsg("using mapped username for authentication", "username", saslUsername, "mapped_username", mapped)96 }9798 return mapped, nil99}100101func (s *SASLAuth) AuthPlain(username, password string) error {102 if len(s.Plain) == 0 {103 return ErrUnsupportedMech104 }105106 var lastErr error107 for _, p := range s.Plain {108 mappedUsername, err := s.usernameForAuth(context.TODO(), username)109 if err != nil {110 return err111 }112113 s.Log.DebugMsg("attempting authentication",114 "mapped_username", mappedUsername, "original_username", username,115 "module", p)116117 lastErr = p.AuthPlain(mappedUsername, password)118 if lastErr == nil {119 return nil120 }121 }122123 return fmt.Errorf("no auth. provider accepted creds, last err: %w", lastErr)124}125126type ContextData struct {127 // Authentication username. May be different from identity.128 Username string129130 // Password used for password-based mechanisms.131 Password string132}133134// 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 = username141 }142 if identity != username {143 return ErrInvalidAuthCred144 }145146 err := s.AuthPlain(username, password)147 if err != nil {148 s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)149 return ErrInvalidAuthCred150 }151152 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 }161162 return sasllogin.NewLoginServer(func(username, password string) error {163 username, err := s.usernameForAuth(context.Background(), username)164 if err != nil {165 return err166 }167168 err = s.AuthPlain(username, password)169 if err != nil {170 s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)171 return ErrInvalidAuthCred172 }173174 return successCb(username, ContextData{175 Username: username,176 Password: password,177 })178 })179 }180 return FailingSASLServ{Err: ErrUnsupportedMech}181}182183// AddProvider adds the SASL authentication provider to its mapping by parsing184// 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 err189 }190191 hasAny := false192 if plainAuth, ok := any.(module.PlainAuth); ok {193 s.Plain = append(s.Plain, plainAuth)194 hasAny = true195 }196197 if !hasAny {198 return config.NodeErr(node, "auth: specified module does not provide any SASL mechanism")199 }200 return nil201}202203type FailingSASLServ struct{ Err error }204205func (s FailingSASLServ) Next([]byte) ([]byte, bool, error) {206 return nil, true, s.Err207}