'High-level IRC protocol / server connection management.'
 # built-ins
 from abc import ABC, abstractmethod
+from base64 import b64encode
 from dataclasses import dataclass
 from getpass import getuser
 from threading import Thread
             self.clear()
             self.challenge('LS', '302')
             return []
-        if self._challenge_met('END'):
+        if self._challenged('END'):
             return [f'ignoring post-END CAP message not NEW, DEL: {params}']
         match params[0]:
             case 'LS' | 'LIST':
                     self._dict[cap_name].enabled = params[0] == 'ACK'
         if self._challenge_met('LIST'):
             self.challenge('END')
-            self._challenge_set('END', done=True)
             return (['server capabilities (enabled: "+"):']
                     + [cap.str_for_log(cap_name)
                        for cap_name, cap in self._dict.items()])
         self._send(IrcMessage(verb='CAP', params=params))
         self._challenge_set(challenge_key)
 
+    @property
+    def could_sasl_plain(self) -> bool:
+        'Whether opportunity for some SASL AUTHENTICATE PLAIN attempt.'
+        return (self._challenged('END')
+                and 'sasl' in self._dict
+                and 'PLAIN' in self._dict['sasl'].data.split(','))
+
     def _challenge_met(self, step: str) -> bool:
         return self._challenges.get(step, False)
 
     hostname: str
     nickname: str
     realname: str
+    password: str
 
 
 class Client(ABC, ClientQueueMixin):
             case 'CAP':
                 for to_log in self._caps.process_msg(msg.params[1:]):
                     self.log.add(to_log)
+                if self._caps.could_sasl_plain and self.conn_setup.password:
+                    # NB: spec recommends to AUTHENTICATE during, rather than
+                    # after, CAPS negotation; opting for simplicity now instead
+                    self.send(IrcMessage('AUTHENTICATE', ('PLAIN',)))
+            case 'AUTHENTICATE':
+                if msg.params == ('+',):
+                    auth = b64encode((self.conn_setup.nickname + '\0' +
+                                      self.conn_setup.nickname + '\0' +
+                                      self.conn_setup.password
+                                      ).encode('utf-8')).decode('utf-8')
+                    self.send(IrcMessage('AUTHENTICATE', (auth,)))
+            case '904':
+                self.log.alert('SASL authentication failed')
 
 
 @dataclass
 
             client_wins[0].prompt.prefix_copy_to(win.prompt)
         return win
 
-    def cmd__connect(self, hostname: str, nickname: str, realname: str
+    def cmd__connect(self, hostname: str, nickname_and_pw: str, realname: str
                      ) -> None:
         'Create Client and pass it via NewClientEvent.'
+        split = nickname_and_pw.split(':', maxsplit=1)
+        nickname = split[0]
+        pw = split[1] if len(split) > 1 else ''
         self._put(NewClientEvent(
             _ClientKnowingTui(
                 q_out=self.q_out,
-                conn_setup=IrcConnSetup(hostname=hostname, nickname=nickname,
-                                        realname=realname))))
+                conn_setup=IrcConnSetup(hostname, nickname, realname, pw))))
 
 
 @dataclass