Tuesday, October 12, 2010

Importing user profile attributes from Claims based security tokens in SharePoint 2010

With the relatively new (August) labs release of Azure Access Control Service (ACS) version 2, there's been a rush of articles on how to authenticate to SharePoint 2010 through that. The benefit of using ACSv2 is the near immediate support for authenticating against both Windows Live, Google, Yahoo and Facebook.

My approach to familiarizing myself with ASCv2, and Claims in SharePoint as such, was to read most of the source code of the Windows Identity Foundation dlls - and especially those brought into the SharePoint namespace. Recently, however, quite detailed how-to-articles have surfaced - so if you're new to it now, there's no need to follow my awkward route to Rome :) If you're looking for info on how to do the initial setup of ACSv2, I suggest looking to Travis Nielsen's blog post, found here.

If you're still reading, I'm guessing you're more interested in what to do with the claim sets once you've actually got ACSv2 integrated in your environment.

While SharePoint 2010 supporting claims based identity management is a great thing; there was one thing I really wished for out of the box, when I first started poking with it. When you're working with identity information from AD or similar directories within your organization, you usually import profile fields after bringing new users into the system. When identities are provided by ACSv2, and e.g. Google's OAuth identity provider, SharePoint will rely an identity claim type you provide. This could be "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" for Google OAuth - which means that a newly added user will have his email address as userid. The same field's data will be put in the user profile's name field, and as such be the value shown in the top-right corner once logged in.

Image courtesy of Travis Nielsen

ACSv2 will actually let you provide more than just the identity field to the requesting application, however. At the time of writing, Windows Live through ACSv2 will only provide the somewhat obscurely encoded "nameidentifier", but the Google and Yahoo identity providers also supply a full name, email address and possibly more.

This means that one could see the following flow of operations when adding a new user:

  1. Admin adds new user's identity email address, such as einar@contoso.com, which could be associated with one of ACSv2's identity providers.
  2. SharePoint internally creates a new user object, and sets the loginname and name fields to einar@contoso.com
  3. User comes to SharePoint portal, and authenticates against whichever identity provider he's associated with.
  4. A custom SharePoint addon notices the new claims based authentication request, and examines the other attributes sent by the identity provider. If it finds a attribute of type "name" (with value "Einar Stangvik"), and the user's current name is the same as his loginname (einar@contoso.com) - which indicates that the user hasn't changed it yet; it replaces the value with the claims provided value.
  5. ... Repeat for other interesting claims-provided fields

The result would be a user logged on for the first time, with relevant fields imported and ready to go. Rather than seeing an obscure identifier or an email address in the top right corner of the screen, he or she would see her or his real name.


If you wish to give this a go, here's a pre-packed wsp: Grep.SharePoint.ImportClaimsFields.wsp.

Do note that I consider this slightly experimental, although stable in test against the current ACS labs release. Use it and develop it at your own risk.

If you're more interested in the source code itself, here's the important parts from the HTTPModule:

namespace Grep.SharePoint.ImportClaimsFields
{
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Web;
using Microsoft.IdentityModel.Tokens.Saml11;
using Microsoft.IdentityModel.Web;
using Microsoft.SharePoint;

public class ClaimsAuthenticationListenerModule : IHttpModule
{
private Dictionary<string, ProcessAttributeDelegate> _attributeHandlers;
private SamlSecurityToken _token;

#region Methods: private

private void OnSecurityTokenReceived(object sender, SecurityTokenReceivedEventArgs e)
{
_token = e.SecurityToken as SamlSecurityToken;
}

private void OnSignedIn(object sender, EventArgs e)
{
if (_token != null)
{
var assertion = _token.Assertion as Saml11Assertion;
if (assertion != null)
{
foreach (
SamlAttributeStatement attributeStatement in
assertion.Statements.OfType<SamlAttributeStatement>())
{
foreach (SamlAttribute attribute in attributeStatement.Attributes)
{
string key = (attribute.Namespace + "/" + attribute.Name).ToLower();
if (_attributeHandlers.ContainsKey(key))
{
_attributeHandlers[key](attribute);
}
}
}
}
_token = null;
}
}

private void ProcessEmail(SamlAttribute attribute)
{
SPContext.Current.Web.AllowUnsafeUpdates = true;
try
{
var user = SPContext.Current.Web.CurrentUser;
var value = attribute.AttributeValues.FirstOrDefault(v => String.IsNullOrEmpty(v) == false);
if (user != null && value != null)
{
if (string.IsNullOrEmpty(user.Email))
{
user.Email = value;
user.Update();
}
}
}
finally
{
SPContext.Current.Web.AllowUnsafeUpdates = false;
}
}

private void ProcessName(SamlAttribute attribute)
{
SPContext.Current.Web.AllowUnsafeUpdates = true;
try
{
var user = SPContext.Current.Web.CurrentUser;
var value = attribute.AttributeValues.FirstOrDefault(v => String.IsNullOrEmpty(v) == false);
if (user != null && value != null)
{
var identity = HttpContext.Current.User.Identity.Name.Split('|').LastOrDefault();
if (identity != null)
{
identity = HttpUtility.UrlDecode(identity);
if (string.IsNullOrEmpty(user.Name) || user.Name == identity)
{
user.Name = value;
user.Update();
}
}
}
}
finally
{
SPContext.Current.Web.AllowUnsafeUpdates = false;
}
}

#endregion

#region IHttpModule Methods

public void Dispose()
{
_token = null;
}

public void Init(HttpApplication context)
{
_attributeHandlers = new Dictionary<string, ProcessAttributeDelegate>
{
{"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", ProcessEmail},
{"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", ProcessName}
};
var otherModule = context.Modules["FederatedAuthentication"] as WSFederationAuthenticationModule;
if (otherModule != null)
{
otherModule.SecurityTokenReceived += OnSecurityTokenReceived;
otherModule.SignedIn += OnSignedIn;
}
}

#endregion

#region Nested type: ProcessAttributeDelegate

private delegate void ProcessAttributeDelegate(SamlAttribute attribute);

#endregion
}
}

Friday, October 8, 2010

Disjoint SharePoint Farm Integration Examples: Drag'n'drop file transfer

These two video demonstrations show parts of a framework I'm working on, which can connect otherwise severely disjoint SharePoint farms. Both show drag and drop capabilities between an intranet available in the local network and an external farm. The external farm need not be directly connectible from the client computer.

The first video is annotated, so it should speak for itself. Buzzwords include Claims, Azure, ACS, Google OAuth, drag and drop.

The second will show an embedded view of a completely unchanged SharePoint 2007 portal hosted at Microsoft Online (BPOS). In both videos, the outer intranet is a SharePoint 2010 site.

Larger View


Microsoft Online example:

Larger View


Update: Here's yet another example, showing multi-file copy as well.