Deprecating Basic Authentication

As discussed before, Microsoft bangs the drums loudly about the need to stop using basic authentication and start using modern authentication. This is a big problem because hackers continue to compromise Office 365 accounts and tenants with techniques like password spray attacks. Moving to modern authentication and protecting as many accounts as possibly with multi-factor authentication (especially administrator accounts as mandated by Azure AD Security Defaults) are the right steps to take.

Update (September 25, 2021): Microsoft’s latest plan to decommission basic authentication for email connection protocols excludes SMTP AUTH. For now, Microsoft is focusing on removing basic authentication for protocols like IMAP4 and POP3, but they will come back to remove basic authentication for SMTP AUTH. It’s therefore wise to be considering how you will replace this protocol in production when that time comes.

Update (November 15, 2022): Three ways are available to send email using the Graph. Using the API described here is one. Read this article for the full details.

The Send-MailMessage Conundrum

Largely because of history, Exchange Online supports a wide variety of connectivity protocols. Microsoft is making some progress to convince customers to disable basic authentication for protocols they never use, and has upgraded older protocols like POP3 and IMAP4 to use OAuth 2.0 for modern authentication. As discussed in this blog, tenants will need to find PowerShell scripts which call the Send-MailMessage cmdlet and eventually upgrade the code with a more modern method to send email.

The Send-MailMessage cmdlet depends on the SMTP AUTH protocol to send email using basic authentication. Microsoft announced OAuth 2.0 support for SMTP AUTH in April 2020, but this doesn’t mean that an off-the-shelf replacement cmdlet is available. Microsoft says that the announcement “is for interactive applications to enable OAuth for IMAP and SMTP [AUTH].” In effect, this means mail clients or other applications which send, read, or otherwise process email. A quick trip to the referenced page leaves no doubt that this means more than replacing a few lines of code in a PowerShell script.

If no obvious replacement is available for Send-MailMessage, what should you do if you want to eliminate basic authentication but have a bunch of scripts using the cmdlet to send email? It’s a conundrum.

Enter the Graph

Microsoft’s note about SMTP AUTH points people to the Microsoft Graph API as an alternative method to send email. The SendMail call is part of the Graph Outlook API. Like all Graph API calls, it can be invoked through PowerShell.

SendMail can be used in two ways, depending on the permissions held by the app which calls it:

  • If the app has the Mail.Send application permission, the app can send email as any user or shared mailbox (but not a group mailbox) in the organization. An administrator must give consent to an app to use application permissions.
  • If the app has the Mail.Send delegated permission, the app can send mail as the signed-in user.

Using an application permission to send email is therefore extremely powerful (or dangerous). The Graph ignores Exchange Send As or Send on Behalf of permissions existing for mailboxes and is able to impersonate any user or shared mailbox to send email as if the message originated from that mailbox.

Converting PowerShell Scripts from Send-MailMessage

To test how easy it is to convert a script from “classic: Send-MailMessage code to use SendMail calls, I took a sample script written to illustrate how to send a welcome message to new users. The basic steps in the conversion are:

  • Create a registered app in Azure AD.
  • Assign the Mail.Send Graph (application or delegated) permission to the app.
  • Note the GUIDs for the app identifier and tenant identifier and generate an app secret (if using application permission).
  • In the script, add code to generate an access token and replace the code to call to Send-MailMessage with the equivalent for SendMail.

First, let’s look at using application permission. The code to create an access token looks like that shown below. The values passed for the application id ($AppId) and tenant identifier ($TenantId) will be different in your organization.

$AppId = "980e01d1-ce75-46ba-a054-4b61c787f682"
$AppSecret = "~_Q2.IPI3kpbmhOG.VtCb1r0_J9G4-D8jG"
$TenantId = "b562313f-14fc-43a2-9a7a-d2e27f4f3478"

# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $AppId
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $AppSecret
    grant_type    = "client_credentials"
}

$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
# Unpack Access Token
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$Headers = @{
            'Content-Type'  = "application\json"
            'Authorization' = "Bearer $Token" }

With a valid access token secured, the script can now make Graph API calls using the permissions encoded in the token. The script finds the set of user accounts created recently and loops through each user to compose a customized welcome message. Much of this needs to be done to use Send-MailMessage, so the part which changes is to swap out the creation of parameters to create and send the message with code to do the job with SendMail.

