Index: trunk/downstream.go
===================================================================
--- trunk/downstream.go	(revision 723)
+++ trunk/downstream.go	(revision 724)
@@ -245,4 +245,9 @@
 }
 
+type downstreamSASL struct {
+	server                       sasl.Server
+	plainUsername, plainPassword string
+}
+
 type downstreamConn struct {
 	conn
@@ -268,10 +273,9 @@
 	supportedCaps   map[string]string
 	caps            map[string]bool
+	sasl            *downstreamSASL
 
 	lastBatchRef uint64
 
 	monitored casemapMap
-
-	saslServer sasl.Server
 }
 
@@ -687,100 +691,26 @@
 		}
 	case "AUTHENTICATE":
-		if !dc.caps["sasl"] {
-			return ircError{&irc.Message{
+		credentials, err := dc.handleAuthenticateCommand(msg)
+		if err != nil {
+			return err
+		} else if credentials == nil {
+			break
+		}
+
+		if err := dc.authenticate(ctx, credentials.plainUsername, credentials.plainPassword); err != nil {
+			dc.logger.Printf("SASL authentication error: %v", err)
+			dc.endSASL(&irc.Message{
 				Prefix:  dc.srv.prefix(),
 				Command: irc.ERR_SASLFAIL,
-				Params:  []string{"*", "AUTHENTICATE requires the \"sasl\" capability to be enabled"},
-			}}
-		}
-		if len(msg.Params) == 0 {
-			return ircError{&irc.Message{
-				Prefix:  dc.srv.prefix(),
-				Command: irc.ERR_SASLFAIL,
-				Params:  []string{"*", "Missing AUTHENTICATE argument"},
-			}}
-		}
-
-		var resp []byte
-		if msg.Params[0] == "*" {
-			dc.saslServer = nil
-			return ircError{&irc.Message{
-				Prefix:  dc.srv.prefix(),
-				Command: irc.ERR_SASLABORTED,
-				Params:  []string{"*", "SASL authentication aborted"},
-			}}
-		} else if dc.saslServer == nil {
-			mech := strings.ToUpper(msg.Params[0])
-			switch mech {
-			case "PLAIN":
-				dc.saslServer = sasl.NewPlainServer(sasl.PlainAuthenticator(func(identity, username, password string) error {
-					// TODO: we can't use the command context here, because it
-					// gets cancelled once the command handler returns. SASL
-					// might take multiple AUTHENTICATE commands to complete.
-					return dc.authenticate(context.TODO(), username, password)
-				}))
-			default:
-				return ircError{&irc.Message{
-					Prefix:  dc.srv.prefix(),
-					Command: irc.ERR_SASLFAIL,
-					Params:  []string{"*", fmt.Sprintf("Unsupported SASL mechanism %q", mech)},
-				}}
-			}
-		} else if msg.Params[0] == "+" {
-			resp = nil
-		} else {
-			// TODO: multi-line messages
-			var err error
-			resp, err = base64.StdEncoding.DecodeString(msg.Params[0])
-			if err != nil {
-				dc.saslServer = nil
-				return ircError{&irc.Message{
-					Prefix:  dc.srv.prefix(),
-					Command: irc.ERR_SASLFAIL,
-					Params:  []string{"*", "Invalid base64-encoded response"},
-				}}
-			}
-		}
-
-		challenge, done, err := dc.saslServer.Next(resp)
-		if err != nil {
-			dc.saslServer = nil
-			if ircErr, ok := err.(ircError); ok && ircErr.Message.Command == irc.ERR_PASSWDMISMATCH {
-				return ircError{&irc.Message{
-					Prefix:  dc.srv.prefix(),
-					Command: irc.ERR_SASLFAIL,
-					Params:  []string{"*", ircErr.Message.Params[1]},
-				}}
-			}
-			dc.SendMessage(&irc.Message{
-				Prefix:  dc.srv.prefix(),
-				Command: irc.ERR_SASLFAIL,
-				Params:  []string{"*", "SASL error"},
-			})
-			return fmt.Errorf("SASL authentication failed: %v", err)
-		} else if done {
-			dc.saslServer = nil
-			// Technically we should send RPL_LOGGEDIN here. However we use
-			// RPL_LOGGEDIN to mirror the upstream connection status. Let's see
-			// how many clients that breaks. See:
-			// https://github.com/ircv3/ircv3-specifications/pull/476
-			dc.SendMessage(&irc.Message{
-				Prefix:  dc.srv.prefix(),
-				Command: irc.RPL_SASLSUCCESS,
-				Params:  []string{dc.nick, "SASL authentication successful"},
-			})
-		} else {
-			challengeStr := "+"
-			if len(challenge) > 0 {
-				challengeStr = base64.StdEncoding.EncodeToString(challenge)
-			}
-
-			// TODO: multi-line messages
-			dc.SendMessage(&irc.Message{
-				Prefix:  dc.srv.prefix(),
-				Command: "AUTHENTICATE",
-				Params:  []string{challengeStr},
-			})
-		}
+				Params:  []string{"Authentication failed"},
+			})
+			break
+		}
+
+		// Technically we should send RPL_LOGGEDIN here. However we use
+		// RPL_LOGGEDIN to mirror the upstream connection status. Let's
+		// see how many clients that breaks. See:
+		// https://github.com/ircv3/ircv3-specifications/pull/476
+		dc.endSASL(nil)
 	case "BOUNCER":
 		var subcommand string
