Saturday 5 October 2013

Retrieving Credentials from the SharePoint Secure Store using C#

Introduction

There are times when you need to connect SharePoint to an system external. If the external system requires authentication, unless you have Kerberos authentication (and delegation) configured in your environment, you will suffer from the "Double Hop" authentication issue (explained here). Because of the double hop issue, you will need to reference a username and password within your to connect to the external system (unless anonymous access is allowed). However, hard coding a username and password is not only a bad practice, it's also insecure, unmanageable and lacks portability.

This article focuses on using the SharePoint Secure Store Application to store user credentials for use in code based solutions. Credential information can be retrieved by code to authenticate against other systems, such as Active Directory. Credentials in the Secure Store are stored securely and can be managed via the Central Administration site.

This article assumes you have the SharePoint Secure Store Application configured. To complete the example, you will need access to the Central Administration site, and will need permissions to create new Target Applications and deploy solutions.

The source code for this project can be downloaded from the Microsoft TechNet Gallery, here: Retrieving Credentials from the SharePoint Secure Store using C#

Creating a Target Application in the Secure Store

Before looking at the code, we are going to step through creating a Target Application in the Secure Store.

The new Target Application we create will store a Windows Domain user credential, which we will then use in the code example to authenticate against Active Directory.

1. Browse to the Central Administration site
2. Click on Application Management
3. Click on Manage Service Applications
4. Click the Secure Store Application
5. Create a new Target Application
5.1 On the ribbon, in the Manage Target Applications, click New


5.2 In the Create New Secure Store Target Application page, enter the following information;

Target Application ID: ActiveDirectoryConnection
Display Name: Active Directory Connection
Contact E-mail: (enter your email address)
Target Application Type: Group


5.3 Click Next. On the next page, Specify the credential fields for the Secure Store Target Application, configure the fields that are used to store the credential information.

Configure the following fields:

Field NameField TypeMasked
UserNameWindows UsernameNo
PasswordWindows PasswordYes
DomainKeyNo


5.4 Click Next. On the next page, Specify the membership Settings, you need to enter administrators and members. Administrators are people who will be able to manage this target application, while members are people who will have permissions to retrieve (read) the user credentials.

Since the user credentials will be accessed via code under the context of a standard site user, we are adding Domain Users as Members. The SharePoint farm account (and any other administrators) should be added as administrators.


5.5 Click OK to save the new Target Application.
6. Set the credentials of the new Target Application
6.1 Select the new Target Application, and click Set (in the Credentials section of the ribbon)


6.2 Enter the username, password and domain of the Windows Domain user account that you want to use. This account will be used to connect to, and query, Active Directory.


6.3 Click OK to save the credential information

You have finished creating the new Target Application. The next step is to write some code that will access and use the credentials.

Building a Class to Access the Credentials


This part of the example requires creating a some classes and methods for accessing the secure store, retrieving a credential object, and returning it to the caller.

1. Create a new empty SharePoint Project (deploy as a farm solution)
2. Add the following references to the project:

Microsoft.Office.SecureStore.dll (see the reference below about finding the Microsoft.Office.SecureStore.dll in the GAC (Global Assembly Cache))
Microsoft.BusinessData.dll

3. Add a new class to the project, called SecureStoreProxy
4. Make the class as public and static

namespace SecureStoreCredentialsExample
{
    public static class SecureStoreProxy
    {
    
    }
}


5. Add the following using statements

using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using Microsoft.BusinessData.Infrastructure.SecureStore;
using Microsoft.Office.SecureStoreService.Server;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint;


4. Add the following code to the SecureStoreProxy class
4.1. Add CredentialType enum. This will be used by the GetCredentialsFromSecureStoreService method.

public enum CredentialType
{
    Domain,
    Generic
}


4.2. Add a new class to store credential information. This class implements IDisposable to ensure the classes SecureString properties are correctly disposed of. It also contains a method for returning a SecureString as a String.

public class UserCredentials : IDisposable
{

