Поделиться через


Creating a Fiddler Extension for SharePoint 2013 App Tokens

This post will show how to create a Fiddler extension to inspect SharePoint 2013 context and access tokens.

 

[ Update 11/21/2013: Andrew Connell was nice enough to post this project to GitHub to allow the community to make contributions to the project. Visit his blog for more information on contributing!]

Overview

In a previous post, I showed how to inspect a SharePoint 2013 context token.  While working with apps, I frequently demonstrate how the context, refresh, and access tokens work.  I realized that it might be helpful to see the actual tokens while working with an app.  Instead of writing more code in my web page to show the information, I decided to use Fiddler instead.  The result is a handy extension for Fiddler that SharePoint 2013 app builders can use to inspect the context and access tokens for your apps for provider hosted apps using either a client secret or a certificate.

image

Creating the Extension

The first step is to create an extension for Fiddler using the Inspector2 and IRequestInspector2 interfaces.  These will simply give you the HTTP headers and HTTP body.  I also created a Windows Forms control called “SPOAuthRequestControl” that contains a textbox and a DataGridView.

To start with, I created a new Windows Forms User Control project and added references to System.Web, System.Web.Extensions, and a reference to Fiddler.exe.

image

Next, we add a class for the Fiddler extension.

 

 using System;
using System.Windows.Forms;
using Fiddler;

[assembly: Fiddler.RequiredVersion("4.4.5.1")]

namespace MSDN.Samples.SharePoint.OAuth
{
    public class SPOAuthExtension : Inspector2, IRequestInspector2    
    {

        private bool _readOnly;
        HTTPRequestHeaders _headers;
        private byte[] _body;
        SPOAuthRequestControl _displayControl;

        #region Inspector2 implementation
        public override void AddToTab(TabPage o)
        {
            _displayControl = new SPOAuthRequestControl();
            o.Text = "SPOAuth";
            o.Controls.Add(_displayControl);
            o.Controls[0].Dock = DockStyle.Fill;
        }

        public override int GetOrder()
        {
            return 0;
        }
        #endregion


        #region IRequestInspector2 implementation
        public HTTPRequestHeaders headers
        {
            get
            {
                return _headers;
            }
            set
            {
                _headers = value;
                System.Collections.Generic.Dictionary<string, string> httpHeaders = 
                    new System.Collections.Generic.Dictionary<string, string>();
                foreach (var item in headers)
                {
                    httpHeaders.Add(item.Name, item.Value);
                }
                _displayControl.Headers = httpHeaders;
            }
        }

        public void Clear()
        {
            _displayControl.Clear();
        }

        public bool bDirty
        {
            get { return false; }
        }

        public bool bReadOnly
        {
            get
            {
                return _readOnly;
            }
            set
            {
                _readOnly = value;
            }
        }

        public byte[] body
        {
            get
            {
                return _body;
            }
            set
            {
                _body = value;
                _displayControl.Body = body;                
            }
        }
        #endregion

    }

}

Creating the User Control

The next step is to create the Windows Forms user control.  I used a textbox and a DataGridView.  The DataGridView has two columns to show the key and value.  Here is the design surface:

image

The code for the control is pretty basic.  There is a property, Headers, and another property, Body.  When the Headers property is set, we look for an HTTP header with the name Authorization.  If we see one, then we strip out the value “Bearer “ and that gives us the UTF8 encoded access token.  We use a helper class, JsonWebToken, to decode and deserialize. 

 using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Web.Script.Serialization;


namespace MSDN.Samples.SharePoint.OAuth
{
    public partial class SPOAuthRequestControl : UserControl
    {
        public SPOAuthRequestControl()
        {
            InitializeComponent();

        }

        public Dictionary<string, string> Headers
        {
            set
            {
                txtContext.Text = string.Empty;
                foreach (string key in value.Keys)
                {
                    if (key == "Authorization")
                    {
                        //Access token
                        string accessToken = value[key].Replace("Bearer ", string.Empty);
                        string token = JsonWebToken.Decode(accessToken)[1];
                        txtContext.Text = token;
                        var dictionary = JsonWebToken.Deserialize(token);

                        PopulateGrid(dictionary);    
                                                                   
                    }
                }
            }
        }

