Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented OAuth support for Exchange Online and upgraded target framework to .net 4.8 #135

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Documentation/FullyAnnotatedConfigReference.xml
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,27 @@
</EWSKeyVaultSecret>
-->

<!-- Add the information of your Office365 Application here. You must setup an App registration in Microsoft Entra for Mail2Bug, in order to use OAuth.
Minimum required API permissions are for the type "Application" for "Office 365 Exchange Online" with IMAP.*, Mail.*, POP.*, SMTP.*
The application will impersonate the EWSUsername and get access to that application. Make sure to allow the application to access the EWSUsername and allow impersonating it.

If you want to use Basic Authentication, omit this element!
Warning: You must still setup the EWSPasswordFile and EWSUsername, because they are used to log in in Azure Devops/TFS. OAuth only supports the Exchange Online login to read/send mails!
-->
<EWSOAuthSecret>
<!-- Your Tenant ID, can be found in the application Overview -->
<TenantID>00000000-0000-0000-0000-000000000000</TenantID>
<!-- Your Application (client) ID, can be found in the application Overview -->
<ClientID>00000000-0000-0000-0000-000000000000</ClientID>
<!-- Pointer to a file that stores the client credentials secret. The file is expected to be encrypted
with DPAPI - use the DpapiTool.exe from the Tools subfolder to create one. Note that the tool needs to be
run under the same credentials that mail2bug will run with, otherwise it won't be able to decrypt the
contents. The client secret string that must be encrypted has to be generated via Microsoft Entra and looks like this: k5r933EbGAQHjTzzNwii71JtRMb0rxI09htsJqMF -->
<ClientSecretFile>.\Resources\mail2bugAppClientSecret.bin</ClientSecretFile>
<!-- Optional useragent -->
<UserAgentName>Mail2Bug/1.0.0.0</UserAgentName>
</EWSOAuthSecret>

<!-- Instead of using the whole ConversationIndex, use only the ConversationID, which is the guid portion of
the ConversationIndex, to identify a conversation. This should help avoid the possibility of replies
inadvertently spawning new bugs. See https://msdn.microsoft.com/en-us/library/ee202481(v=exchg.80).aspx
Expand Down
16 changes: 16 additions & 0 deletions Documentation/Mail2BugConfigExample.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@
contents. -->
<EWSPasswordFile>.\Resources\mail2bug.bin</EWSPasswordFile>

<!-- Add the information of your Office365 Application here. You must setup an App registration in Microsoft Entra for Mail2Bug, in order to use OAuth.
Minimum required API permissions are for the type "Application" for "Office 365 Exchange Online" with IMAP.*, Mail.*, POP.*, SMTP.*
The application will impersonate the EWSUsername and get access to that application. Make sure to allow the application to access the EWSUsername and allow impersonating it.
If you want to use Basic Authentication, omit this element! Warning: You must still setup the EWSPasswordFile and EWSUsername to log in in Azure Devops/TFS.
-->
<EWSOAuthSecret>
<TenantID>00000000-0000-0000-0000-000000000000</TenantID>
<ClientID>00000000-0000-0000-0000-000000000000</ClientID>
<!-- Pointer to a file that stores the client credentials secret. The file is expected to be encrypted
with DPAPI - use the DpapiTool.exe from the Tools subfolder to create one. Note that the tool needs to be
run under the same credentials that mail2bug will run with, otherwise it won't be able to decrypt the
contents. The client secret string that must be encrypted has to be generated via Microsoft Entra and looks like this: k5r933EbGAQHjTzzNwii71JtRMb0rxI09htsJqMF -->
<ClientSecretFile>.\Resources\mail2bugAppClientSecret.bin</ClientSecretFile>
<UserAgentName>Mail2Bug/1.0.0.0</UserAgentName>
</EWSOAuthSecret>

<!-- Warning: Changing this on an existing instance will break links with bugs that it already created. -->
<UseConversationGuidOnly>true</UseConversationGuidOnly>

Expand Down
9 changes: 9 additions & 0 deletions Mail2Bug/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public class KeyVaultSecret
public string ApplicationSecretEnvironmentVariableName { get; set; }
}

public class OAuthSecret
{
public string TenantID { get; set; }
public string ClientID { get; set; }
public string ClientSecretFile { get; set; }
public string UserAgentName { get; set; }
}