    private readonly SecureString _userName;
    public String UserName
    {
        get { return ConvertToUnsecuredString(_userName); }
    }

    public String DomainName;

    private readonly SecureString _password;
    public String Password
    {
        get { return ConvertToUnsecuredString(_password); }
    }
    public UserCredentials(SecureString username, SecureString password)
    {
        _userName = username.Copy();
        _password = password.Copy();
    }

    public UserCredentials(SecureString username, SecureString password, SecureString domain)
    {
        _userName = username.Copy();
        _password = password.Copy();
        DomainName = ConvertToUnsecuredString(domain);
    }

    private static string ConvertToUnsecuredString(SecureString securedString)
    {
        if (securedString == null) return String.Empty;
        IntPtr uString = IntPtr.Zero;
        try
        {
            uString = Marshal.SecureStringToGlobalAllocUnicode(securedString);
            return Marshal.PtrToStringUni(uString);
        }
        finally
        {
            Marshal.ZeroFreeGlobalAllocUnicode(uString);
        }
    }

    private Boolean _isDisposed;
    public void Dispose()
    {
        if (_isDisposed) return;
        _userName.Dispose();
        _password.Dispose();
        _isDisposed = true;
    }
}


4.3. Add a new public static method used to retrieve credential information from the Secure Store. This method takes an Application ID (a target application id), and the CredentialType enum as inputs, and returns a UserCredentials object.

public static UserCredentials GetCredentialsFromSecureStoreService(string applicationId, CredentialType credentialType)
{
    ISecureStoreProvider provider = SecureStoreProviderFactory.Create();
    if (provider == null)
    {
        throw new InvalidOperationException("Unable to get an ISecureStoreProvider");
    }

    using (SecureStoreCredentialCollection credentials = provider.GetCredentials(applicationId))
    {
        var un = from c in credentials
                    where c.CredentialType == (credentialType == CredentialType.Domain ? SecureStoreCredentialType.WindowsUserName : SecureStoreCredentialType.UserName)
                    select c.Credential;

        var pd = from c in credentials
                    where c.CredentialType == (credentialType == CredentialType.Domain ? SecureStoreCredentialType.WindowsPassword : SecureStoreCredentialType.Password)
                    select c.Credential;

        var dm = from c in credentials
                    where c.CredentialType == SecureStoreCredentialType.Key
                    select c.Credential;


        SecureString userName = un.First(d => d.Length > 0);
        SecureString password = pd.First(d => d.Length > 0);
        SecureString domain = dm.First(d => d.Length > 0);
        var userCredientals = new UserCredentials(userName, password, domain);
        return userCredientals;
    }
}


The method connects to the Secure Store, and retrieves the credentials of the Target Application. If the user context that the code is running isn't in the membership of the Target Application, an exception will be thrown.

Once the method has connected to the Secure Store and retrieved the Target Application's credentials (returned as a SecureStoreCredentialCollection), it parses the collection, extracting the username, password and domainname into a new UserCredential object.

5. Build the solution.


Building a Webpart that uses the Credentials to Connect to Active Directory


The final step in our example is to build a webpart the retrieves the membership of an Active Directory group.

1. Add a new standard webpart to the project called GetGroupMembership
2. Add a new reference to the project

System.DirectoryServices.AccountManagement

3. Add a the following using statements to the webparts code file

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using System.DirectoryServices.AccountManagement;
using System.Text;
using IdentityType = System.DirectoryServices.AccountManagement.IdentityType;


4. Copy the following code into the webpart file.

The webpart contains a textbox, button and label. When an enduser enters a group name and clicks submit, the webpart will connect to Active Directory, using the credentials retrieved from the Secure Store. It will search for the group, using methods in the System.DirectoryServices.AccountManagement namespace. Finally it will return a list of the groups members, and message indicating if the current user is a member of the group.

Notice how the GetPrinipalContext method retrieves the UserCredients (used to authenticated against Active Directory) from the SharePoint Secure Store, via the Proxy class we created early in this article.

