Dynamic Controller/Action Authorization in ASP.NET MVC

Note: This has a CodePlex project now: http://mvcauthorization.codeplex.com/. The code there is a little different than the code below, and has some nice new features. The code below should still serve as a good reference, however.

Often times, after you’ve authenticated your users, you’ll want to authorize what they actually have control over based on role. Typically in an ASP.NET MVC project this happens by using an authorize attribute not unlike the one shown below:

[Authorize(Roles = "Programmer, Manager")]
public ActionResult MyTopSecretActionForSuperCoolPeopleOnly()

Unfortunately, the above code directly ties your action and controller code to your user roles. So what happens if a new role is added? Well, in my fake system I just added one, and it’s called the Fonzie role. Anyone who is at Fonzie status is by definition super cool, and needs to have access to our super cool action. So our code above would become:

[Authorize(Roles = "Programmer, Manager, Fonzie")]
public ActionResult MyTopSecretActionForSuperCoolPeopleOnly()

But, because our solution is code based we need to wait for a re-deployment and only then will the Fonzies get access to to our action. Not good! Also, what do we do about any action links to that action? We don’t want them just showing up for any old user. We’d better address that as well.

The solution to this problem is to create new configuration sections in web.config, which will allow us to specify authorization roles for our actions and controllers in a declarative way. That way, we only need to update the configuration, and the roles are automatically mapped to the actions and controllers without need for re-deployment. Below is what the code using this functionality should ultimately look like.

Define a configuration for controller/action roles:

  <authorizationConfiguration>
    <controllerAuthorizationMappings>
      <add controller="Home" role="GeneralAccess">        
        <!-- Define allowed roles for actions under the Home Controller -->
        <actionAuthorizationMappings>
          <add action="MyTopSecretActionForSuperCoolPeopleOnly" roles="Developer,Manager,Fonzie" />
        </actionAuthorizationMappings>
      </add>
    </controllerAuthorizationMappings>
  </authorizationConfiguration>

The configuration above says we should allow any user with GeneralAccess to the “Home” controller, but our top secret action should only be accessible to users with a Developer, Manager, or Fonzie role. Then to use it, we would just need to decorate our action with a new attribute called ActionAuthorize, which is derived from AuthorizeAttribute, and needs no parameters:

[ActionAuthorize]
public ActionResult MyTopSecretActionForSuperCoolPeopleOnly()

And the new attribute would be smart enough to look-up the roles for this action and allow/disallow access based on the configuration. (Note that in addition to [ActionAuthorize], we also have an attribute called [ControllerAuthorize] which does the same thing for controllers).

When we link to this action, we would need to have a custom html helper that only renders the action link when the user is in role for that action. The usage would look like:

<%= Html.SecuredActionLink("Secure Link.", "MyTopSecretActionForSuperCoolPeopleOnly") %>

To accomplish the above we will need to start off with five classes for our configuration section:

AuthorizationConfiguration
This class defines the root or our configuration section and contains a static accessor property, as well as a ControllerAuthorizationConfigurationCollection.

    public class AuthorizationConfiguration : ConfigurationSection
    {
        private static AuthorizationConfiguration _authorizationConfiguration
            = ConfigurationManager.GetSection("authorizationConfiguration") as AuthorizationConfiguration;

        public static AuthorizationConfiguration Section
        {
            get
            {
                return _authorizationConfiguration;
            }
        }

        [ConfigurationProperty("controllerAuthorizationMappings")]
        public ControllerAuthorizationConfigurationCollection ControllerAuthorizationMappings
        {
            get
            {
                return this["controllerAuthorizationMappings"] as ControllerAuthorizationConfigurationCollection;
            }
        }
    }

ControllerAuthorizationConfigurationCollection
This class will encapsulate our collection of Controller configurations.

   public class ControllerAuthorizationConfigurationCollection : ConfigurationElementCollection, 

