OAuth 2.0 for Microsoft Accounts (installed applications)

You can create a Windows application which can access Outlook.com, Hotmail.com or Live.com account of a user via IMAP and SMTP without knowing the password of this user.

.NET Core and UWP/UAP notes

Microsoft API for .NET Core does not support .NET Core and UWP. DotNetOpenAuth library was never ported to .NET Core or UWP.

Register Microsoft project

Create an application in Application Registration Portal. You may need to create an account there first.

For OAuth 2.0, you should create a Live SDK application. Click Add an app in Live SDK applications section.

On the next screen, click Add Platform and and select Mobile application (yes, we're writing console application, but that's OK), then save changes. The result will look like:

Application ID (Client ID) and password (application secret) are there as well.

Create application in Visual Studio

Now create a console application in Visual Studio. Alternatively, you can open OAuthConsoleApps sample project and set MicrosoftEasyLogin class as startup object in the project properties.

If you need WinForms version, check MicrosoftOAuthWinForms sample.

Then proceed to adding the required dependencies. One NuGet packager manager console and type:

Install-Package DotNetOpenAuth
Install-Package Newtonsoft.Json
Install-Package MailBee.NET

Alternatively, you can use Add Reference to plug MailBee.NET.45.dll to your project. It resides in Assemblies/Framework/Extensions in case if MailBee.NET Objects is fully installed in the system, or use Browse if you have just the .DLL file.