The following steps occur in the flow:

  • Populate the $MsgFrom variable (the person sending the message) with a valid email address for the sender. Because the SendMail API (with application permission) can send as any valid (licensed) mailbox, you can use an SMTP proxy address for any user or shared mailbox, including secondary proxy addresses. All that matters is that the address used is resolvable for a supported mailbox type. SendMail validates the sender when it tries to send the message and returns a 404 error if the sender address cannot be found in the tenant directory.
  • Loop through the set of recently created mailboxes to create the customized HMTL body for the message. Then populate a JSON-format parameter structure defining the message body (To and CC recipients and the message subject), an attachment, together with the commands used to submit the message to Exchange Online. Make sure that you use the primary SMTP addresses of the addressees, which can include distribution groups, mail contacts, guest accounts, and Microsoft 365 Groups.
  • Call the Invoke-RestMethod cmdlet to process the parameters and send the message.
$MsgFrom = “HRWelcomeMailbox@office365itpros.com”
$ccRecipient1 = “Kim.Akers@office365itpros.com”
$ccRecipient2 = “James.Ryan@Office365itpros.com”
# Define attachment to send to new users
$AttachmentFile = "C:\temp\WelcomeToOffice365ITPros.docx"
$ContentBase64 = [convert]::ToBase64String( [system.io.file]::readallbytes($AttachmentFile))
ForEach ($User in $Users) {
      $EmailRecipient = $User.PrimarySmtpAddress
      Write-Host "Sending welcome email to" $User.DisplayName
      $MsgSubject = "A Hundred Thousand Welcomes to " + $User.DisplayName
      $htmlHeaderUser = "<h2>New User " + $User.DisplayName + "</h2>"
      $htmlline1 = "<p><b>Welcome to Office 365</b></p>"
      $htmlline2 = "<p>You can open Office Online by clicking <a href=https://www.office.com/?auth=2>here</a> </p>"
      $htmlline3 = "<p>Have a great time and be sure to call the help desk if you need assistance.</p>"
      $htmlbody = $htmlheaderUser + $htmlline1 + $htmlline2 + $htmlline3 + "<p>"
      $HtmlMsg = "</body></html>" + $HtmlHead + $HtmlBody
# Create message body and properties and send
        $MessageParams = @{
          "URI"         = "https://graph.microsoft.com/v1.0/users/$MsgFrom/sendMail"
          "Headers"     = $Headers
          "Method"      = "POST"
          "ContentType" = 'application/json'
          "Body" = (@{
                "message" = @{
                "subject" = $MsgSubject
                "body"    = @{
                    "contentType" = 'HTML' 
                     "content"     = $htmlMsg }
         "attachments" = @(
             @{
              "@odata.type" = "#microsoft.graph.fileAttachment"
              "name" = $AttachmentFile
              "contentType" = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
              "contentBytes" = $ContentBase64 })  
           "toRecipients" = @(
           @{
             "emailAddress" = @{"address" = $EmailRecipient }
           } ) 
          "ccRecipients" = @(
           @{
             "emailAddress" = @{"address" = $ccRecipient1 }
           } ,
            @{
             "emailAddress" = @{"address" = $ccRecipient2 }
           } )       
         }
      }) | ConvertTo-JSON -Depth 6
   }   # Send the message
   Invoke-RestMethod @Messageparams
}

You can download the full script from GitHub. As you’ll note, adding attachments to a message is not as straightforward as it is with Send-MailMessage. Two points to note are:

  • Uploading an attachment when sending a message works with attachments up to 3 MB. If you need to send larger attachments of up to 150 MB, you need to upload them using a different approach (see Microsoft’s note). Of course, other factors such as the mailbox and organizational limits influence the maximum size of messages.
  • The content type is the MIME type for the attachment (remember, we’re using email, which is why the attachment content must be Base64-encoded). I found the list of MIME types available here helpful. I haven’t tested every possible type of attachment, but I know that when I ran into problems, sometimes the “multipart/mixed” content type worked where an official MIME type didn’t.

The Mail.Send API can set x-headers in messages, which is something that the Send-MailMessage cmdlet cannot do. This is one reason why people used the (now deprecated) .Net SmtpClient class to create and send email in scripts. You should convert these scripts to use the Graph instead.

That Big Permission Question

If you’re not worried about having an app which can send email for any mailbox in the organization, it would be easy to use the same app as the basis to upgrade all scripts. The code to secure an access token remains the same and can be pasted into each script. The remaining task is then to update the Send-MailMessage code to use SendMail.

