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.
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.
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?
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
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/
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.
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
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.
Can we send files or transfer files over smtp to sftp.
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?
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
Nope. I can’t debug code remotely. And advice given without seeing your code is unlikely to be much good.
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!
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
I believe that this might be a gap that Microsoft has still to close. The current Graph APIs require a user identifier and don’t appear to support the Send As and Send On Behalf of scenarios for distribution groups and Microsoft 365 groups. I’ve asked Microsoft about this.
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.
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
You do have an Azure AD tenant. Microsoft 365 can’t work without it. If you have an admin account, try signing into https://aad.portal.azure.com/.
You rock. Thank you for the very fast response.
Hy, great job
Is there an example to put this in c#?
thanks
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
Did you search the Microsoft documentation for the relevant Graph APIs?
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
Like https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
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
“the only thing which changes is signing in to authenticate.”
It would be great if you could post an example of this as well..
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
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
Yep. I think Microsoft designed the feature with user mailboxes in mind. They’re making changes to these policies and perhaps expanding to cover other mailbox types will be one of them. I will ping some people I know…
the most recent update:
Today, we are announcing that, effective October 1, 2022, we will begin to permanently disable Basic Auth in all tenants, regardless of usage (with the exception of SMTP Auth, which can still be re-enabled after that).
https://techcommunity.microsoft.com/t5/exchange-team-blog/basic-authentication-and-exchange-online-september-2021-update/ba-p/2772210
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
SMTP is an exception: https://office365itpros.com/2021/09/27/basic-authentication-disappears-exchange-online/
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. 🙂
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
No one said live was meant to be easy… I’m hoping Microsoft will make the API more flexible over time.
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”
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
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…
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?
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
The Graph APIs don’t exist for on-premises servers, so the answer depends on where the account being used to send the email is located. If it’s on-premises, I believe you’ll need to continue using Send-MailMessage. If online, you can use the Graph APIs. However, I have not tested this scenario and am open to correction on the point.
Thanks for the reply. We are going to continue using send-message, until Microsoft decides to delete it from the available commands.
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.
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?
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
I can’t help because apart from reading the Graph API documentation about how to upload an attachment, I didn’t go through the steps and code necessary to do the work. There’s a limit to what you can fit in a single article…
what are the limitations imposed by Microsoft ?
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
What do you mean? Have you looked at the API documentation referenced in the article?
im talking about sending limitations. as you might know using O365 as relay impose limitations
https://docs.microsoft.com/en-gb/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365
im just wondering if throught graph API we have to deal with limitations.
The Real Person!
Author Tony Redmond acts as a real person and passed all tests against spambots. Anti-Spam by CleanTalk.
The restrictions are imposed by the service (Exchange Online), not the API. The same limitations exist in terms of rate of message send. This isn’t open season to use Exchange Online as a bulk mail sender.