The sample does not use ExternalProviders library (like it's done in web apps version of OAuth 2.0 tutorial). The library uses ASP.NET Identity database as storage of access tokens and related data but we're not using that database in the sample for this article.

Using MailBee.NET and DotNetOpenAuth to access a Microsoft e-mail account

That's what we need to do in order to get access to the user's e-mails:

In subsequent topics we'll also add autodetection of the user's e-mail address, programmatic delivery of the authorization code from the browser to the application, and more. For now, let's stick to the simplest implementation.

using System;
using System.Collections.Specialized;
using System.Diagnostics;
using DotNetOpenAuth.OAuth2;
using MailBee;
using MailBee.ImapMail;

// This program shows how to get and use OAuth access token to check a user's inbox on Microsoft servers.
//
// To run this program, select "MicrosoftEasyLogin" in Project / project's Properties / Application tab / Startup object.
//
// What this sample does:
// - Gets OAuth access token from Microsoft OAuth server at https://login.live.com (works for outlook.com, hotmail.com,
// live.com and other services). Offline access is not requested, the access token received is not persisted.
// To get the token, it first starts the browser where the user is supposed to complete all the steps and get authorization code.
// After that, the user must copy/paste that code into the console of this app. Upon the successful authorization, the sample
// requests the access token from Microsoft server using the authorization code.
//
// - Checks the user's inbox via IMAP on outlook.com mail server using XOAUTH2 authentication in MailBee.NET Objects.
// outlook.com mail server can also be used for hotmail.com and live.com and other Microsoft domains.
//
// This sample is independent from OAuthMVC5 and ReadAspNetUsers samples, it does not store tokens anywhere
// (neither it reads them from any storage).
public class MicrosoftEasyLogin
{
	private static string GetAccessToken(string clientID, string clientSecret, string code)
	{
		UserAgentClient consumer;
		IAuthorizationState grantedAccess;

		StringDictionary parameters2 = new StringDictionary();
		parameters2.Add(OAuth2.RedirectUriKey, "https://login.live.com/oauth20_desktop.srf");

		AuthorizationServerDescription server = new AuthorizationServerDescription();
		server.AuthorizationEndpoint = new Uri("https://login.live.com/oauth20_authorize.srf");
		server.TokenEndpoint = new Uri("https://login.live.com/oauth20_token.srf");
		server.ProtocolVersion = ProtocolVersion.V20;

		consumer = new UserAgentClient(server, clientID, clientSecret);

		consumer.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(clientSecret);

		IAuthorizationState authorizationState = new AuthorizationState(null);

		if (parameters2.ContainsKey(OAuth2.RedirectUriKey))
		{
			authorizationState.Callback = new Uri(parameters2[OAuth2.RedirectUriKey]);
		}

		try
		{
			grantedAccess = consumer.ProcessUserAuthorization(
				new Uri("http://example.com/?code=" + code), authorizationState);
		}
		catch (DotNetOpenAuth.Messaging.ProtocolException e)
		{
			Console.WriteLine(e.Message);
			return null;
		}

		return grantedAccess.AccessToken;
	}

	public static void Main(string[] args)
	{
		// You get these at https://account.live.com/developers/applications/create
		string clientID = "Your Client ID, like 000000001A2B3456";
		string clientSecret = "Your Client Secret, like 0a1b2c-3d4e5f";

		// E-mail address can be of any MS domain: hotmail.com, outlook.com, live.com, etc. All MS services will work.
		// See MicrosoftLogin advanced sample on how to autodetect the e-mail address (in particular, GetUserData call)
		// and don't forget to change the scope from "wl.imap" to "wl.imap,wl.emails" at least.
		string userEmail = "user@outlook.com";

		OAuth2 myOAuth2 = new OAuth2(clientID, clientSecret);

		StringDictionary parameters1 = new StringDictionary();
		parameters1.Add(OAuth2.Scope, "wl.imap"); // Works for both IMAP and SMTP.
		parameters1.Add(OAuth2.RedirectUriKey, "https://login.live.com/oauth20_desktop.srf");
		parameters1.Add(OAuth2.ResponseTypeKey, "code");

		string url = myOAuth2.AuthorizeToken("https://login.live.com/oauth20_authorize.srf", parameters1);
		Process.Start(url);

		Console.Write("Please enter the authorization code: ");
		string authorizationCode = Console.ReadLine().Trim();

		if (string.IsNullOrEmpty(authorizationCode))
		{
			Console.WriteLine("No authorization code provided");
			return;
		}

		string accessToken = GetAccessToken(clientID, clientSecret, authorizationCode);
		if (accessToken == null)
		{
			Console.WriteLine("Can't get the access token");
			return;
		}

		string imapXOAuthKey = OAuth2.GetXOAuthKeyStatic(userEmail, accessToken);

		// Uncomment and set your key if you haven't specified it in app.config or Windows registry.
		// MailBee.Global.LicenseKey = "Your MNXXX-XXXX-XXXX key here";

		Imap imp = new Imap();

		// Logging is not necessary but useful for debugging.
		imp.Log.Filename = "C:\\Temp\\log.txt";
		imp.Log.HidePasswords = false;
		imp.Log.Enabled = true;
		imp.Log.Clear();

		imp.Connect("imap-mail.outlook.com", 993);
		imp.Login(null, imapXOAuthKey, AuthenticationMethods.SaslOAuth2, AuthenticationOptions.None, null);
		imp.SelectFolder("INBOX");

		Console.WriteLine(imp.MessageCount.ToString() + " message(s) in INBOX");

		imp.Disconnect();
	}
}
Imports System.Collections.Specialized
Imports System.Diagnostics
Imports DotNetOpenAuth.OAuth2
Imports MailBee
Imports MailBee.ImapMail

' This program shows how to get and use OAuth access token to check a user's inbox on Microsoft servers.
'
' To run this program, select "MicrosoftEasyLogin" in Project / project's Properties / Application tab / Startup object.
'
' What this sample does:
' - Gets OAuth access token from Microsoft OAuth server at https://login.live.com (works for outlook.com, hotmail.com,
' live.com and other services). Offline access is not requested, the access token received is not persisted.
' To get the token, it first starts the browser where the user is supposed to complete all the steps and get authorization code.
' After that, the user must copy/paste that code into the console of this app. Upon the successful authorization, the sample
' requests the access token from Microsoft server using the authorization code.
'
' - Checks the user's inbox via IMAP on outlook.com mail server using XOAUTH2 authentication in MailBee.NET Objects.
' outlook.com mail server can also be used for hotmail.com and live.com and other Microsoft domains.
'
' This sample is independent from OAuthMVC5 and ReadAspNetUsers samples, it does not store tokens anywhere
' (neither it reads them from any storage).
Public Class MicrosoftEasyLogin
	Private Shared Function GetAccessToken(clientID As String, clientSecret As String, code As String) As String
		Dim consumer As UserAgentClient
		Dim grantedAccess As IAuthorizationState

		Dim parameters2 As New StringDictionary()
		parameters2.Add(OAuth2.RedirectUriKey, "https://login.live.com/oauth20_desktop.srf")

		Dim server As New AuthorizationServerDescription()
		server.AuthorizationEndpoint = New Uri("https://login.live.com/oauth20_authorize.srf")
		server.TokenEndpoint = New Uri("https://login.live.com/oauth20_token.srf")
		server.ProtocolVersion = ProtocolVersion.V20

		consumer = New UserAgentClient(server, clientID, clientSecret)

		consumer.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(clientSecret)

		Dim authorizationState As IAuthorizationState = New AuthorizationState(Nothing)

		If parameters2.ContainsKey(OAuth2.RedirectUriKey) Then
			authorizationState.Callback = New Uri(parameters2(OAuth2.RedirectUriKey))
		End If

		Try
			grantedAccess = consumer.ProcessUserAuthorization(New Uri("http://example.com/?code=" & code), authorizationState)
		Catch e As DotNetOpenAuth.Messaging.ProtocolException
			Console.WriteLine(e.Message)
			Return Nothing
		End Try

		Return grantedAccess.AccessToken
	End Function

	Public Shared Sub Main(args As String())
		' You get these at https://account.live.com/developers/applications/create
		Dim clientID As String = "Your Client ID, like 000000001A2B3456"
		Dim clientSecret As String = "Your Client Secret, like 0a1b2c-3d4e5f"

		' E-mail address can be of any MS domain: hotmail.com, outlook.com, live.com, etc. All MS services will work.
		' See MicrosoftLogin advanced sample on how to autodetect the e-mail address (in particular, GetUserData call)
		' and don't forget to change the scope from "wl.imap" to "wl.imap,wl.emails" at least.
		Dim userEmail As String = "user@outlook.com"

		Dim myOAuth2 As New OAuth2(clientID, clientSecret)

		Dim parameters1 As New StringDictionary()
		parameters1.Add(OAuth2.Scope, "wl.imap") ' Works for both IMAP and SMTP.
		parameters1.Add(OAuth2.RedirectUriKey, "https://login.live.com/oauth20_desktop.srf")
		parameters1.Add(OAuth2.ResponseTypeKey, "code")

		Dim url As String = myOAuth2.AuthorizeToken("https://login.live.com/oauth20_authorize.srf", parameters1)
		Process.Start(url)

		Console.Write("Please enter the authorization code: ")
		Dim authorizationCode As String = Console.ReadLine().Trim()

		If String.IsNullOrEmpty(authorizationCode) Then
			Console.WriteLine("No authorization code provided")
			Return
		End If

		Dim accessToken As String = GetAccessToken(clientID, clientSecret, authorizationCode)
		If accessToken Is Nothing Then
			Console.WriteLine("Can't get the access token")
			Return
		End If

		Dim imapXOAuthKey As String = OAuth2.GetXOAuthKeyStatic(userEmail, accessToken)

		' Uncomment and set your key if you haven't specified it in app.config or Windows registry.
		' MailBee.Global.LicenseKey = "Your MNXXX-XXXX-XXXX key here"

		Dim imp As New Imap()

		' Logging is not necessary but useful for debugging.
		imp.Log.Filename = "C:\Temp\log.txt"
		imp.Log.HidePasswords = False
		imp.Log.Enabled = True
		imp.Log.Clear()

		imp.Connect("imap-mail.outlook.com", 993)
		imp.Login(Nothing, imapXOAuthKey, AuthenticationMethods.SaslOAuth2, AuthenticationOptions.None, Nothing)
		imp.SelectFolder("INBOX")

		Console.WriteLine(imp.MessageCount.ToString() & " message(s) in INBOX")

		imp.Disconnect()
	End Sub
End Class

This sample does not use async methods of MailBee.NET library. For async version, refer to MicrosoftOAuthWinForms sample. See Sample projects overview section for details.

Autodetecting the user's e-mail address

If you don't want to have the user to type their e-mail address manually, you can autodetect it, provided that you change the scope from wl.imap to wl.imap,wl.emails. You can also get other details of the user such as their name.

The below is the code snippet demonstrating how GetUserData method can be called, and the code snippet which implements the method itself and its supporting methods and types.

You may also need to add some using directives (Imports in VB) to make this code compile. Refer to OAuthConsoleApps\MicrosoftLogin.cs(vb) for the full list.

Usage

IDictionary<string, string> userData = GetUserData(authState.AccessToken);
userEmail = userData["email"];
Dim userData As IDictionary(Of String, String) = GetUserData(authState.AccessToken)
userEmail = userData("email")

Implementation

const string LiveGetProfileUri = "https://apis.live.net/v5.0/me?access_token=";

protected class ExtendedMicrosoftClientUserData
{
	public string FirstName { get; set; }
	public string Gender { get; set; }
	public string Id { get; set; }
	public string LastName { get; set; }
	public Uri Link { get; set; }
	public string Name { get; set; }
	public Emails Emails { get; set; }
}

protected class Emails
{
	public string Preferred { get; set; }
	public string Account { get; set; }
	public string Personal { get; set; }
	public string Business { get; set; }
}

private static readonly string[] UriRfc3986CharsToEscape = new string[] { "!", "*", "'", "(", ")" };
private static string EscapeUriDataStringRfc3986(string value)
{
	StringBuilder escaped = new StringBuilder(Uri.EscapeDataString(value));

	// Upgrade the escaping to RFC 3986, if necessary.
	for (int i = 0; i < UriRfc3986CharsToEscape.Length; i++)
	{
		escaped.Replace(UriRfc3986CharsToEscape[i], Uri.HexEscape(UriRfc3986CharsToEscape[i][0]));
	}

	// Return the fully-RFC3986-escaped string.
	return escaped.ToString();
}

// Inspired by http://answer.techwikihow.com/154458/getting-email-oauth-authentication-microsoft.html
// Be sure to have "wl.emails" in the requested scopes if you're using this method.
private static IDictionary<string, string> GetUserData(string accessToken)
{
	ExtendedMicrosoftClientUserData graph;
	WebRequest request =
		WebRequest.Create(LiveGetProfileUri + EscapeUriDataStringRfc3986(accessToken));
	using (WebResponse response = request.GetResponse())
	{
		using (Stream responseStream = response.GetResponseStream())
		{
			using (StreamReader sr = new StreamReader(responseStream))
			{
				string data = sr.ReadToEnd();
				graph = JsonConvert.DeserializeObject<ExtendedMicrosoftClientUserData>(data);
			}
		}
	}

	Dictionary<string, string> userData = new Dictionary<string, string>();
	userData.Add("id", graph.Id);
	userData.Add("username", graph.Name);
	userData.Add("name", graph.Name);
	userData.Add("link", graph.Link == null ? null : graph.Link.AbsoluteUri);
	userData.Add("gender", graph.Gender);
	userData.Add("firstname", graph.FirstName);
	userData.Add("lastname", graph.LastName);
	userData.Add("email", graph.Emails.Preferred);
	return userData;
}
Const LiveGetProfileUri As String = "https://apis.live.net/v5.0/me?access_token="

Protected Class ExtendedMicrosoftClientUserData
	Public Property FirstName() As String
		Get
			Return m_FirstName
		End Get
		Set(value As String)
			m_FirstName = Value
		End Set
	End Property
	Private m_FirstName As String
	Public Property Gender() As String
		Get
			Return m_Gender
		End Get
		Set(value As String)
			m_Gender = Value
		End Set
	End Property
	Private m_Gender As String
	Public Property Id() As String
		Get
			Return m_Id
		End Get
		Set(value As String)
			m_Id = Value
		End Set
	End Property
	Private m_Id As String
	Public Property LastName() As String
		Get
			Return m_LastName
		End Get
		Set(value As String)
			m_LastName = Value
		End Set
	End Property
	Private m_LastName As String
	Public Property Link() As Uri
		Get
			Return m_Link
		End Get
		Set(value As Uri)
			m_Link = Value
		End Set
	End Property
	Private m_Link As Uri
	Public Property Name() As String
		Get
			Return m_Name
		End Get
		Set(value As String)
			m_Name = Value
		End Set
	End Property
	Private m_Name As String
	Public Property Emails() As Emails
		Get
			Return m_Emails
		End Get
		Set(value As Emails)
			m_Emails = Value
		End Set
	End Property
	Private m_Emails As Emails
End Class

Protected Class Emails
	Public Property Preferred() As String
		Get
			Return m_Preferred
		End Get
		Set(value As String)
			m_Preferred = Value
		End Set
	End Property
	Private m_Preferred As String
	Public Property Account() As String
		Get
			Return m_Account
		End Get
		Set(value As String)
			m_Account = Value
		End Set
	End Property
	Private m_Account As String
	Public Property Personal() As String
		Get
			Return m_Personal
		End Get
		Set(value As String)
			m_Personal = Value
		End Set
	End Property
	Private m_Personal As String
	Public Property Business() As String
		Get
			Return m_Business
		End Get
		Set(value As String)
			m_Business = Value
		End Set
	End Property
	Private m_Business As String
End Class

Private Shared ReadOnly UriRfc3986CharsToEscape As String() = New String() {"!", "*", "'", "(", ")"}
Private Shared Function EscapeUriDataStringRfc3986(value As String) As String
	Dim escaped As New StringBuilder(Uri.EscapeDataString(value))

	' Upgrade the escaping to RFC 3986, if necessary.
	For i As Integer = 0 To UriRfc3986CharsToEscape.Length - 1
		escaped.Replace(UriRfc3986CharsToEscape(i), Uri.HexEscape(UriRfc3986CharsToEscape(i)(0)))
	Next

	' Return the fully-RFC3986-escaped string.
	Return escaped.ToString()
End Function

' Inspired by http://answer.techwikihow.com/154458/getting-email-oauth-authentication-microsoft.html
' Be sure to have "wl.emails" in the requested scopes if you're using this method.
Private Shared Function GetUserData(accessToken As String) As IDictionary(Of String, String)
	Dim graph As ExtendedMicrosoftClientUserData
	Dim request As WebRequest = WebRequest.Create(LiveGetProfileUri & EscapeUriDataStringRfc3986(accessToken))
	Using response As WebResponse = request.GetResponse()
		Using responseStream As Stream = response.GetResponseStream()
			Using sr As New StreamReader(responseStream)
				Dim data As String = sr.ReadToEnd()
				graph = JsonConvert.DeserializeObject(Of ExtendedMicrosoftClientUserData)(data)
			End Using
		End Using
	End Using

	Dim userData As New Dictionary(Of String, String)()
	userData.Add("id", graph.Id)
	userData.Add("username", graph.Name)
	userData.Add("name", graph.Name)
	userData.Add("link", If(graph.Link Is Nothing, Nothing, graph.Link.AbsoluteUri))
	userData.Add("gender", graph.Gender)
	userData.Add("firstname", graph.FirstName)
	userData.Add("lastname", graph.LastName)
	userData.Add("email", graph.Emails.Preferred)
	Return userData
End Function

Note that it's not required to get the e-mail address of the user on every run of the application for the same user. The e-mail address of the user will never change so you can save the result once and then re-use on future runs.

Using Offline mode and refresh tokens

Microsoft access tokens are short-lived, their lifetime is just 1 hour. We can enable "Offline mode" scope when requesting authorization and get the refresh token in addition to the access token. After that, the access token can then be renewed without user interaction.

To persist access/refresh token data between sessions, any kind of data storage can be used. For instance, JSON-serialized text file. We need to implement this persistence manually as DotNetOpenAuth library does not provide any means for that (unlike Google APIs).

Instead of GetAccessToken method from the original sample, let's introduce GetOrRefreshAccessToken.

// Gets access and refresh token if grantedAccess is null, and returns the token data on success or null on failure.
// Refreshes the access token using the refresh token if grantedAccess is not null, and returns the token data (also
// modifying the input parameter grantedAccess) on successful renewal or null if the renewal is not required yet.
private static AuthorizationState GetOrRefreshAccessToken(
	string clientID, string clientSecret, string code, AuthorizationState grantedAccess)
{
	UserAgentClient consumer;

	// Get a new token or refresh an existing token?
	bool getMode = (grantedAccess == null);

	StringDictionary parameters2 = new StringDictionary();
	parameters2.Add(OAuth2.RedirectUriKey, LiveDesktopRedirectUri);

	AuthorizationServerDescription server = new AuthorizationServerDescription();
	if (getMode)
	{
		server.AuthorizationEndpoint = new Uri(LiveAuthorizationUri);
	}
	server.TokenEndpoint = new Uri(LiveTokenEndpoint);
	server.ProtocolVersion = ProtocolVersion.V20;

	consumer = new UserAgentClient(server, clientID, clientSecret);

	// Not needed in our tests, uncomment the next line if for some reason it's required in your case.
	// consumer.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(clientSecret);

	if (getMode)
	{
		grantedAccess = new AuthorizationState(null);
		if (parameters2.ContainsKey(OAuth2.RedirectUriKey))
		{
			grantedAccess.Callback = new Uri(parameters2[OAuth2.RedirectUriKey]);
		}
		return (AuthorizationState)consumer.ProcessUserAuthorization(
			new Uri("http://example.com/?code=" + code), grantedAccess);
	}
	else
	{
		return consumer.RefreshAuthorization(grantedAccess, TimeSpan.FromMinutes(1)) ? grantedAccess : null;
	}
}
' Gets access and refresh token if grantedAccess is null, and returns the token data on success or null on failure.
' Refreshes the access token using the refresh token if grantedAccess is not null, and returns the token data (also
' modifying the input parameter grantedAccess) on successful renewal or null if the renewal is not required yet.
Private Shared Function GetOrRefreshAccessToken(clientID As String, clientSecret As String, code As String, _
	grantedAccess As AuthorizationState) As AuthorizationState

	Dim consumer As UserAgentClient

	' Get a new token or refresh an existing token?
	Dim getMode As Boolean = (grantedAccess Is Nothing)

	Dim parameters2 As New StringDictionary()
	parameters2.Add(OAuth2.RedirectUriKey, LiveDesktopRedirectUri)

	Dim server As New AuthorizationServerDescription()
	If getMode Then
		server.AuthorizationEndpoint = New Uri(LiveAuthorizationUri)
	End If
	server.TokenEndpoint = New Uri(LiveTokenEndpoint)
	server.ProtocolVersion = ProtocolVersion.V20

	consumer = New UserAgentClient(server, clientID, clientSecret)

	' Not needed in our tests, uncomment the next line if for some reason it's required in your case.
	' consumer.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(clientSecret);

	If getMode Then
		grantedAccess = New AuthorizationState(Nothing)
		If parameters2.ContainsKey(OAuth2.RedirectUriKey) Then
			grantedAccess.Callback = New Uri(parameters2(OAuth2.RedirectUriKey))
		End If
		Return DirectCast(consumer.ProcessUserAuthorization(New Uri("http://example.com/?code=" & code), grantedAccess),  _
			AuthorizationState)
	Else
		Return If(consumer.RefreshAuthorization(grantedAccess, TimeSpan.FromMinutes(1)), grantedAccess, Nothing)
	End If
End Function

You can use this method like below:

AuthorizationState authState;
string tokenData;
bool mustSaveToken = true;
tokenData = File.ReadAllText(tokenFile);
authState = JsonConvert.DeserializeObject<AuthorizationState>(tokenData);
if (GetOrRefreshAccessToken(clientID, clientSecret, null, authState) != null)
{
	Console.WriteLine("Refreshing the token completed");
}
else
{
	Console.WriteLine("Refreshing the token is not required yet");
	mustSaveToken = false;
}
if (mustSaveToken)
{
	tokenData = JsonConvert.SerializeObject(authState);
	File.WriteAllText(tokenFile, tokenData);
}
Dim authState As AuthorizationState
Dim tokenData As String
Dim mustSaveToken As Boolean = True
tokenData = File.ReadAllText(tokenFile)
authState = JsonConvert.DeserializeObject(Of AuthorizationState)(tokenData)
If GetOrRefreshAccessToken(clientID, clientSecret, Nothing, authState) IsNot Nothing Then
	Console.WriteLine("Refreshing the token completed")
Else
	Console.WriteLine("Refreshing the token is not required yet")
	mustSaveToken = False
End If
If mustSaveToken Then
	tokenData = JsonConvert.SerializeObject(authState)
	File.WriteAllText(tokenFile, tokenData)
End If

We read the stored access token data from a file (tokenFile must contain its filename) and pass it as authState object to GetOrRefreshAccessToken method which updates it if necessary. If the method returned null (Nothing in VB), the refresh is not needed and the method did nothing. Otherwise, the token data in authState object has been changed and the app must update the access token file to save the changes.

After that, authState.AccessToken string can be used to pass OAuth 2.0 authentication against Microsoft server (just like in the previous samples).

Automating authorization code delivery from the browser to your app

The original sample simply started the browser prompting the user to copy/paste the authorization code from the web page to the app manually. There are methods to automate this.

Unlike Google APIs library which provides the built-in mechanism of authorization code delivery to the app, DotNetOpenAuth has no means for that. What options do we have?

Pros and cons:

WebBrowser approach seems to be a better alternative:

If you're creating an app which will support both Microsoft and Google, you need to take care of Microsoft only regarding this issue. Google API hides authorization codes in its internals and the app receives access token right away.

Unfortunately, it's not possible to simulate Google method of delivering authorization token to the application when dealing with Microsoft OAuth provider. Google API internally starts a local web server on localhost domain and Google web site redirects the browser there once the user completes the authorization process. But Microsoft does not allow for localhost server name. You'd need to run it on a custom domain name which would require to run your app as Administrator or perform specific Windows configuration (also under Administrator).

Obtaining authorization code with WebBrowser control

You can display a form even in a console app, although it requires a bit more coding than in WinForms app (MicrosoftOAuthWinForms is an example).

The main method is RunWebBrowserFormAndGetCode. In a console app, you need to run in on STA thread (WebBrowser control limitation), so there is StartTaskAsSTAThread helper method.

And be sure to add reference to System.Windows.Forms.

Usage

string authorizationCode = StartTaskAsSTAThread(() => RunWebBrowserFormAndGetCode(url)).Result;
Dim authorizationCode As String = StartTaskAsSTAThread(Function() RunWebBrowserFormAndGetCode(url)).Result

Implementation

// Provides means to display OAuth authorization page on Microsoft web server using WebBrowser control. The control loads
// that page, the user authenticates on Microsoft web server if necessary, and grants authorization to our app. Each step
// involves a browser redirect. We monitor DocumentCompleted events to determine out the moment when "code" parameter appears
// in the current URL and return it to the caller.
//
// Pros: WebBrowser control is always available.
// Cons: the user will have to log in their Microsoft account in IE as it's the only browser supported by WebBrowser control.
private static string RunWebBrowserFormAndGetCode(string url)
{
	string code = null;

	Form webBrowserForm = new Form();
	webBrowserForm.WindowState = FormWindowState.Maximized;
	WebBrowser webBrowser = new WebBrowser();
	webBrowser.Dock = DockStyle.Fill;
	webBrowser.Url = new Uri(url);

	webBrowserForm.Controls.Add(webBrowser);

	WebBrowserDocumentCompletedEventHandler documentCompletedHandler = (s, e) =>
	{
		string[] parts = webBrowser.Url.Query.Split(new char[] { '?', '&' });
		foreach (string part in parts)
		{
			if (part.StartsWith("code="))
			{
				code = part.Split('=')[1];
				webBrowserForm.Close();
			}
			else if (part.StartsWith("error="))
			{
				webBrowserForm.Close();
			}
		}
	};

	webBrowser.DocumentCompleted += documentCompletedHandler;
	Application.Run(webBrowserForm);
	webBrowser.DocumentCompleted -= documentCompletedHandler;

	return code;
}

// We must run WebBrowser control in a separate STA thread (otherwise, it just wouldn't work in a console app),
// but it's not required for Forms-based apps (there you'd simply place it on the form and that't all).
private static Task<T> StartTaskAsSTAThread<T>(Func<T> taskFunc)
{
	TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
	Thread thread = new Thread(() =>
	{
		try
		{
			tcs.SetResult(taskFunc());
		}
		catch (Exception e)
		{
			tcs.SetException(e);
		}
	});
	thread.SetApartmentState(ApartmentState.STA);
	thread.Start();
	return tcs.Task;
}
' Provides means to display OAuth authorization page on Microsoft web server using WebBrowser control. The control loads
' that page, the user authenticates on Microsoft web server if necessary, and grants authorization to our app. Each step
' involves a browser redirect. We monitor DocumentCompleted events to determine out the moment when "code" parameter appears
' in the current URL and return it to the caller.
'
' Pros: WebBrowser control is always available.
' Cons: the user will have to log in their Microsoft account in IE as it's the only browser supported by WebBrowser control.
Private Shared Function RunWebBrowserFormAndGetCode(url As String) As String
	Dim code As String = Nothing

	Dim webBrowserForm As New Form()
	webBrowserForm.WindowState = FormWindowState.Maximized
	Dim webBrowser As New WebBrowser()
	webBrowser.Dock = DockStyle.Fill
	webBrowser.Url = New Uri(url)

	webBrowserForm.Controls.Add(webBrowser)

	Dim documentCompletedHandler As WebBrowserDocumentCompletedEventHandler = _
		Sub(s, e)
			Dim parts As String() = webBrowser.Url.Query.Split(New Char() {"?", "&"})
			For Each part As String In parts
				If part.StartsWith("code=") Then
					code = part.Split("=")(1)
					webBrowserForm.Close()
				ElseIf part.StartsWith("error=") Then
					webBrowserForm.Close()
				End If
			Next
		End Sub

	AddHandler webBrowser.DocumentCompleted, documentCompletedHandler
	Application.Run(webBrowserForm)
	RemoveHandler webBrowser.DocumentCompleted, documentCompletedHandler

	Return code
End Function

' We must run WebBrowser control in a separate STA thread (otherwise, it just wouldn't work in a console app),
' but it's not required for Forms-based apps (there you'd simply place it on the form and that't all).
Private Shared Function StartTaskAsSTAThread(Of T)(taskFunc As Func(Of T)) As Task(Of T)
	Dim tcs As New TaskCompletionSource(Of T)()
	Dim thread As New Thread( _
		Sub()
			Try
				tcs.SetResult(taskFunc())
			Catch e As Exception
				tcs.SetException(e)
			End Try
		End Sub)
	thread.SetApartmentState(ApartmentState.STA)
	thread.Start()
	Return tcs.Task