        public byte[] Body
        {            
            set
            {                                
                bool found = false;
                if (null != value)
                {
                    string ret = System.Text.Encoding.UTF8.GetString(value);

                    //Context token may be sent as a querystring with these names, but this is not
                    //recommended practice and is not implemented here as a result. Only POST values
                    //are processed.
                    string[] formParameters = ret.Split('&');
                    string[] paramNames = { "AppContext", "AppContextToken", "AccessToken", "SPAppToken" };

                    foreach (string valuePair in formParameters)
                    {
                        string[] formParameter = valuePair.Split('=');
                        foreach (string paramName in paramNames)
                        {
                            if (formParameter[0] == paramName)
                            {                                
                                //Decode header of JWT token
                                string tokenHeader = JsonWebToken.Decode(formParameter[1])[0];
                                txtContext.Text = tokenHeader;

                                //Decode body of JWT token
                                string tokenBody = JsonWebToken.Decode(formParameter[1])[1];
                                txtContext.Text += tokenBody;
                                
                                var dictionary = JsonWebToken.Deserialize(tokenBody);
                                
                                PopulateGrid(dictionary);                                                                     

                                found = true;
                                break;
                            }
                        }
                        if (found)
                            break;
                    }                        
                }

            }
        }



        public void Clear()
        {
            txtContext.Text = string.Empty;

        }

        private void PopulateGrid(IReadOnlyDictionary<string,object> dictionary)
        {
            dataGridView1.Rows.Clear();

            foreach (string key in dictionary.Keys)
            {
                if (key == "nbf" || key == "exp")
                {
                    double d = double.Parse(dictionary[key].ToString());
                    DateTime dt = new DateTime(1970, 1, 1).AddSeconds(d);
                    dataGridView1.Rows.Add(key, dt);
                }
                else
                {
                    dataGridView1.Rows.Add(key, dictionary[key]);
                }
            }     
        }
    }
}

You can see there’s not much to the code other than how we find the context or access tokens.  Once we have them, we get the data and then populate the data in the grid.  The PopulateGrid calculates the nbf (not before) and exp (expires) values according to the number of seconds since January 1st, 1970.  For more information, see Tips and FAQs: OAuth and remote apps for SharePoint 2013

The JsonWebToken Helper Class

The JsonWebToken helper class provides methods to decode and deserialize the JWT token.  The JWT token may have multiple parts separated by “.”, which is why we use the Split function to split the header and body.  Once we have the header and body, we next need to make sure the token is the correct length by padding with “=” characters.  Finally, the Deserialize method will just create an IReadOnlyDictionary<string,object> so that we can easily access the data in the JWT token.

 

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web.Script.Serialization;

namespace MSDN.Samples.SharePoint.OAuth
{
    public class JsonWebToken
    {
        public static string[] Decode(string token)
        {

            var parts = token.Split('.');
            string[] ret = new string[2];

            var header = parts[0];
            var payload = parts[1];
            
            var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
            ret[0] = headerJson;

            string payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
            ret[1] = payloadJson;
            
            return ret;
        }

        public static IReadOnlyDictionary<string, object> Deserialize(string token)
        {
            JavaScriptSerializer ser = new JavaScriptSerializer();
            var dict = ser.Deserialize<dynamic>(token);
            return dict;
        }

        
        private static byte[] Base64UrlDecode(string value)
        {
            string convert = value;

            switch (convert.Length % 4) 
            {
                case 0:                     
                    break; 
                case 2: 
                    convert += "=="; 
                    break; 
                case 3: 
                    convert += "="; 
                    break; 
                default: 
                    throw new System.Exception("oops");
            }
            var ret = Convert.FromBase64String(convert); 
            return ret;
        }

    }

}

We now have 3 classes:  the extension, the user control class, and the helper class.  That’s all we need in order to inspect the traffic in Fiddler and to provide a different visualization of the data.

Debugging the Extension

To debug the extension, I created post-build events to copy the .DLL to the Scripts and Inspectors folders for Fiddler.

image

I then set the debug start action to start Fiddler.  Now, when I hit F5, the assemblies are automatically copied and Fiddler starts.

image

 

The Result

I tested this with both an S2S provider hosted app and a provider hosted app that uses a client secret.  First, let’s look at the provider hosted app that uses S2S.  I am showing the access token here because in the S2S model there is not a context token, you only have the access token.  Click on the request to _vti_bin/client.svc and you will now see the details of the access token, including an easier to read nbf and exp value.

 

image

Next, here is an app that uses a client secret.  In this model, we are looking at the context token, which includes the refresh token.  Note again that we can easily read the values of nbf and exp to see when the context token expires. 

image

As you can see, this can be tremendously valuable in debugging your apps while trying to determine any possible causes for 401 unauthorized issues, such as possibly an expired access token or an incorrect audience value.

The code is attached to this post.  No warranties, provided as-is. 

 

For More Information

Inside SharePoint 2013 OAuth Context Tokens

Tips and FAQs: OAuth and remote apps for SharePoint 2013

Build a Custom Inspector for Fiddler

OAuth Fiddler Extension on Github

SPOAuthExtension.zip

Comments

  • Anonymous
    August 27, 2013
    Great job, Kirk.  Thanks for sharing!  I definitely plan on using this.