Federating OAuth and OpenId

April 12, 2013 Leave a comment

I was playing around with some of the new WCM features in SharePoint 2013 the other day and I reached a point where I wanted to authenticate ‘customers’.  I thought that this would be a good opportunity to figure out how to authenticate with external providers such as Google, Windows Live, Facebook, etc.

At first I had hoped that SharePoint 2013 would support this out of the box, especially with how it leverages OAuth in the new App model.  I did some looking and couldn’t find a way to do this, if someone knows how to do it I’d love to know how.

After reading various MSDN articles and blogs I found this post from Steve Peschka http://blogs.technet.com/b/speschka/archive/2012/03/01/finally-a-useful-way-to-federate-with-windows-live-and-sharepoint-2010-using-oauth-and-saml.aspx.  Basically he shows how to build your own Security Token Service using WIF to authenticate via Windows Live.  I decided to take his example and swap out the authentication code with a call to DotNetOpenAuth.  A lot of credit to this should go to Steve, his original post saved me a lot of heavy lifting.

I started by creating a new solution with two projects identity.sts.web and identity.sts.  You could probably do this all in one project, it just felt better to me if I made two.  At a high level the solution has the following components:

  • passive-sts.aspx.  This is the main page of the token service.  It delegates the authentication and processes the WIF request.
  • Global.asax.  The providers are initialized in the Application_Start.
  • AcmeSecurityTokenService.cs.  This is the token service it’s self, authorizes applications, etc.
  • AcmeSecurityTokenServiceConfiguration.cs.  Configures the token service.

The basic structure of the passive-sts.aspx file looks like this.  We’ll fill in the details a little later, but essentially the flow goes like this.

  1. The initial request request from SharePoint will have an action of WSFederationConstants.Actions.SignIn.
  2. If the user is not authenticated we will make an authentication request and set the return url back to our self with a custom action of ‘callback’.
  3. At that point we will see if the authentication was successful and either process a WIF SignIn request or throw an unauthorized exception.
string action = Request.QueryString[WSFederationConstants.Parameters.Action];

if (Request.QueryString[Constants.Parameters.STSAction] == Constants.Actions.Callback)
{
    string url = string.Format("/passive-sts.aspx?{0}={1}",
                                Constants.Parameters.STSAction,
                                Constants.Actions.Callback);
    var result = OpenAuth.VerifyAuthentication(url);
    if (result.IsSuccessful)
    {
        //Process Signin Request
    }
    else
    {
        throw new UnauthorizedAccessException();
    }
}
else if (action == WSFederationConstants.Actions.SignIn)
{
    //process signin request
    if (!User.Identity.IsAuthenticated)
    {
        //request authentication
    }
    else
    {
        throw new UnauthorizedAccessException();
    }
}
else if (action == WSFederationConstants.Actions.SignOut)
{
    // Process signout request.
}
else
{
    //Invalid Request
}

Requesting authentication through the DotNetOpenAuth library is just a couple lines of code.  All that we need to do is call the ‘RequestAuthentication’ method and provide a callback url.  When building the callback url we provide our custom ‘callback’ action and keep track of the original query string in a ‘state’ parameter.

string queryString = HttpUtility.UrlEncode(Request.Url.Query);
OpenAuth.RequestAuthentication("google", string.Format("/passive-sts.aspx?{0}={1}&{2}={3}", 
                                                        Constants.Parameters.STSAction, 
                                                        Constants.Actions.Callback,
                                                        Constants.Parameters.STSState,
                                                        queryString));

Processing the callback from our provider is a little more work, but it’s nothing crazy.  Essentially we need to:

  1. Put claim values into a dictionary.
  2. Build a new SignInRequestMessage for the original uri.
  3. Create a Security Token Service with the dictionary of values.
  4. Process the SignIn request using the message from step 2 and the STS from step 3.
string originalQueryString = HttpUtility.UrlDecode(Request[Constants.Parameters.STSState]);

Dictionary<string, string> values = new Dictionary<string, string>();

//always add these values as claims
values.Add("preferred", result.UserName);
values.Add("provier-userid", result.ProviderUserId);
values.Add("provider", result.Provider);

//add anything in the extra data array as well
foreach (var key in result.ExtraData.Keys)
{
    values.Add(key, result.ExtraData[key]);
}

//create a signin request message; include the original query string
Uri uri = new Uri(GetCurrentPath() + originalQueryString);
SignInRequestMessage requestMessage = 
    (SignInRequestMessage)WSFederationMessage.CreateFromUri(uri);

//create an instance of our sts and pass in the dictionary of values we got back
SecurityTokenService sts = 
    new AcmeSecurityTokenService(AcmeSecurityTokenServiceConfiguration.Current, values);

//this response message is where our GetOutputClaimsIdentity method in CustomSecurityTokenService is invoked
SignInResponseMessage responseMessage = 
    FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest(requestMessage, User, sts);
FederatedPassiveSecurityTokenServiceOperations.ProcessSignInResponse(responseMessage, Response);

That is all of the major components of the web project.  The Security Token Service is relatively straight forward as well. We have three methods that we are going to override.

  • void ValidateAppliesTo(EndpointAddress appliesTo).  This method validates that the requesting application is allowed to authenticate through this token service.
  • Scope GetScope(IClaimsPrincipal principal, RequestSecurityToken request). This method gets the configuration for the the instance of the token signing request.
  • IClaimsIdentity GetOutputClaimsIdentity(IClaimsPrincipal principal, RequestSecurityToken request, Scope scope).  Creates a new claims identity for the provided request.  This method will leverage the dictionary that we built up in passive-sts.aspx.

