Pure Python SASL client and server library.
Pure 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 offers
CRAM-MD5 mechanisms by
XOAUTH2 mechanisms are also available for special
There are currently no plans to implement security layer negotiation support.
Available in PyPi:
pip install pysasl
Install into a virtual environment:
python3 -m venv .venv source .venv/bin/activate pip install -r test/requirements.txt pip install -e .
Run the tests and report coverage metrics:
Server-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.
Implementations may decide on any sub-set of mechanisms to advertise. Make this
choice when instantiating the
from pysasl import SASLAuth auth1 = SASLAuth.defaults() # equivalent to... auth2 = SASLAuth.named([b'PLAIN', b'LOGIN'])
To get the names of all available mechanisms:
mechanisms = [mech.name for mech in auth1.server_mechanisms] mech = auth1.get(b'PLAIN')
Once a mechanism has been chosen by the client, enter a loop of issuing challenges to the client:
def server_side_authentication(sock, mech): challenges =  while True: try: creds, _ = mech.server_attempt(challenges) return creds except ServerChallenge as chal: 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 as
+. See the appropriate RFC for your
protocol, such as RFC 4954 for SMTP or RFC 3501 for IMAP.
Once the challenge-response loop has been completed and we are left with the
AuthenticationCredentials object, we can access information from the
from pysasl.creds import StoredSecret print('Authenticated as:', result.authcid) print('Authorization ID:', result.authzid) print('Assumed identity:', result.identity) # To compare to a known password... assert result.check_secret(StoredSecret('s3kr3t')) # Or to compare hashes... from pysasl.hashing import BuiltinHash secret = StoredSecret('1baa33d03d0...', hash=BuiltinHash()) assert result.check_secret(secret) # Or use passlib hashing... from passlib.apps import custom_app_context secret = StoredSecret('$6$rounds=656000$...', hash=custom_app_context) assert result.check_secret(secret)
The goal of client-side authentication is to respond to server challenges until the authentication attempt either succeeds or fails.
Choosing a Mechanism
The 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:
from pysasl import SASLAuth auth = SASLAuth.named(advertised_mechanism_names) mech = auth.client_mechanisms
Any mechanism name that is not recognized will be ignored.
Once a mechanism is chosen, we enter of a loop of responding to server challenges:
from pysasl.creds import AuthenticationCredentials def client_side_authentication(sock, mech, username, password): creds = AuthenticationCredentials(username, password) challenges =  while True: resp = mech.client_attempt(creds, challenges) sock.send(resp + b'\r\n') data = sock.recv(1024).rstrip(b'\r\n') if data == 'SUCCESS': return True elif data == 'FAILURE': return False challenges.append(ServerChallenge(data))
As you might expect, a real protocol probably won't return
FAILURE, that will depend entirely on the details of the protocol.
Supporting Initial Responses
Some 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 successful
In this case, both client-side and server-side authentication should be handled a bit differently. For example for server-side:
challenges =  if initial_response: challenges.append(ChallengeResponse(b'', initial_response))
And for client-side, just call
resp = 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.
The MIT License (MIT)
Copyright (c) 2020 Ian Good
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Release history Release notifications | RSS feed
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
|Filename, size||File type||Python version||Upload date||Hashes|
|Filename, size pysasl-0.8.0-py3-none-any.whl (16.5 kB)||File type Wheel||Python version py3||Upload date||Hashes View|
|Filename, size pysasl-0.8.0.tar.gz (17.1 kB)||File type Source||Python version None||Upload date||Hashes View|