This guide is about INTERACTIVE flow where the e-mail account owner gives their consent to let your app access their mailbox. If you want NON-INTERACTIVE authentication (in the manner of login/password authentication which is no longer supported by Office 365), see Office 365 accounts for non-interactive applications guide.
This is the original version of the tutorial. It does not support .NET Core (only .NET Framework is supported) and does not support authorization token exchange via a local web server (which is often the most convenient method possible). The revised application is explained in OAuth 2.0 for Microsoft Accounts (installed applications running HttpListener) topic.
The idea is to 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.
This is a .NET Framework version of this guide (it uses DotNetOpenAuth library which exists for .NET Framework only). If you're on .NET Core, see this version. Universal Windows sample is explained in OAuth 2.0 for Universal Windows apps guide.
Create an application in Application Registration Portal. You may need to create an account there first.
For OAuth 2.0, create a Live SDK application. Click Add an app in Live SDK applications section.
On the next screen, click Add Platform and select Native application, then save changes.
You may also add Web platform as well. This is not required by gives you the ability to use the simplified approach for getting OAuth authorization code from Microsoft server (so-called local server, see below).
Application ID (Client ID) and password (application secret) are there as well.
Make sure to add the required permissions (scopes) as well:
Microsoft is changing things there often. Sometimes, Live SDK app simply doesn't work and you may try creating a Converged application. Also, you may be asked to use Azure portal instead of apps.dev.microsoft.com site. At the moment of writing, Azure portal was not stable enough for that.
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 package 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.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.
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.
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.
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
.
In case if you're getting error 400 from Microsoft server during
consumer.RefreshAuthorization
call, you'll have to disable chunked transfer encoding when making HTTP requests to the Microsoft server. This is clearly a bug in their service and the only known workaround is to modify DotNetOpenAuth library for that (more exactly, DotNetOpenAuth.OAuth.Common.dll). You can find it on Github. The file to be updated is DotNetOpenAuth.OAuth.Common\OAuth\DefaultOAuthHostFactories.cs. InCreateHttpClient(HttpMessageHandler handler)
method, addclient.DefaultRequestHeaders.TransferEncodingChunked = false;
.
// 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).
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?
Form
, put WebBrowser
control there (actually, Internet Explorer), navigate it to Microsoft OAuth authorization page, let the user complete the authorization so that "code" parameter finally appears in the URL, and grab that code.Pros and cons:
WebBrowser
control, the user will have to fully log in their Microsoft account, including typing the e-mail address (not very convenient). This is because WebBrowser
control environment is not the same as the default browser's environment where the user may have already been logged in. However, this method always works.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 the access token right away.
Actually, it's also 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. The same approach can be used with Microsoft as well. See LocalServer version for details.
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.
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.
Unlike Google APIs library, there is no built-in method of revoking refresh tokens yet in DotNetOpenAuth library. However, you can make a direct REST query for that. See Revoke refresh tokens section in Manage access tokens for API requests for details.
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:
MicrosoftLogin
provides 2 methods of getting the authorization code: with WebBrowser
control and launching the system's default browser in a separate process. FormMain
class only offers WebBrowser
control as the natural method for WinForms apps.FormMain
class uses async methods which don't block UI during send e-mail or check e-mail operations with MailBee.NET Objects. Most of the operations can be canceled by hitting ESC or closing the app.
OAuth calls involving DotNetOpenAuth
library are still sync-only due to the lack of async versions in the current version of DotNetOpenAuth
(can be worked around by putting the entire thing in a worker thread, however).
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.
Copyright © 2006-2024 AfterLogic Corporation. All rights reserved.