Having an all-powerful mail sending app will make life easier (or at least, make the conversion easier) for programmers but might not be deemed acceptable by a corporate security team unless a guarantee is given that the app can’t be abused. Given that such a guarantee is impossible, you can see where the problem lies.

Use an Application Access Policy to Restrict Mailboxes Used by the App

One solution is to use an application access policy to allow the app to access only certain mailboxes. An application access policy combines three elements:

  • Defines deny or allow access.
  • Covers one or more apps registered in Azure AD.
  • Applies to Exchange Web Services or Graph API calls made to mailboxes for members of a mail-enabled security group or individual users (objects with a security principal).

Thus, we can create an application access policy to restrict access to a set of mailboxes. For example:

New-ApplicationAccessPolicy -AppId 970e01d1-ce75-46ba-a054-4b61c787f682 -PolicyScopeGroupId SendMailApp.Control@office365itpros.com -AccessRight RestrictAccess -Description "Restrict access to app allowed to send email using the Graph SendMail API"

This policy restricts access to the App with identifier 970e01d1-ce75-46ba-a054-4b61c787f682 to the mailboxes defined in the security group SendMailApp.Control@office365itpros.com. After creating the policy, only the mailboxes for the members of the security group can be used to send email using the SendMail API call. Attempting to send email using any other account generates a 403 error when calling SendMail because the policy denies access to the mailbox.

Exchange Online caches group membership for access policies to make checks better performing and you might have to wait for an hour or so before membership changes become effective, but the policy restrictions work very well.

Use Delegated Permissions

Another option is to use an Azure AD registered app with delegated Mail.Send permission. This restricts the ability to send email to the signed in account, so it’s more in tune with the way Send-MailMessage works. The code to create and send the message remains the same; the only thing which changes is that the access token generated by Azure AD when the app authenticates restricts its use to a signed-in user. In most cases, using delegated permission to call the SendMail API is the better option. Unless of course you really do want to be able to send messages as everyone in the tenant.

About the Author

Tony Redmond

Tony Redmond has written thousands of articles about Microsoft technology since 1996. He is the lead author for the Office 365 for IT Pros eBook, the only book covering Office 365 that is updated monthly to keep pace with change in the cloud. Apart from contributing to Practical365.com, Tony also writes at Office365itpros.com to support the development of the eBook. He has been a Microsoft MVP since 2004.