End Function

Note that in native WinForms apps, StartTaskAsSTAThread method is not needed. MicrosoftOAuthWinForms sample project demonstrates using RunWebBrowserFormAndGetCode method directly.

Obtaining authorization code using the default browser

In case if you still think WebBrowser control approach does not suit your needs, you can use the alternate approach of running the system's default browser.

You can explore the full code #region "DefaultBrowser" section in OAuthConsoleApps\MicrosoftLogin.cs(vb) file to see how process monitoring can be implemented. Here, we just list the main method to let you get the idea.

private static async Task<string> RunDefaultBrowserProcessAndGetCodeAsync(string url, string timestamp, CancellationToken cancelToken)
{
	// Opens the default browser to start OAuth authorization procedure. It will eventually redirect to the URL like
	// https://login.live.com/oauth20_desktop.srf?code=c9afcb5f-fd66-4fec-49b4-9194f5a074d5&lc=1033
	// The user will need to copy/paste c9afcb5f-fd66-4fec-49b4-9194f5a074d5 code in case if we couldn't get it from the
	// browser's process name (Firefox is problematic as it does not send the process name from the current URL
	// while IE and Chrome are fine with that).
	// The side effect could occur if the user changed the current browser tab to another tab which already had
	// https://login.live.com/oauth20_desktop.srf URL for some reason (like previous OAuth authorization attempts). This sample
	// could take it as the authorization code source. To locate the browser tab which was opened by the current OAuth attempt
	// (among multiple opened tabs with https://login.live.com/oauth20_desktop.srf URL) we use timestamp parameter. In your apps
	// you can use any other unique string which is unlikely to occur in other https://login.live.com/oauth20_desktop.srf windows.
	Process.Start(url);

	while (!cancelToken.IsCancellationRequested)
	{
		await Task.Delay(1000);
		Process[] processes = Process.GetProcesses();
		foreach (Process proc in processes)
		{
			string title = proc.MainWindowTitle;
			if (!String.IsNullOrEmpty(title))
			{
				if (title.IndexOf(LiveDesktopRedirectUri) > -1 && title.IndexOf(timestamp) > -1)
				{
					string[] parts = title.Split(new char[] { '?', '&' });
					foreach (string part in parts)
					{
						if (part.StartsWith("code="))
						{
							return part.Split('=')[1];
						}
						else if (part.StartsWith("error="))
						{
							return null;
						}
					}
				}
			}
		}
	}

	return null;
}
Private Shared Async Function RunDefaultBrowserProcessAndGetCodeAsync( _
	url As String, timestamp As String, cancelToken As CancellationToken) As Task(Of String)

	' Opens the default browser to start OAuth authorization procedure. It will eventually redirect to the URL like
	' https://login.live.com/oauth20_desktop.srf?code=c9afcb5f-fd66-4fec-49b4-9194f5a074d5&lc=1033
	' The user will need to copy/paste c9afcb5f-fd66-4fec-49b4-9194f5a074d5 code in case if we couldn't get it from the
	' browser's process name (Firefox is problematic as it does not send the process name from the current URL
	' while IE and Chrome are fine with that).
	' The side effect could occur if the user changed the current browser tab to another tab which already had
	' https://login.live.com/oauth20_desktop.srf URL for some reason (like previous OAuth authorization attempts). This sample
	' could take it as the authorization code source. To locate the browser tab which was opened by the current OAuth attempt
	' (among multiple opened tabs with https://login.live.com/oauth20_desktop.srf URL) we use timestamp parameter. In your apps
	' you can use any other unique string which is unlikely to occur in other https://login.live.com/oauth20_desktop.srf windows.
	Process.Start(url)

	While Not cancelToken.IsCancellationRequested
		Await Task.Delay(1000)
		Dim processes As Process() = Process.GetProcesses()
		For Each proc As Process In processes
			Dim title As String = proc.MainWindowTitle
			If Not String.IsNullOrEmpty(title) Then
				If title.IndexOf(LiveDesktopRedirectUri) > -1 AndAlso title.IndexOf(timestamp) > -1 Then
					Dim parts As String() = title.Split(New Char() {"?", "&"})
					For Each part As String In parts
						If part.StartsWith("code=") Then
							Return part.Split("=")(1)
						ElseIf part.StartsWith("error=") Then
							Return Nothing
						End If
					Next
				End If
			End If
		Next
	End While

	Return Nothing
End Function

As you can see, this method also utilizes "state" parameter in OAuth URLs. Unlike WebBrowser control which is fully controlled by our app, running the default browser does not guarantee the user won't change the current tab (making it active) and this tab won't have the same URL like the one we need (for instance, if the user recently authorized our app and didn't close that browser tab). We need to somehow make sure that the currently opened browser tab was indeed initiated by our app. Moreover, by the current run of our app. For that, we set "state" parameter to some timestamp and then compare the browser process name (i.e. URL) if it contains that timestamp.

OAuthConsoleApps\MicrosoftLogin.cs(vb) also shows some techniques to let the user bypass the standard method of waiting for the authorization code in the process name and type it directly in the console. For instance, if the current browser does not set the process name from the URL, the user will still have an option to copy/paste the code manually. This means the app must support two input sources simultaneously - from the browser process and from console input.

For instance, the sample shows how to stop waiting for console input in case if the result became available by other means (from the process name in our case). The sample does this by sending [Enter] keypress event to the console.

Revoking Microsoft access token

Unlike Google, Microsoft does not provide programmatic methods of revoking access tokens yet. The user must do that manually. See Obtaining User Consent for details.

Sample projects overview

All the code in this tutorial is available in MicrosoftEasyLogin and MicrosoftLogin classes of OAuthConsoleApps project. WinForms version is available in FormMain class of MicrosoftOAuthWinForms project.

All samples are available in both C# and VB versions.

MicrosoftEasyLogin provides the very basic means of using Microsoft OAuth provider to check the user's mailbox via IMAP.

MicrosoftLogin adds autodetection of the e-mail address, persistent storage of access token data, Offline mode and refresh tokens, automated detection of the authorization code via detecting it in the browser URL(manual user input is still available), sending e-mail via SMTP.

FormMain class MicrosoftOAuthWinForms sample has most features of MicrosoftLogin class of OAuthConsoleApps, with the following differences:

Get source code

All C# and VB samples discussed in this article (MicrosoftOAuthWinForms and OAuthConsoleApps) are shipped with MailBee.NET Objects installer and get installed into My Documents\MailBee.NET Objects\Samples\WinForms\NET 4.5 OAuth folder.


Send feedback to AfterLogic

Copyright © 2006-2017 AfterLogic Corporation. All rights reserved.