Azure-Samples / ms-identity-python-webapp

A Python web application calling Microsoft graph that is secured using the Microsoft identity platform
MIT License
284 stars 135 forks source link

How to use this for authenticating an SMTP session? #70

Closed tks-socius closed 2 years ago

tks-socius commented 2 years ago

I have used this app (downloaded from the AAD portal with my secret in it) and tried to use the given token for SMTP authentication, using the information from

Logging in with my office email account works and the /graphcall works as well. However the SMTP authentication is not working, I am receiving reply: retcode (535); Msg: b'5.7.3 Authentication unsuccessful [LO2P265CA0061.GBRP265.PROD.OUTLOOK.COM]' from it.

The Scope of the token that I am asking for is SCOPE = ["User.ReadBasic.All", "https://outlook.office.com/SMTP.Send"] and the app has SMTP.send turned on in the config file

Here is a class extending smtplib.SMTP that I have written based on those:


import smtplib
import base64

class MicrosoftSMTP(smtplib.SMTP):
    def __init__(self, host="smtp.office365.com", port=587, **kwargs):
        super().__init__(host=host, port=port, **kwargs)

    @staticmethod
    def encode_auth_token(username, token):
        just_a_str = f"user={username}\x01auth=Bearer {token}\x01\x01"
        xoauth2_token = base64.b64encode(just_a_str.encode())

        return xoauth2_token

    def authenticate(self, username, token):
        self.helo()

        # first step, we
        code, msg = self.docmd("auth", "XOAUTH2")
        if code != 334:
            raise Exception(msg.decode())

        # send the token
        self.send(self.encode_auth_token(username, token))

and the code to connect with the credentials from the app here, and adding a page where I display the token json for a sanity check:


@app.route("/send_to_self")
def send_to_self():
    token = _get_token_from_cache(app_config.SCOPE)
    if not token:
        return redirect(url_for("login"))

    # connect to the server
    connection = MicrosoftSMTP()
    connection.set_debuglevel(True)  # for output
    connection.starttls()
    connection.authenticate(
        # same as session["user"]["preferred_username"]
        token["id_token_claims"]["preferred_username"],
        token["access_token"],
    )

    # ... would write an email here with connection.sendmail( ... )

    connection.quit()

    return render_template(
        "send_to_self.html",
        data=token,
        data_session=session["flow"],
        data_user=session["user"],
    )

The authentication is failing, here is the full log:

send: 'ehlo 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa\r\n'
reply: b'250-LO2P265CA0516.outlook.office365.com Hello [<A.GENERAL.IP>]\r\n'
reply: b'250-SIZE 157286400\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-DSN\r\n'
reply: b'250-ENHANCEDSTATUSCODES\r\n'
reply: b'250-STARTTLS\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250-BINARYMIME\r\n'
reply: b'250-CHUNKING\r\n'
reply: b'250 SMTPUTF8\r\n'
reply: retcode (250); Msg: b'LO2P265CA0516.outlook.office365.com Hello [<A.GENERAL.IP>]\nSIZE 157286400\nPIPELINING\nDSN\nENHANCEDSTATUSCODES\nSTARTTLS\n8BITMIME\nBINARYMIME\nCHUNKING\nSMTPUTF8'
send: 'STARTTLS\r\n'
reply: b'220 2.0.0 SMTP server ready\r\n'
reply: retcode (220); Msg: b'2.0.0 SMTP server ready'
send: 'helo 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa\r\n'
reply: b'250 LO2P265CA0516.outlook.office365.com Hello [<A.GENERAL.IP>]\r\n'
reply: retcode (250); Msg: b'LO2P265CA0516.outlook.office365.com Hello [<A.GENERAL.IP>]'
send: 'auth XOAUTH2\r\n'
reply: b'334 \r\n'
reply: retcode (334); Msg: b''
send: b'dX......EB'
send: 'quit\r\n'
reply: b'535 5.7.3 Authentication unsuccessful [LO2P265CA0516.GBRP265.PROD.OUTLOOK.COM]\r\n'
reply: retcode (535); Msg: b'5.7.3 Authentication unsuccessful [LO2P265CA0516.GBRP265.PROD.OUTLOOK.COM]'

things I have cheeked:

  1. we have SMTP allowed for this mailbox
  2. the token has the SMTP allowed
  3. the XOAUTH2 token encoder's output matches that of the example on the website

FYI the token data looks like this, with the tokens and username removed

{
    "access_token": "ey<...>aw",
    "client_info": "ey<...>In0",
    "expires_in": 3599,
    "ext_expires_in": 3599,
    "id_token": "ey<...>jQ",
    "id_token_claims": {
        "aud": "8<...>9",
        "exp": 1634319637,
        "iat": 1634315737,
        "iss": "https://login.microsoftonline.com/5<...>1/v2.0",
        "name": "<Name of the user>",
        "nbf": 1634315737,
        "nonce": "c1<...>d0",
        "oid": "cd<...>1b",
        "preferred_username": "user.name@company.com",
        "rh": "0.A<...>As.",
        "sub": "2w<...>ww",
        "tid": "50<...>31",
        "uti": "8W<...>AA",
        "ver": "2.0"
    },
    "refresh_token": "0.A<...>4Y",
    "scope": "openid profile SMTP.Send User.ReadBasic.All email",
    "token_type": "Bearer"
}
rayluo commented 2 years ago

I haven't tried using the acquired access token (AT) in an SMTP session. Would you mind checking out from the SMTP side of docs to see what credential does SMTP accept? Traditionally, SMTP session would require the end user password, and a send mail Graph API would require an OAuth2 access token. They are different, unless an SMTP service explicitly documents that you could use an AT as SMTP password.

This sample utilizes the MSAL library to get you an AT, and AT only.

rayluo commented 2 years ago

Besides the comment above, it seems the StackOverflow community answered your question since then. Please follow up there.