Comments

  1. Joost

    I find it strange that there’s a deprecation warning for the entire cmdlet when one use case (authenticated sending via Exchange Online) might stop working.
    Unauthenticated send (because your IP address is on a relay whitelist, or because you connect to a foreign mail server where you can’t authenticate anyway) should not result in a deprecation warning, IMO.

  2. Aaron

    Hi Tony, I found this page a while back and have been using your code in many of my scripts. I’m looking to migrate to certificate-based authentication for these scripts and I was wondering if you had a version showing how to use a certificate instead of the client secret?

    1. Avatar photo

      Well, it’s a matter of authentication, so the app needs to reference a certificate that’s known to Azure AD. See https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-create-self-signed-certificate.

      BTW, there are other ways to send email using the Graph. See this later article: https://practical365.com/send-email-powershell-graph/

  3. Chris P

    Hi – Do you have an example of acquiring the token with MSAL.PS for the delegated flow?

    I have the shared mailbox where a service account has send-as permissions in Exchange Online.
    I have the AAD app which has Mail.Send Delegated API permissions.

    When I try to access token with MSAL.PS module, the option for client credentials is not there after I specify clientsecret etc.

    1. Avatar photo

      AFAIK, the Graph API methods for sending email all depend on having credentials for a specific mailbox connected to a specific account. Work is ongoing to migrate EWS to the Graph and that’s when support for scenarios like sending email from a shared mailbox using SendAs permission will be supported. At least, that’s what I heard. Don’t quote me on it.

  4. Mahesh S

    Can we send files or transfer files over smtp to sftp.

  5. Will

    Thanks for sharing the script with us.

    I tried to use it by replacing the information, but the email was not delivered, nor did it return any errors.
    Do you know what it could be?

  6. Arjan

    Hi,
    Just wondering, since this solution cannot be used to send as a Distribution Group, would you know of any Send-MailMessage alternative to do that?
    I have a bunch of scripts that send on behalf of a DL, but I cannot find a good way to move this away from Send-MailMessage.
    Many thanks!

  7. Doug

    So for a small business that does not have an azure tenant, but has M365 through a partner like godady, is there a way to set up a registered app? I am helping a friend and am using send-mailmessage, but would like to get it flipped to a modern auth.

      1. Doug

        You rock. Thank you for the very fast response.

  8. andrea

    Hy, great job
    Is there an example to put this in c#?

    thanks

  9. Avatar photo

    The text of the article includes a link to a GitHub script. In the script you can see how to use a registered Azure AD app to get an access token from Azure AD. The access token contains the permissions. If you assign a delegated permission, it can only be used by a user. If it’s an application permission, it can be used by an app to impersonate users. That’s how Graph authentication happens. See https://docs.microsoft.com/en-us/graph/auth/auth-concepts

  10. greg

    “the only thing which changes is signing in to authenticate.”

    It would be great if you could post an example of this as well..

  11. Andy Jackson

    Just noticed that the -PolicyScopeGroupID parameter on New-ApplicationAccessPolicy only accepts users, meaning you are unable to set a shared mailbox.

    This parameter only accepts recipients that are security principals (users or groups that can have permissions assigned to them). The following types of recipients are not security principals, so you can’t use them with this parameter:

    Discovery mailboxes
    Dynamic distribution groups
    Distribution groups
    Shared mailboxes

  12. Joerg

    I have a question on the delegated vs application access permissions.
    If I understand correctly, I need to use application permissions for automated scripts that run for example from a scheduled task?
    Because for delegated permission the user needs to be logged in and if MFA is enforced in the tenant the user would be prompted regularly to enter an MFA code. For delegated permissions I cannot use cert based auth or an application secret.
    So the only choice would be application permissions and restrict to a single mailbox with application access policy.
    Sounds like a lot of work just to send an email now and then. 🙂

  13. Roy Ashbrook

    Any particular reason using webrequest instead of restmethod on the first call?

    $tokenresponse = Invoke-RestMethod -Uri $uri -Method POST -Body $body
    $token = $tokenresponse.access_token

    Also, thanks so much for this. I have been looking for an example to start the replacement process for send-mailmessage and all examples use /me/ in the sendMail URI. In hindsight, it’s pretty obvious, but this line ended a few hours of frustrating recreations of scripts and tutorials trying to see what i was missing. Not all heroes wear capes! =)

    “URI” = “https://graph.microsoft.com/v1.0/users/$MsgFrom/sendMail”

    1. Avatar photo

      I’m no hero, as evident by the way that I mix and match PowerShell commands without any rhyme or reason. Seriously, I put together scripts using code I already have rather than writing everything from scratch. It saves time and gets me to the point where something interesting happens, like sending email. Besides, I know that people take code like this and transform it to meet their own needs…

  14. Michiel van Heerde

    Thanks for the article and the heads up on this. It seems however that this covers authenticating to and sending mail directly using the online environment, any idea how environments that have scripts use send-mailmessage functionality to on premises hybrid servers to handle this?

      1. Michiel van Heerde

        Thanks for the reply. We are going to continue using send-message, until Microsoft decides to delete it from the available commands.

  15. Stuart Schifter

    Just used GRAPH Send Mail and Create Message with attachments and I ran into some challenges. Currently, I have not implemented support for attachments > 3MB that must be uploaded by chunks. My app accessing Calendars creates CSV logs of its work. In my Logging behaviors at the end of application processing I allow two options for the logs:

    Option 1: Use Create Message to save the message with attachments in the mailbox “drafts” folder. This message is not Sent.
    Option 2: Send the message to recipients with attachments

    I have a couple of points \ maybe questions to share.
    1. The MS development team was not consistent with the authoring of the JSON “message” Body used for Send Mail and Create Message API. (Bug, by Design) In your Send Mail example the JSON Body has a top node named “messages” which is correct. But, when using Create Message the JSON Body structure is EXACTLY the same except that the JSON Body does not have a root node named “messages’. This took me hours to figure out and resolve. It should be fixed as both API’s deal with messages.

    1. Stuart Schifter

      2. I understand how you can add a large >3mb attachment by chunks using CreateMessage which saves in the “Drafts” folder. Once you post the initial CreateMessage, you can use the upload the attachment as you have the messageID to use. After uploading the attachment by chunks, you can call Send on the Draft email to deliver the email and save a copy in the Sent folder.
      a. Using Send, what I don’t know is how you execute the upload of the chunked large attachment to a message that is Sent to the Outbox folder as soon as it is submitted. How is the upload event session tied to the Send API before the Invoke?

  16. Olivier

    what are the limitations imposed by Microsoft ?

Leave a Reply