Package: pysasl
pysaslPure Python SASL client and server library. The design of the library is
intended to be agnostic of the protocol or network library.The library currently offersPLAINandLOGINmechanisms by default. TheCRAM-MD5,EXTERNAL, andXOAUTH2mechanisms are also available for special
circumstances.There are currently no plans to implementsecurity layernegotiation support.API DocumentationInstallationAvailable inPyPi:$pipinstallpysaslRunning TestsFirst, installHatch. To run the build checks:$hatchruncheck$hatchrunall:check# to run against all supported Python versionsUsageServer-sideServer-side SASL has three basic requirements:Must advertise supported mechanisms,Must issue authentication challenges to the client and read responses,Must determine if credentials are considered valid.Advertising MechanismsImplementations may decide on any sub-set of mechanisms to advertise. Make this
choice when instantiating theSASLAuthobject:frompysaslimportSASLAuthauth1=SASLAuth.defaults()# equivalent to...auth2=SASLAuth.named([b'PLAIN',b'LOGIN'])To get the names of all available mechanisms:mechanisms=[mech.nameformechinauth1.server_mechanisms]mech=auth1.get_server(b'PLAIN')Issuing ChallengesOnce a mechanism has been chosen by the client, enter a loop of issuing
challenges to the client:defserver_side_authentication(sock,mech):challenges=[]whileTrue:try:creds,_=mech.server_attempt(challenges)returncredsexceptServerChallengeaschal:sock.send(chal.data+b'\r\n')resp=sock.recv(1024).rstrip(b'\r\n')challenges.append(ChallengeResponse(chal.data,resp))It's worth noting that implementations are not quite that simple. Most will
expect all transmissions to base64-encoded, often with a prefix before the
server challenges such as334or+. See the appropriate RFC for your
protocol, such asRFC 4954 for SMTPorRFC 3501 for IMAP.Checking CredentialsOnce the challenge-response loop has been completed and we are left with the
aServerCredentialsobject, we can access information from the
attempt:frompysasl.creds.serverimportServerCredentialsfrompysasl.identityimportClearIdentity,HashedIdentityresult:ServerCredentials=...print('Authenticating as:',result.authcid)print('Authorizing as:',result.authzid)# To compare to a known cleartext password...identity=ClearIdentity('myuser','s3kr3t')assertresult.verify(identity)# Or to compare hashes...frompysasl.hashingimportBuiltinHashidentity=HashedIdentity('myuser, '$pbkdf2-sha256$500000$...', hash=BuiltinHash())assertresult.verify(identity)# Or use passlib hashing...frompasslib.appsimportcustom_app_contextidentity=HashedIdentity('myuser','$6$rounds=656000$...',hash=custom_app_context)assertresult.verify(identity)Client-sideThe goal of client-side authentication is to respond to server challenges until
the authentication attempt either succeeds or fails.Choosing a MechanismThe first step is to pick a SASL mechanism. The protocol should allow the server
to advertise to the client which mechanisms are available to it:frompysaslimportSASLAuthauth=SASLAuth.named(advertised_mechanism_names)mech=auth.client_mechanisms[0]Any mechanism name that is not recognized will be ignored.Issuing ResponsesOnce a mechanism is chosen, we enter of a loop of responding to server
challenges:frompysasl.creds.clientimportClientCredentialsdefclient_side_authentication(sock,mech,username,password):creds=ClientCredentials(username,password)challenges=[]whileTrue:resp=mech.client_attempt(creds,challenges)sock.send(resp+b'\r\n')data=sock.recv(1024).rstrip(b'\r\n')ifdata=='SUCCESS':returnTrueelifdata=='FAILURE':returnFalsechallenges.append(ServerChallenge(data))As you might expect, a real protocol probably won't returnSUCCESSorFAILURE, that will depend entirely on the details of the protocol.Supporting Initial ResponsesSome protocols (e.g. SMTP) support the client ability to send an initial
response before the first server challenge, for mechanisms that support it.
A perfectly valid authentication can then have no challenges at all:AUTH PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk
235 2.7.0 Authentication successfulIn this case, both client-side and server-side authentication should be
handled a bit differently. For example for server-side:challenges=[]ifinitial_response:challenges.append(ChallengeResponse(b'',initial_response))And for client-side, just callresp = mech.client_attempt(creds, [])to get the initial response before starting the transmission. All
mechanisms should either return an initial response or an empty string
when given an empty list for the second argument.