IEnumerable<ControllerAuthorizationConfigurationElement>
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public ControllerAuthorizationConfigurationElement this[int index]
        {
            get
            {
                return base.BaseGet(index) as ControllerAuthorizationConfigurationElement;
            }
            set
            {
                if (base.BaseGet(index) != null)
                {
                    base.BaseRemoveAt(index);
                }
                this.BaseAdd(index, value);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public IEnumerable<ControllerAuthorizationConfigurationElement> Elements
        {
            get
            {
                for (int i = 0; i < base.Count; ++i)
                {
                    yield return this[i];
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public new IEnumerator<ControllerAuthorizationConfigurationElement> GetEnumerator()
        {
            for (int i = 0; i < base.Count; ++i)
            {
                yield return this[i];
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected override ConfigurationElement CreateNewElement()
        {
            return new ControllerAuthorizationConfigurationElement();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((ControllerAuthorizationConfigurationElement)element).Controller;
        } 
    }

ControllerAuthorizationConfigurationElement

This class will define the base configuration element for a controller. It has an optional property called “Roles” which we can use to lock down a controller by a certain set of roles. It also has an actionAuthorizationConfigurationCollection to maintain a list of actions.

    public class ControllerAuthorizationConfigurationElement : ConfigurationElement
    {
        [ConfigurationProperty("controller", IsRequired = true)]
        public string Controller
        {
            get
            {
                return (string)this["controller"];
            }
            set
            {
                this["controller"] = value;
            }
        }

        [ConfigurationProperty("roles", IsRequired = false)]
        public string Roles
        {
            get
            {
                return (string)this["roles"];
            }
            set
            {
                this["roles"] = value;
            }
        }

        [ConfigurationProperty("actionAuthorizationMappings")]
        public ActionAuthorizationConfigurationCollection ActionAuthorizationMappings
        {
            get
            {
                return this["actionAuthorizationMappings"] as ActionAuthorizationConfigurationCollection;
            }
        }
    }

ActionAuthorizationConfigurationCollection
This class will encapsulate our collection of Controller configurations under the current controller.

   public class ActionAuthorizationConfigurationCollection : ConfigurationElementCollection, 

IEnumerable<ActionAuthorizationConfigurationElement>
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public ActionAuthorizationConfigurationElement this[int index]
        {
            get
            {
                return base.BaseGet(index) as ActionAuthorizationConfigurationElement;
            }
            set
            {
                if (base.BaseGet(index) != null)
                {
                    base.BaseRemoveAt(index);
                }
                this.BaseAdd(index, value);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public IEnumerable<ActionAuthorizationConfigurationElement> Elements
        {
            get
            {
                for (int i = 0; i < base.Count; ++i)
                {
                    yield return this[i];
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public new IEnumerator<ActionAuthorizationConfigurationElement> GetEnumerator()
        {
            for (int i = 0; i < base.Count; ++i)
            {
                yield return this[i];
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        protected override ConfigurationElement CreateNewElement()
        {
            return new ActionAuthorizationConfigurationElement();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((ActionAuthorizationConfigurationElement)element).Action;
        } 
    }

ActionAuthorizationConfigurationElement
This class will encapsulate a particular action under a controller. It has a required “Roles” parameter.

    public class ActionAuthorizationConfigurationElement : ConfigurationElement
    {
        [ConfigurationProperty("action", IsRequired = true)]
        public string Action
        {
            get
            {
                return (string)this["action"];
            }
            set
            {
                this["action"] = value;
            }
        }

        [ConfigurationProperty("roles", IsRequired = true)]
        public string Roles
        {
            get
            {
                return (string)this["roles"];
            }
            set
            {
                this["roles"] = value;
            }
        }
    }

After we have defined our configuration elements and registered the section in our web.config, we can start using it. We define two attribute classes which will perform the authentication look-up for our controllers and actions:

ActionAuthorizeAttribute targets an action method:

    /// <summary>
    /// Authorization attribute for an MVC action method
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public class ActionAuthorizeAttribute : AuthorizeAttribute
    {
        private string[] _rolesArray;
        private string _authorizationGroupName;

        /// <summary>
        /// 
        /// </summary>
        public string AccessDeniedController { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string AccessDeniedAction { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public ActionAuthorizeAttribute()
        {
            AccessDeniedController = ConfigurationManager.AppSettings["DefaultAccessDeniedController"] ?? 

"AccessDenied";
            AccessDeniedAction = ConfigurationManager.AppSettings["DefaultAccessDeniedAction"] ?? "Index";
            _authorizationGroupName = string.Empty;
        }

        /// <summary>
        /// 
        /// </summary>
        public string[] RolesArray
        {
            get
            {
                return _rolesArray;
            }
            set
            {
                _rolesArray = value;
                this.Roles = string.Join(",", _rolesArray);
            }
        }

        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;

            // Get any controller specific role mappings
            var controllerRoleMappings = AuthorizationConfiguration.Section.ControllerAuthorizationMappings.Where(e 

=> e.Controller == controllerName).FirstOrDefault();

            if (controllerRoleMappings != null)
            {
                var actionRoleMappings = controllerRoleMappings.ActionAuthorizationMappings.Where(e => e.Action == 

filterContext.ActionDescriptor.ActionName).FirstOrDefault();

                if (actionRoleMappings != null && !string.IsNullOrEmpty(actionRoleMappings.Roles))
                {
                    this.RolesArray = actionRoleMappings.Roles.Split(',');
                }
            }

            //
            base.OnAuthorization(filterContext);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { 

Controller = AccessDeniedController, Action = AccessDeniedAction, Roles = Roles }));
        }
    }

ControllerAuthorizeAttribute targets a controller class:

    /// <summary>
    /// Authorization attribute for an MVC controller
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class ControllerAuthorizeAttribute : AuthorizeAttribute
    {
        private string[] _rolesArray;
        private string _authorizationGroupName;

        /// <summary>
        /// 
        /// </summary>
        public string AccessDeniedController { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string AccessDeniedAction { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public ControllerAuthorizeAttribute()
        {
            AccessDeniedController = ConfigurationManager.AppSettings["DefaultAccessDeniedController"] ?? 

"AccessDenied";
            AccessDeniedAction = ConfigurationManager.AppSettings["DefaultAccessDeniedAction"] ?? "Index";
            _authorizationGroupName = string.Empty;
        }

        /// <summary>
        /// 
        /// </summary>
        public string[] RolesArray
        {
            get
            {
                return _rolesArray;
            }
            set
            {
                _rolesArray = value;
                this.Roles = string.Join(",", _rolesArray);
            }
        }

        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;

            // Get any controller specific role mappings
            var controllerRoleMappings = AuthorizationConfiguration.Section.ControllerAuthorizationMappings.Where(e 

=> e.Controller == controllerName).FirstOrDefault();

            if (controllerRoleMappings != null && !string.IsNullOrEmpty(controllerRoleMappings.Roles))
            {
                this.RolesArray = controllerRoleMappings.Roles.Split(',');
            }

            //
            base.OnAuthorization(filterContext);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { 

Controller = AccessDeniedController, Action = AccessDeniedAction, Roles = Roles }));
        }
    }

Then for dynamic action link rendering based on role, the following Html helper would be as follows:


        /// <summary>
        /// Renders the action link, provided the user has access to the action method being linked to
        /// </summary>
        /// <param name="helper"></param>
        /// <param name="linkText"></param>
        /// <param name="actionName"></param>
        /// <returns></returns>
        public static MvcHtmlString SecuredActionLink(this HtmlHelper helper, string linkText, string actionName)
        {
            MvcHtmlString html = MvcHtmlString.Empty;
            string[] roles = null;

            RouteData routeData = helper.ViewContext.RouteData;
            string controllerName = routeData.GetRequiredString("controller");

            // Get any controller specific role mappings
            var controllerRoleMappings = AuthorizationConfiguration.Section.ControllerAuthorizationMappings.Where(e 

=> e.Controller == controllerName).FirstOrDefault();
            ActionAuthorizationConfigurationElement actionRoleMappings = null;

            if (controllerRoleMappings != null)
            {
                actionRoleMappings = controllerRoleMappings.ActionAuthorizationMappings.Where(e => e.Action == 

actionName).FirstOrDefault();

                if (actionRoleMappings != null && !string.IsNullOrEmpty(actionRoleMappings.Roles))
                {
                    roles = actionRoleMappings.Roles.Split(',');
                }
            }

            if (roles != null)
            {
                foreach (string role in roles)
                {
                    if (ApplicationUser.Current.Roles.Contains(role))
                    {
                        html = System.Web.Mvc.Html.LinkExtensions.ActionLink(helper, linkText, actionName);
                        break;
                    }
                }
            }
            else if (actionRoleMappings == null)
            {
                // If there's no roles associated to that action then render the link
                html = System.Web.Mvc.Html.LinkExtensions.ActionLink(helper, linkText, actionName);
            }

            return html;
        }
    }

The helper above will figure out if the user has access to the action, display accordingly, and viola! Users will never see a link to an action for which they do not have access, and if they do somehow get to an action that is restricted based on their role they will get redirected to an access denied page of your choosing.

This is just a basic sample, but there’s lots that can be done in addition to this. Be sure to download the source here to see it in action (VS 2010, ASP.NET MVC 2, and WIF required). In particular play around with the role definitions at the bottom of the web.config to see the actions dynamically authorize as the roles in the config change. The authentication is done via the default STS server project that gets created when you create a federated security application, so you will need to install the WIF RTW and SDK in order to successfully run the project the way it is.

The .NET Facebook API and Silverlight

Awesome! There is now a library for .NET developers to interface with the Facebook services, opening up a whole new world of integration between your app and all of your user’s information in Facebook. In this post I have included code samples for a simple Facebook friend viewer. With the viewer, you can pull all tagged photos of yourself, your friends, and all their information. While the app functions, there is so much more that can be done. Comments can be posted to Facebook remotely, pictures uploaded and tagged remotely, videos uploaded, etc. Virtually all of the Facebook functionality is exposed through the API. Checkout the bottom of the post for a link to the project, as well as a working demo. Let’s dive in!

First, downoad the required assemblies. Note that there are assemblies for WinForms, WPF, ASP.NET MVC (not just Silverlight) included!

Then, you will need to setup your account at http://www.facebook.com/developers/. Click on “Setup New Application” and enter a new application name as shown in the screenshot below:

CreateApp

Then fill out the fields shown below on the “Basic Info” tab. Note the developers area. Add any devs who will be working on the app there:

FillOutInfo

From there fill out the connect information. In development this will be the local http://localhost:[port] that that you are developing on. In production it will be yourdomain.com:

connect

Then click on the advanced tab and enable Sandbox mode, as we will be using this account for development purposes:

advancedSandbox

Cool! Now that we have our account setup we need to create a “Silverlight Navigation Application” in Visual Studio. To make everything work, we need to add a javascript file to the same directory as the HTML page that is hosting our Silverlight app. This will allow us to facilitate the cross domain communication between our app and Facebook (note the var silverLightPluginId must match the DOM id of the Silverlight plugin on the page!!!):

fblogin.js

//must match the Silverlight plugin's DOM ID
var silverlightPluginId = 'Silverlight1';

function facebook_init(appid) {
    FB.init(appid, "/xd_receiver.htm");
}

function isUserConnected() {
    FB.ensureInit(function () {
        FB.Connect.get_status().waitUntilReady(function (status) {
            var plugin = document.getElementById(silverlightPluginId);
        });
    });
}

function facebook_login() {
    FB.ensureInit(function () {
        FB.Connect.requireSession(facebook_getSession, true);
    });
}

function facebook_logout() {
    FB.Connect.logout(facebook_onlogout);
}

function facebook_getSession() {

    FB.Facebook.get_sessionState().waitUntilReady(function () {
        var session = FB.Facebook.apiClient.get_session();
        var plugin = document.getElementById(silverlightPluginId);
        plugin.Content.FacebookLoginControl.LoggedIn(session.session_key, session.secret, session.expires, session.uid);
    });
}

function facebook_onlogout() {
    var plugin = document.getElementById(silverlightPluginId);
    plugin.Content.FacebookLoginControl.LoggedOut();
}

function facebook_onpermission(accepted) {
    var plugin = document.getElementById(silverlightPluginId);
    plugin.Content.FacebookLoginControl.PermissionCallback(accepted);
}

function facebook_prompt_permission(permission) {
    FB.ensureInit(function () {
        FB.Connect.showPermissionDialog(permission, facebook_onpermission);
    });
}

Include the script in the page hosting the Silverlight app:

<script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php" type="text/javascript"></script>
<script type="text/javascript" src="fblogin.js"></script>

Now, add the file xd_receiver.htm to the same directory as the javascript file. This will receive the response from Facebook and route it appropriately:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
  <body>
    <script src=http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.js
                    type="text/javascript">
    </script>
  </body>
</html>

Whew! Now that all that’s done we can tackle the Silverlight part. Add a reference the assembly Facebook.Silverlight.dll to your Silverlight project. Note that if you are using Visual Studio 2010 you might get a build error similar to the following after you add the reference:

—> System.NotSupportedException: An attempt was made to load an assembly from a network location which would have caused the assembly to be sandboxed in previous versions of the .NET Framework. This release of the .NET Framework does not enable CAS policy by default, so this load may be dangerous. If this load is not intended to sandbox the assembly, please enable the loadFromRemoteSources switch. See http://go.microsoft.com/fwlink/?LinkId=155569 for more information.

This is because the CAS policies have changed in VS2010. No worries though, just locate the assembly in the file system and Unblock it by clicking on the button in the screenshot below:

UnBlock

In your Silverlight application you then just need to initialize the Facebook API object and invoke the Login() method:


        private Api _facebookAPI;
        private readonly BrowserSession _browserSession;
        private const string ApplicationKey = "[Put your application key here, yes I know it's not the best place to initialize it!]";

        public Home()
        {
            InitializeComponent();

            _browserSession = new BrowserSession(ApplicationKey);
            _browserSession.LoginCompleted += browserSession_LoginCompleted;
            _browserSession.GetStatusCompleted += new EventHandler<GetStatusCompletedEventArgs>(_browserSession_GetStatusCompleted);
            _browserSession.Login();
        }

This will generate the browser popup to have the user login to their Facebook account. Once the user is logged in the _browserSession_GetStatusCompleted method gets invoked:

        private void browserSession_LoginCompleted(object sender, EventArgs e)
        {
            _facebookAPI = new Api(_browserSession);

            //set the photo and friends list item sources
            Photos.ItemsSource = myImages;
            FriendsList.ItemsSource = myFriends;

            //get my friends
            _facebookAPI.Friends.GetUserObjectsAsync(
                (friends, state, ex) =>
                {
                    if (ex == null)
                    {
                        Dispatcher.BeginInvoke(
                            () =>
                            {
                                AddFriends(friends);
                            });
                    }
                }, null);

            Facebook.Session.BrowserSession session = sender as Facebook.Session.BrowserSession;

            _facebookAPI.Photos.GetAsync(session.UserId.ToString(), null, null,
                new Photos.GetCallback(
                    (photos, obj, ex) =>
                    {
                        Dispatcher.BeginInvoke(
                            () =>
                            {
                                AddPhotos(photos);
                            });
                    }), null);
        }

In here we’re doing a couple things. First we’re invoking the FB-API and getting the user’s friends and a listing of all photos they have been tagged in. Once we have those we add them to the myImages and myFriends observable collections, which are bound to the UI. The AddFriends() and AddPhotos() implementations are listed below:


        //add friends to the observable collection
        private void AddFriends(IList<Facebook.Schema.user> friends)
        {
            foreach (Facebook.Schema.user friend in friends)
            {
                myFriends.Add(new Friend()
                {
                    Name = friend.first_name,
                    Photo = new BitmapImage(new Uri(friend.pic_small)),
                    UserId = friend.uid.GetValueOrDefault(),
                    Activities = friend.activities,
                    Affiliations = friend.affiliations != null && friend.affiliations.affiliation != null && friend.affiliations.affiliation.Count > 0 ? friend.affiliations.affiliation.Select(a => a.name).Aggregate((i, j) => string.Concat(i, ", ", j)) : string.Empty,
                    Books = friend.books,
                    EducationHistory = friend.education_history != null && friend.education_history.education_info != null ? GetEducation(friend.education_history) : string.Empty,
                    Interests = friend.interests,
                    Movies = friend.movies,
                    Music = friend.music,
                    Quotes = friend.quotes,
                    Sex = friend.sex,
                    TV = friend.tv,
                    WorkHistory = friend.work_history != null && friend.work_history.work_info != null && friend.work_history.work_info.Count > 0 ? friend.work_history.work_info.Select(e => e.company_name).Aggregate((i, j) => string.Concat(i, ", ", j)) : string.Empty
                });
            }
        }

        private void AddPhotos(IList<Facebook.Schema.photo> photos)
        {
            foreach (Facebook.Schema.photo photo in photos)
            {
                myImages.Add(
                        new Photo() { 
                            Small = new BitmapImage(new Uri(photo.src_small)),
                            Normal = new BitmapImage(new Uri(photo.src)),
                            Large = new BitmapImage(new Uri(photo.src_big))
                        }
                    );
            }
        }


        /// <summary>
        /// gets a string representing all education history
        /// </summary>
        /// <param name="educationHistory"></param>
        /// <returns></returns>
        private string GetEducation(Facebook.Schema.userEducation_history educationHistory)
        {
            return educationHistory.education_info.Select(
                    (e) =>
                    {
                        return string.Format(" {0} {1} {2} {3}", 
                            //degree info
                           !string.IsNullOrEmpty(e.degree) ? "Degree: " + e.degree : string.Empty, 

                            //concentration info
                            e.concentrations != null && e.concentrations.concentration != null && e.concentrations.concentration.Count > 0
                            ? 
                                "Concentrations: " + e.concentrations.concentration.Aggregate(
                                (i, j) => {
                                    return string.Concat(i, ", ", j);
                                }) : string.Empty, 
                            
                            //college name
                            !string.IsNullOrEmpty(e.name) ? "College Name: " + e.name : string.Empty, 
                            
                            //college year
                            e.year > 0 ? "Year Graduated: " + e.year : string.Empty);
                    }).Aggregate((i, j) =>
                    {
                        return string.Concat(i, j);
                    });
        }

That’s the core code! Now our UI just needs to bind to the collections and we have all the data we need. Check it out!


Working Demo (If your firewall blocks requests to/from Facebook this will just hang and will not load!!)

Download the project to get the fully working solution (in VS2010 and Silverlight 4)

Silverlight 4 Webcam and Microphone Support

The beta of Silverlight 4 has lots of neat stuff packaged with it. One of these things included is native Webcam and Microphone support. In this post I will show how to create a simple webcam viewer complete with A/V and picture capture. However, one thing not currently in the beta is streaming the video and audio to a remote destination such as a file, another client, or a media server. I have not tried to implement this in this functionality in this post because it will most likely be implemented by the time the final release comes out, so rather than “pre-invent” the wheel, I have decided to wait until Microsoft releases a version of their own. I have, however, included implementations of the AudioSink and VideoSink which can be used to pull the raw audio and video and either stream it or save it. Since the the data passed to the OnSamples () method in VideoSink is nothing more than 32bpp ARGB raw bitmap frames it is feasible to create a custom implementation that can save the video to a stream suitable for saving to a file or media server. Be sure to check out the bottom of the post for links to a screenshot of the final result and complete source code. There are also links to shader effects and an attempt at saving the streams to AVI files.

First, to house the video we create a rectangle in XAML. Note the render transform. This will flip the image about it’s Y-Axis so it appears like a mirror, which is more intuitive when viewing the video on screen.

        <Rectangle Height="254" HorizontalAlignment="Left" Margin="31,47,0,0" Name="VideoPane" Stroke="Black" StrokeThickness="1" VerticalAlignment="Top" Width="511" RenderTransformOrigin="0.5,0.5">
            <Rectangle.RenderTransform>
                <TransformGroup>
                    <ScaleTransform ScaleX="-1"/>
                    <SkewTransform AngleX="0" AngleY="0"/>
                    <RotateTransform Angle="0"/>
                    <TranslateTransform/>
                </TransformGroup>
            </Rectangle.RenderTransform>
        </Rectangle>

Next we create two listboxes to show a list of devices suitable for audio and video capture. The user needs to select an audio and video device before starting capture.

        <dataInput:Label Height="18" HorizontalAlignment="Left" Margin="109,426,0,0" Content="Video Devices" Name="label1" VerticalAlignment="Top" Width="83" />
        <ListBox Height="48" Margin="31,443,0,0" Name="VideoCaptureDevices" VerticalAlignment="Top" HorizontalAlignment="Left" Width="256">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding FriendlyName}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <dataInput:Label Height="18" HorizontalAlignment="Left" Margin="367,426,0,0" Content="Audio Devices" Name="label2" VerticalAlignment="Top" Width="83" />
        <ListBox Height="48" HorizontalAlignment="Left" Margin="293,443,0,0" Name="AudioCaptureDevices" VerticalAlignment="Top" Width="249">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding FriendlyName}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox> 

The following will display a list of images captured when the user takes a CamShot:

        <ScrollViewer Margin="34,313,33,102" Width="505" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Auto">
            <ItemsControl x:Name="PhotoShots" Height="106">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Image Source="{Binding}" Margin="5" Stretch="UniformToFill" Height="70" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </ScrollViewer>

In our code behind we create a capture source and an ObservableCollection to hold our images captured from the webcam. We then get the list of devices and set the item source properties when the page is loaded.

        CaptureSource captureSource = new CaptureSource();
        ObservableCollection<WriteableBitmap> photoShots = new ObservableCollection<WriteableBitmap>();

        public MainPage()
        {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(MainPage_Loaded);
        }

        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            VideoCaptureDevices.ItemsSource = CaptureDeviceConfiguration.GetAvailableVideoCaptureDevices();
            AudioCaptureDevices.ItemsSource = CaptureDeviceConfiguration.GetAvailableAudioCaptureDevices();
            PhotoShots.ItemsSource = photoShots;
        }

When we are told to start the capture, make sure the user selected the audio and video capture devices, request access to the device, and if the user allowed us access then set the sink capture sources, create the brush, start capture, and give the rectangle a drop shadow effect.

        private void StartWebCam_Click(object sender, RoutedEventArgs e)
        {
            if (VideoCaptureDevices.SelectedItem != null && AudioCaptureDevices.SelectedItem != null)
            {
                //stop the source if it's started
                if (captureSource.State == CaptureState.Started)
                {
                    captureSource.Stop();
                }

                //VideoCaptureDevice device = CaptureDeviceConfiguration.GetDefaultVideoCaptureDevice();
                VideoCaptureDevice videoDevice = VideoCaptureDevices.SelectedItem as VideoCaptureDevice;

                //AudioCaptureDevice device = CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice();
                AudioCaptureDevice audioDevice = AudioCaptureDevices.SelectedItem as AudioCaptureDevice;

                //check to see if we have already been allowed access or request if we have not
                if (CaptureDeviceConfiguration.AllowedDeviceAccess || CaptureDeviceConfiguration.RequestDeviceAccess())
                {
                    captureSource.VideoCaptureDevice = videoDevice;
                    captureSource.AudioCaptureDevice = audioDevice;

                    //set the video sink's capture source
                    videoSink.CaptureSource = captureSource;
                    audioSink.CaptureSource = captureSource;

                    //create a brush and set the source to the capture source
                    VideoBrush brush = new VideoBrush();
                    brush.SetSource(captureSource);
                    brush.Stretch = Stretch.UniformToFill;

                    //start the capture
                    captureSource.Start();

                    //paint the rectangle with the video
                    VideoPane.Fill = brush;

                    //set the effect
                    VideoPane.Effect = new DropShadowEffect() { ShadowDepth = 7 };
                }
            }
            else
            {
                MessageBox.Show("Please select an audio and a video capture device to start the session");
            }
        }

The following will take a screenshot and add it to the observable collection of WriteableBitmaps:

        private void TakeCamShot_Click(object sender, RoutedEventArgs e)
        {
            if (captureSource != null)
            {
                captureSource.AsyncCaptureImage(
                    (img) =>
                    {
                        photoShots.Add(img);
                    });
            }
        }

Below are the AudioSink and VideoSink implementations. They can be extended to save the streams to whatever destination is needed:

    /// <summary>
    /// 
    /// </summary>
    public class MSVideoSink : VideoSink
    {
        /// <summary>
        /// Video
        /// </summary>
        public MemoryStream VideoStream
        {
            get;
            private set;
        }


        /// <summary>
        /// Format
        /// </summary>
        public VideoFormat Format
        {
            get;
            set;
        }

        /// <summary>
        /// Capture started
        /// </summary>
        protected override void OnCaptureStarted()
        {
            VideoStream = new MemoryStream();
        }


        /// <summary>
        /// Capture stops
        /// </summary>
        protected override void OnCaptureStopped()
        {
            //post-process video data
        }


        /// <summary>
        /// Format changing
        /// </summary>
        /// <param name="videoFormat"></param>
        protected override void OnFormatChange(VideoFormat videoFormat)
        {
            //do stuff on format change, i.e. close off stream, etc
            Format = videoFormat;
        }

        /// <summary>
        /// Write the sample data to the stream
        /// </summary>
        /// <param name="sampleTime"></param>
        /// <param name="frameDuration"></param>
        /// <param name="sampleData"></param>
        protected override void OnSample(long sampleTime, long frameDuration, byte[] sampleData)
        {
            //do something here...i.e. stream it to a file, etc
            //VideoStream.Write(sampleData, 0, sampleData.Length);
        }
    }


    /// <summary>
    /// 
    /// </summary>
    public class MSAudioSink : AudioSink
    {
        /// <summary>
        /// Capture starts
        /// </summary>
        protected override void OnCaptureStarted()
        {
            WaveformDataStream = new MemoryStream();
        }

        /// <summary>
        /// Post-process audio data
        /// </summary>
        protected override void OnCaptureStopped()
        {
        }

        /// <summary>
        /// Audio format
        /// </summary>
        public AudioFormat Format
        {
            get;
            set;
        }

        /// <summary>
        /// Memory stream
        /// </summary>
        public MemoryStream WaveformDataStream
        {
            get;
            set;
        }

        /// <summary>
        /// Format is changing
        /// </summary>
        /// <param name="format"></param>
        protected override void OnFormatChange(AudioFormat format)
        {
            //do stuff on format change, i.e. close off stream, etc
            Format = format;
        }

        /// <summary>
        /// Save each sample
        /// </summary>
        /// <param name="sampleTime"></param>
        /// <param name="sampleDuration"></param>
        /// <param name="sampleData"></param>
        protected override void OnSamples(long sampleTime, long sampleDuration, byte[] sample)
        {
            //do something here...i.e. stream it to a file, etc
           // WaveformDataStream.Write(sample, 0, sample.Length);
        }
    }  

Below is a screenshot of the result:
WebcamAppScreenshot

Download the source code (VS2010):
SilverlightWebcamApp.zip

Edge detection (some cool effects):
http://kodierer.blogspot.com/2009/11/edgecam-silverlight-4-webcam-edge.html
http://kodierer.blogspot.com/2009/07/livin-on-edge-silverlight-parametric_4324.html

Saving streams to AVI files:
http://mtaulty.com/CommunityServer/blogs/mike_taultys_blog/archive/2009/12/10/silverlight-4-more-on-capturing-video-from-webcams.aspx