My ValidateAppliesTo method looks like this.  The method expects an exception if something is wrong, so you should be able to leave it blank.  My example checked against a hard coded string, but something more configurable would be better.

if (appliesTo == null)
{
    throw new ArgumentNullException("appliesTo");
}

// TODO: Enable AppliesTo validation for allowed relying party Urls 
//       by setting enableAppliesToValidation to true. By default it is false.
if (enableAppliesToValidation)
{
    bool validAppliesTo = false;
    foreach (string rpUrl in PassiveRedirectBasedClaimsAwareWebApps)
    {
        if (appliesTo.Uri.Equals(new Uri(rpUrl)))
        {
            validAppliesTo = true;
            break;
        }
    }

    if (!validAppliesTo)
    {
        string message = string.Format("The 'appliesTo' address '{0}' is not valid.", 
                                        appliesTo.Uri.OriginalString);
        throw new InvalidRequestException(message);
    }
}

GetScope looks like this.  This code is verbatim from Steve.  I have not had a chance to try out encrypting the claims yet.

ValidateAppliesTo(request.AppliesTo);

//
// Note: The signing certificate used by default has a Distinguished name of "CN=STSTestCert",
// and is located in the Personal certificate store of the Local Computer. Before going into production,
// ensure that you change this certificate to a valid CA-issued certificate as appropriate.
//
Scope scope = new Scope(request.AppliesTo.Uri.OriginalString, 
                        SecurityTokenServiceConfiguration.SigningCredentials);

string encryptingCertificateName = WebConfigurationManager.AppSettings["EncryptingCertificateName"];
if (!string.IsNullOrEmpty(encryptingCertificateName))
{
    // Important note on setting the encrypting credentials.
    // In a production deployment, you would need to select a certificate that 
    // is specific to the RP that is requesting the token.
    // You can examine the 'request' to obtain information to determine the certificate to use.
    scope.EncryptingCredentials = 
        new X509EncryptingCredentials(CertificateUtil.GetCertificate(StoreName.My, 
                                                                        StoreLocation.LocalMachine, 
                                                                        encryptingCertificateName));
}
else
{
    // If there is no encryption certificate specified, the STS will not perform encryption.
    // This will succeed for tokens that are created without keys (BearerTokens) or asymmetric keys.  
    scope.TokenEncryptionRequired = false;
}

// Set the ReplyTo address for the WS-Federation passive protocol (wreply). 
// This is the address to which responses will be directed. 
// In this template, we have chosen to set this to the AppliesToAddress.
scope.ReplyToAddress = scope.AppliesToAddress;

return scope;

Finally GetOutputClaimsIdentity looks like this.  At a high level we are creating a new ClaimsIdentity and adding a claim for each value in the dictionary.  Notice that some of the claims have specific values, such as ‘preferred’.

if (null == principal)
{
    throw new ArgumentNullException("principal");
}

ClaimsIdentity outputIdentity = new ClaimsIdentity();

// Issue custom claims.
// Update the application's configuration file too to reflect new claims requirement.

//for each item in the dictionary that isn't == null, we should
//create a new claim for it
//we want to create an email claim equal to "preferred" in the dictionary
//we'll also create an accesstoken claim for the accessToken variable

outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.Name, oAuthValues["preferred"]));
outputIdentity.Claims.Add(new Claim(ClaimTypes.Email, oAuthValues["preferred"]));

//this is the claim type that we'll use as the default prefix for all
const string CLAIM_PREFIX = "http://schema.acme.com/claims/";
string claimType = CLAIM_PREFIX;

foreach (string key in oAuthValues.Keys)
{
    if (oAuthValues[key] != "null")
    {
        //we added email claim above separately; the rest we'll add as role claims
        if (key != "preferred")
        {
            //weird side effect - if the ClaimType ends in "name" then WIF sticks it in 
            //the ClaimTypes.Name claim type for some reason so we'll just rename it to what
            //it actually is
            if (key == "name")
                claimType += "full_";
            else
                claimType = CLAIM_PREFIX;

            outputIdentity.Claims.Add(new Claim(claimType + key, oAuthValues[key]));
        }
    }
}

return outputIdentity;

Once all of those pieces are put together deploy the website to IIS and configure SharePoint to use it as a Trusted Identity Provider using the same PowerShell commands that you would use to configure any identity provider (i.e. ADFS).

$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(“c:\installs\acme.wildcard.cer”)

New-SPTrustedRootAuthority -Name “Acme Authority” -Certificate $cert

$map1 = New-SPClaimTypeMapping -IncomingClaimType “http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress” -IncomingClaimTypeDisplayName “EmailAddress” -SameAsIncoming

$realm = https://www.acme.com/_trust

$ap = New-SPTrustedIdentityTokenIssuer -Name “acme-saml” -Description “SharePoint secured by SAML” -realm $realm -ImportTrustCertificate $cert -ClaimsMappings $map1 -SignInUrl “https://login.acme.com/passive-sts.aspx” -IdentifierClaim “http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress&#8221;

At this point you can configure your new identity provider for your web application in central admin.

image

Finally you can pull up your SharePoint site, click on ‘Sign In’ and log in with your external provider.

imageimage

I’ve placed the code here.  Feel free to take a look and let me know what you think.

Categories: SharePoint, WIF

My First Post

November 28, 2012 Leave a comment

Hi,

I’ve been an avid consumer of technical blogs over the past few years and I feel like it’s time for me to start giving back a little.  I mainly focus on building public facing websites and extranets using SharePoint, so my content will center around that.  I figure that I’ll post what I find interesting and see where it goes from there.

Categories: Uncategorized