Tuesday, 2 June 2015

OAuth in Winforms

The junior dev I am mentoring at my job was recently given the task of extending his knowledge of APIs and asked to demonstrate his newly gained knowledge by producing an application that consumes a third party API. More specifically a RESTful API.

In itself this does not seem the most taxing of tasks, but bear in mind that the junior dev has no web development experience, his only dev exposure in his current role has been on a winforms application. So to make this a targeted task, aimed at learning about RESTful APIs, it was decided a simple winforms application using the 4 main verbs would demonstrate sufficient understanding.

This however did raise a question, how to interact with an OAuth provider from a winforms application.  This should be a simple matter, but it is something that is not well documented, especially in the documentation of most of the more famous APIs.  There are plenty of tutorials for how to authenticate with an OAuth provider from a web site, and most of the APIs the junior dev looked at provided their own OAuth.

The final choice of the API to consume was Instagram, which provides great documentation for its OAuth when being consumed in a web site, but nothing for Winforms.  This is not surprising, Winforms is an old technology, not something that you would expect to be used with a service like Intstagram, but why not?  It should be possible (and is). But it is understandable that Instagram have not invested time in providing detailed documentation on how to do this.  So here we go on how it was accomplished:

Firstly, the method of validating the user's claim of access to Instagram is via a web page hosted by Instagram.  The documentation states
which is fairly straightforward in a web app, but how do you do this in a winform application?

The answer is to host a web browser control within your application which will display the url above and be redirected upon completion of the authorization process.  We found some code with a quick trawl of the search engines to perform this action in a pop up window:

 string authorizationCode = StartTaskAsSTAThread(() => RunWebBrowserFormAndGetCode()).Result;
                 
           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;  
           }  
           private static string RunWebBrowserFormAndGetCode()  
           {  
                Form webBrowserForm = new Form();  
                WebBrowser webBrowser = new WebBrowser();  
                webBrowser.Dock = DockStyle.Fill;  
                var uri = new Uri(@"https://api.instagram.com/oauth/authorize/?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code");  
                webBrowser.Url = uri;  
                webBrowserForm.Controls.Add(webBrowser);  
                string code = null;  
                WebBrowserDocumentCompletedEventHandler documentCompletedHandler = (s, e1) =>  
                {  
                     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="))  
                          {  
                               Debug.WriteLine("error");  
                          }  
                     }  
                };  
                webBrowser.DocumentCompleted += documentCompletedHandler;                 
                Application.Run(webBrowserForm);  
                webBrowser.DocumentCompleted -= documentCompletedHandler;  
                return code;  
           }  
which gets you the code included in the redirect URL.  The CLIENT_ID you need to get from Instagram, register an application with them and it will be provided, and the REDIRECT_URL must match, but the location is unimportant, as the web browser will close on completion it will not be seen.

There is still one problem, when using Fiddler to inspect the successful API calls from the test bed of Apigee the access token has three parts period delimited, the user_id, an unknown part, and the code returned from the OAuth authentication stage.  This is not well documented, and at this stage we are unable to generate the full access token.

All this highlights that the documentation of even well established APIs can at times be lacking for non-standard development paths.