For more information on using the System.DirectoryServices.AccountManagement namespace in a SharePoint project, see this article: SharePoint: Querying Active Directory from a Farm Based Solution using C#.Net

namespace SecureStoreCredentialsExample.GetGroupMembership
{
    [ToolboxItemAttribute(false)]
    public class GetGroupMembership : WebPart
    {
        private Label _results;
        private TextBox _groupName;
        private Button _submit;

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            _results = new Label();
            _groupName = new TextBox();
            _submit = new Button {Text = "Submit"};
            _submit.Click += SubmitOnClick;
        }

        protected override void CreateChildControls()
        {
            Controls.Add(_groupName);
            Controls.Add(_submit);
            Controls.Add(new LiteralControl("<br/>"));
            Controls.Add(_results);
        }

        private void SubmitOnClick(object sender, EventArgs eventArgs)
        {
            try
            {
                StringBuilder output = new StringBuilder();
                GroupPrincipal group = GetGroup(_groupName.Text);
                if (group == null)
                {
                    _results.Text = "Group not found.";
                    return;
                }
                output.Append(String.Format("Current user, {0}, {1} a member of {2}", SPContext.Current.Web.CurrentUser.Name, IsUserMemberOfGroup(group, SPContext.Current.Web.CurrentUser.Sid, IdentityType.Sid) ? "is" : "is not", group.DisplayName));
                output.Append("<br/>");
                var groupMembers = GetAllUsersInGroup(group, true);
                String members = String.Empty;
                foreach (UserPrincipal userPrincipal in groupMembers)
                {
                    members = String.Format("{0}{1}", String.IsNullOrEmpty(members) ? "" : String.Format("{0}, ", members), userPrincipal.DisplayName);
                }
                output.Append(String.Format("The current list of users in the {0} group are: {1}", group.DisplayName, members));
                _results.Text = output.ToString();
            }
            catch (Exception e)
            {
                _results.Text = e.Message;
            }
        }

        private UserPrincipal GetUser(String identity, IdentityType identityType)
        {
            PrincipalContext principalContext = GetPrincipalContext;
            return UserPrincipal.FindByIdentity(principalContext, identityType, identity);
        }

        private IEnumerable<UserPrincipal> GetAllUsersInGroup(GroupPrincipal groupPrincipal, Boolean recurse)
        {
            PrincipalSearchResult<Principal> members = groupPrincipal.GetMembers(recurse);
            return members.OfType<UserPrincipal>().ToList();
        }

        private GroupPrincipal GetGroup(String groupName)
        {
            PrincipalContext principalContext = GetPrincipalContext;
            return GroupPrincipal.FindByIdentity(principalContext, IdentityType.Name, groupName);
        }

        private Boolean IsUserMemberOfGroup(GroupPrincipal groupPrincipal, String identity, IdentityType identityType)
        {
            UserPrincipal userPrincipal = GetUser(identity, identityType);
            if (userPrincipal == null) return false;
            return userPrincipal.IsMemberOf(groupPrincipal);
        }

        private static PrincipalContext GetPrincipalContext
        {
            get
            {
                using (var userCredientals = SecureStoreProxy.GetCredentialsFromSecureStoreService("ActiveDirectoryConnection", SecureStoreProxy.CredentialType.Domain))
                {
                    var principalContext = new PrincipalContext(ContextType.Domain, userCredientals.DomainName, userCredientals.UserName, userCredientals.Password);
                    return principalContext;
                }
            }
        }
    }
}


5. Build and deploy the project
6. Add the webpart to a page
7. Enter an Active Directory group into the text box and click Submit. The webpart will search Active Directory for the group, display a message about the current users membership status, and finally enumerate and list the groups members.



See Also

SharePoint: Querying Active Directory from a Farm Based Solution using C#.Net

References

How to use the DirectoryServices Namespace in ASP
Microsoft.Office.SecureStoreService.dll
Getting credients from the Secure Store Provider
Retrieving Credentials from the SharePoint Secure Store using C#

No comments:

Post a Comment