@@ -952,4 +882,105 @@
 }
 
+func (dc *downstreamConn) handleAuthenticateCommand(msg *irc.Message) (result *downstreamSASL, err error) {
+	defer func() {
+		if err != nil {
+			dc.sasl = nil
+		}
+	}()
+
+	if !dc.caps["sasl"] {
+		return nil, ircError{&irc.Message{
+			Prefix:  dc.srv.prefix(),
+			Command: irc.ERR_SASLFAIL,
+			Params:  []string{"*", "AUTHENTICATE requires the \"sasl\" capability to be enabled"},
+		}}
+	}
+	if len(msg.Params) == 0 {
+		return nil, ircError{&irc.Message{
+			Prefix:  dc.srv.prefix(),
+			Command: irc.ERR_SASLFAIL,
+			Params:  []string{"*", "Missing AUTHENTICATE argument"},
+		}}
+	}
+	if msg.Params[0] == "*" {
+		return nil, ircError{&irc.Message{
+			Prefix:  dc.srv.prefix(),
+			Command: irc.ERR_SASLABORTED,
+			Params:  []string{"*", "SASL authentication aborted"},
+		}}
+	}
+
+	var resp []byte
+	if dc.sasl == nil {
+		mech := strings.ToUpper(msg.Params[0])
+		var server sasl.Server
+		switch mech {
+		case "PLAIN":
+			server = sasl.NewPlainServer(sasl.PlainAuthenticator(func(identity, username, password string) error {
+				dc.sasl.plainUsername = username
+				dc.sasl.plainPassword = password
+				return nil
+			}))
+		default:
+			return nil, ircError{&irc.Message{
+				Prefix:  dc.srv.prefix(),
+				Command: irc.ERR_SASLFAIL,
+				Params:  []string{"*", fmt.Sprintf("Unsupported SASL mechanism %q", mech)},
+			}}
+		}
+
+		dc.sasl = &downstreamSASL{server: server}
+	} else {
+		// TODO: multi-line messages
+		if msg.Params[0] == "+" {
+			resp = nil
+		} else if resp, err = base64.StdEncoding.DecodeString(msg.Params[0]); err != nil {
+			return nil, ircError{&irc.Message{
+				Prefix:  dc.srv.prefix(),
+				Command: irc.ERR_SASLFAIL,
+				Params:  []string{"*", "Invalid base64-encoded response"},
+			}}
+		}
+	}
+
+	challenge, done, err := dc.sasl.server.Next(resp)
+	if err != nil {
+		return nil, err
+	} else if done {
+		return dc.sasl, nil
+	} else {
+		challengeStr := "+"
+		if len(challenge) > 0 {
+			challengeStr = base64.StdEncoding.EncodeToString(challenge)
+		}
+
+		// TODO: multi-line messages
+		dc.SendMessage(&irc.Message{
+			Prefix:  dc.srv.prefix(),
+			Command: "AUTHENTICATE",
+			Params:  []string{challengeStr},
+		})
+		return nil, nil
+	}
+}
+
+func (dc *downstreamConn) endSASL(msg *irc.Message) {
+	if dc.sasl == nil {
+		return
+	}
+
+	dc.sasl = nil
+
+	if msg != nil {
+		dc.SendMessage(msg)
+	} else {
+		dc.SendMessage(&irc.Message{
+			Prefix:  dc.srv.prefix(),
+			Command: irc.RPL_SASLSUCCESS,
+			Params:  []string{dc.nick, "SASL authentication successful"},
+		})
+	}
+}
+
 func (dc *downstreamConn) setSupportedCap(name, value string) {
 	prevValue, hasPrev := dc.supportedCaps[name]
@@ -1142,7 +1173,6 @@
 	}
 