public class TfsServerConfig
{
// The TFS collection URL to connect to. e.g:
Expand Down Expand Up @@ -202,6 +210,7 @@ public enum MailboxServiceType
public string EWSUsername { get; set; }
public string EWSPasswordFile { get; set; }
public KeyVaultSecret EWSKeyVaultSecret { get; set; }
public OAuthSecret EWSOAuthSecret { get; set; }

#endregion

Expand Down
43 changes: 38 additions & 5 deletions Mail2Bug/Email/EWS/EWSConnectionManger.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Cache;
using System.Net;
using System.Text;
using log4net;
using Microsoft.Exchange.WebServices.Data;
using Newtonsoft.Json.Linq;
using Mail2Bug.Helpers;

namespace Mail2Bug.Email.EWS
{
Expand All @@ -22,6 +28,24 @@ public struct Credentials
public string EmailAddress;
public string UserName;
public string Password;
public CredentialsOAuth OAuthCredentials;
}

public class CredentialsOAuth
{
public CredentialsOAuth() { }
public CredentialsOAuth(Config.EmailSettings emailSettings, string clientSecret)
{
TenantID = emailSettings.EWSOAuthSecret.TenantID;
ClientID = emailSettings.EWSOAuthSecret.ClientID;
ClientSecret = clientSecret;
UserAgentName = emailSettings.EWSOAuthSecret.UserAgentName;
}

public string TenantID { get; set; }
public string ClientID { get; set; }
public string ClientSecret { get; set; }
public string UserAgentName { get; set; }
}

public struct EWSConnection
Expand Down Expand Up @@ -82,11 +106,21 @@ static private Tuple<string, string, int, bool> GetKeyFromCredentials(Credential
static private EWSConnection ConnectToEWS(Credentials credentials, bool useConversationGuidOnly)
{
Logger.DebugFormat("Initializing FolderMailboxManager for email adderss {0}", credentials.EmailAddress);
var exchangeService = new ExchangeService(ExchangeVersion.Exchange2010_SP1)
ExchangeService exchangeService;
if (credentials.OAuthCredentials != null)
{
Credentials = new WebCredentials(credentials.UserName, credentials.Password),
Timeout = 60000
};
Logger.DebugFormat("OAuth authentication for email address {0}", credentials.EmailAddress);
exchangeService = EWSOAuthHelper.OAuthConnectPost(credentials.OAuthCredentials, credentials.EmailAddress);
}
else
{
Logger.DebugFormat("Basic authentication for email address {0}", credentials.EmailAddress);
exchangeService = new ExchangeService(ExchangeVersion.Exchange2010_SP1)
{
Credentials = new WebCredentials(credentials.UserName, credentials.Password),
Timeout = 60000
};
}

exchangeService.AutodiscoverUrl(
credentials.EmailAddress,
Expand All @@ -108,7 +142,6 @@ static private EWSConnection ConnectToEWS(Credentials credentials, bool useConve
};
}


private readonly Dictionary<Tuple<string, string, int, bool>, EWSConnection> _cachedConnections;
private readonly bool _enableConnectionCaching;

Expand Down
6 changes: 5 additions & 1 deletion Mail2Bug/Email/MailboxManagerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ public IMailboxManager CreateMailboxManager(Config.EmailSettings emailSettings)
{
var credentialsHelper = new Helpers.CredentialsHelper();
string password = credentialsHelper.GetPassword(emailSettings.EWSPasswordFile, emailSettings.EncryptionScope, emailSettings.EWSKeyVaultSecret);
string clientSecret = emailSettings.EWSOAuthSecret != null
? credentialsHelper.GetPassword(emailSettings.EWSOAuthSecret.ClientSecretFile, emailSettings.EncryptionScope, emailSettings.EWSKeyVaultSecret)
: null;
var credentials = new EWSConnectionManger.Credentials
{
EmailAddress = emailSettings.EWSMailboxAddress,
UserName = emailSettings.EWSUsername,
Password = password
Password = password,
OAuthCredentials = emailSettings.EWSOAuthSecret != null ? new EWSConnectionManger.CredentialsOAuth(emailSettings, clientSecret) : null,
};

var exchangeService = _connectionManger.GetConnection(credentials, emailSettings.UseConversationGuidOnly);
Expand Down
73 changes: 73 additions & 0 deletions Mail2Bug/Helpers/EWSOAuthHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Microsoft.Exchange.WebServices.Data;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Cache;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Mail2Bug.Email.EWS;

namespace Mail2Bug.Helpers
{
public class EWSOAuthHelper
{
public static ExchangeService OAuthConnectPost(EWSConnectionManger.CredentialsOAuth oAuthCredentials, string emailAddress)
{
string LoginURL = String.Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/token", oAuthCredentials.TenantID);

var LogValues = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", oAuthCredentials.ClientID },
{ "client_secret", oAuthCredentials.ClientSecret },
{ "scope", "https://outlook.office365.com/.default" }
};
string postData = "";
foreach (var v in LogValues)
{
postData += (String.IsNullOrWhiteSpace(postData) ? "" : "&") + v.Key + "=" + v.Value;
}
var data = Encoding.ASCII.GetBytes(postData);

ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
ServicePointManager.Expect100Continue = true;
ServicePointManager.DefaultConnectionLimit = 9999;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12
| SecurityProtocolType.Ssl3;

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(LoginURL);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.Accept = "*/*";
request.UserAgent = oAuthCredentials.UserAgentName;
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore);
request.ContentLength = data.Length;
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}

using (var response = (HttpWebResponse)request.GetResponse())
using (Stream stream = response.GetResponseStream())
using (var reader = new StreamReader(stream))
{
var json = reader.ReadToEnd();
var aToken = JObject.Parse(json)["access_token"].ToString();

var ewsClient = new ExchangeService();
ewsClient.Url = new Uri("https://outlook.office365.com/EWS/Exchange.asmx");
ewsClient.Credentials = new OAuthCredentials(aToken);
//Impersonate and include x-anchormailbox headers are required!
ewsClient.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, emailAddress);
ewsClient.HttpHeaders.Add("X-AnchorMailbox", emailAddress);
ewsClient.Timeout = 60000;
return ewsClient;
}
}
}
}
16 changes: 10 additions & 6 deletions Mail2Bug/Mail2Bug.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
Expand All @@ -9,12 +9,13 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Mail2Bug</RootNamespace>
<AssemblyName>Mail2Bug</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<RestorePackages>true</RestorePackages>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
Expand Down Expand Up @@ -58,9 +59,11 @@
<HintPath>..\packages\Microsoft.Azure.KeyVault.1.0.0\lib\net45\Microsoft.Azure.KeyVault.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Exchange.WebServices, Version=14.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Exchange.WebServices.1.2\lib\Microsoft.Exchange.WebServices.dll</HintPath>
<Reference Include="Microsoft.Exchange.WebServices, Version=15.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Exchange.WebServices.2.2\lib\40\Microsoft.Exchange.WebServices.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Exchange.WebServices.Auth, Version=15.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Exchange.WebServices.2.2\lib\40\Microsoft.Exchange.WebServices.Auth.dll</HintPath>
</Reference>
<Reference Include="Microsoft.IdentityModel.Clients.ActiveDirectory">
<HintPath>..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.16.204221202\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll</HintPath>
Expand Down Expand Up @@ -318,6 +321,7 @@
<Compile Include="Helpers\CredentialsHelper.cs" />
<Compile Include="Helpers\DisposeUtils.cs" />
<Compile Include="Helpers\DPAPIHelper.cs" />
<Compile Include="Helpers\EWSOAuthHelper.cs" />
<Compile Include="Helpers\FileUtils.cs" />
<Compile Include="InstanceRunner.cs" />
<Compile Include="MessageProcessingStrategies\DateBasedValueResolver.cs" />
Expand Down Expand Up @@ -378,4 +382,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
2 changes: 1 addition & 1 deletion Mail2Bug/packages.config
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<package id="Microsoft.Bcl" version="1.1.9" targetFramework="net45" />
<package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net45" />
<package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net45" />
<package id="Microsoft.Exchange.WebServices" version="1.2" targetFramework="net45" />
<package id="Microsoft.Exchange.WebServices" version="2.2" targetFramework="net48" />
<package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="2.16.204221202" targetFramework="net45" />
<package id="Microsoft.Net.Http" version="2.2.22" targetFramework="net45" />
<package id="Microsoft.TeamFoundationServer.Client" version="14.89.0" targetFramework="net45" />
Expand Down
7 changes: 4 additions & 3 deletions Mail2BugUnitTests/Mail2BugUnitTests.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
Expand All @@ -8,7 +8,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Mail2BugUnitTests</RootNamespace>
<AssemblyName>Mail2BugUnitTests</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
Expand All @@ -18,6 +18,7 @@
<TestProjectType>UnitTest</TestProjectType>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<RestorePackages>true</RestorePackages>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down Expand Up @@ -170,4 +171,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
4 changes: 2 additions & 2 deletions Tools/DpapiTool/DpapiTool.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
Expand All @@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>DpapiTool</RootNamespace>
<AssemblyName>DpapiTool</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
<RestorePackages>true</RestorePackages>
Expand Down