-	if dc.saslServer != nil {
-		dc.saslServer = nil
-		dc.SendMessage(&irc.Message{
+	if dc.sasl != nil {
+		dc.endSASL(&irc.Message{
 			Prefix:  dc.srv.prefix(),
 			Command: irc.ERR_SASLABORTED,
@@ -2331,4 +2361,38 @@
 			Params:  []string{upstreamUser, upstreamChannel},
 		})
+	case "AUTHENTICATE":
+		// Post-connection-registration AUTHENTICATE is unsupported in
+		// multi-upstream mode, or if the upstream doesn't support SASL
+		uc := dc.upstream()
+		if uc == nil || !uc.caps["sasl"] {
+			return ircError{&irc.Message{
+				Prefix:  dc.srv.prefix(),
+				Command: irc.ERR_SASLFAIL,
+				Params:  []string{dc.nick, "Upstream network authentication not supported"},
+			}}
+		}
+
+		credentials, err := dc.handleAuthenticateCommand(msg)
+		if err != nil {
+			return err
+		}
+
+		if credentials != nil {
+			if uc.saslClient != nil {
+				dc.endSASL(&irc.Message{
+					Prefix:  dc.srv.prefix(),
+					Command: irc.ERR_SASLFAIL,
+					Params:  []string{dc.nick, "Another authentication attempt is already in progress"},
+				})
+				return nil
+			}
+
+			uc.logger.Printf("starting post-registration SASL PLAIN authentication with username %q", credentials.plainUsername)
+			uc.saslClient = sasl.NewPlainClient("", credentials.plainUsername, credentials.plainPassword)
+			uc.enqueueCommand(dc, &irc.Message{
+				Command: "AUTHENTICATE",
+				Params:  []string{"PLAIN"},
+			})
+		}
 	case "MONITOR":
 		// MONITOR is unsupported in multi-upstream mode
@@ -2701,21 +2765,6 @@
 func (dc *downstreamConn) handleNickServPRIVMSG(ctx context.Context, uc *upstreamConn, text string) {
 	username, password, ok := parseNickServCredentials(text, uc.nick)
-	if !ok {
-		return
-	}
-
-	// User may have e.g. EXTERNAL mechanism configured. We do not want to
-	// automatically erase the key pair or any other credentials.
-	if uc.network.SASL.Mechanism != "" && uc.network.SASL.Mechanism != "PLAIN" {
-		return
-	}
-
-	dc.logger.Printf("auto-saving NickServ credentials with username %q", username)
-	n := uc.network
-	n.SASL.Mechanism = "PLAIN"
-	n.SASL.Plain.Username = username
-	n.SASL.Plain.Password = password
-	if err := dc.srv.db.StoreNetwork(ctx, dc.user.ID, &n.Network); err != nil {
-		dc.logger.Printf("failed to save NickServ credentials: %v", err)
+	if ok {
+		uc.network.autoSaveSASLPlain(ctx, username, password)
 	}
 }
Index: trunk/upstream.go
===================================================================
--- trunk/upstream.go	(revision 723)
+++ trunk/upstream.go	(revision 724)
@@ -32,4 +32,5 @@
 	"message-tags":     true,
 	"multi-prefix":     true,
+	"sasl":             true,
 	"server-time":      true,
 	"setname":          true,
@@ -294,4 +295,10 @@
 					Params:  []string{dc.nick, mask, "End of /WHO"},
 				})
+			case "AUTHENTICATE":
+				dc.endSASL(&irc.Message{
+					Prefix:  dc.srv.prefix(),
+					Command: irc.ERR_SASLABORTED,
+					Params:  []string{dc.nick, "SASL authentication aborted"},
+				})
 			default:
 				panic(fmt.Errorf("Unsupported pending command %q", pendingCmd.msg.Command))
@@ -312,5 +319,5 @@
 func (uc *upstreamConn) enqueueCommand(dc *downstreamConn, msg *irc.Message) {
 	switch msg.Command {
-	case "LIST", "WHO":
+	case "LIST", "WHO", "AUTHENTICATE":
 		// Supported
 	default:
@@ -613,8 +620,18 @@
 		uc.saslStarted = false
 
-		uc.SendMessage(&irc.Message{
-			Command: "CAP",
-			Params:  []string{"END"},
-		})
+		if dc, _ := uc.dequeueCommand("AUTHENTICATE"); dc != nil && dc.sasl != nil {
+			if msg.Command == irc.RPL_SASLSUCCESS {
+				uc.network.autoSaveSASLPlain(context.TODO(), dc.sasl.plainUsername, dc.sasl.plainPassword)
+			}
+
+			dc.endSASL(msg)
+		}
+
+		if !uc.registered {
+			uc.SendMessage(&irc.Message{
+				Command: "CAP",
+				Params:  []string{"END"},
+			})
+		}
 	case irc.RPL_WELCOME:
 		uc.registered = true
@@ -1705,8 +1722,4 @@
 	}
 
-	if uc.requestSASL() && !uc.caps["sasl"] {
-		requestCaps = append(requestCaps, "sasl")
-	}
-
 	if len(requestCaps) == 0 {
 		return
@@ -1750,4 +1763,7 @@
 	switch name {
 	case "sasl":
+		if !uc.requestSASL() {
+			return nil
+		}
 		if !ok {
 			uc.logger.Printf("server refused to acknowledge the SASL capability")
Index: trunk/user.go
===================================================================
--- trunk/user.go	(revision 723)
+++ trunk/user.go	(revision 724)
@@ -405,4 +405,20 @@
 }
 
+func (net *network) autoSaveSASLPlain(ctx context.Context, username, password string) {
+	// User may have e.g. EXTERNAL mechanism configured. We do not want to
+	// automatically erase the key pair or any other credentials.
+	if net.SASL.Mechanism != "" && net.SASL.Mechanism != "PLAIN" {
+		return
+	}
+
+	net.logger.Printf("auto-saving SASL PLAIN credentials with username %q", username)
+	net.SASL.Mechanism = "PLAIN"
+	net.SASL.Plain.Username = username
+	net.SASL.Plain.Password = password
+	if err := net.user.srv.db.StoreNetwork(ctx, net.user.ID, &net.Network); err != nil {
+		net.logger.Printf("failed to save SASL PLAIN credentials: %v", err)
+	}
+}
+
 type user struct {
 	User
