Monday, October 14, 2013

Integrating ASP .NET Security with Audience Manager

Security is a very important piece in every well defined software architecture. We will find security in all the software development levels from a high level functional specification to a very detailed technical specification.

In this opportunity I will write about how to integrate ASP .NET security with Audience Manager,  Audience Manager is a very powerful Tridion feature that fits perfectly with the ASP .NET membership, roles and profiles features. In this post I will describe how to develop a ASP .NET Membership, Roles and Profile provider that uses Audience Manager.

Membership Provider

A membership provider will provide standard authentication functionality like Create, Update, Validate users as well as Change Password. The following sample will show how to Create, Update and Validate users using Audience Manager within a Membership Provider.

The first step will be to create a class that inherits from Membership Provider.

public class AudienceManagerMembershipProvider : MembershipProvider
{
    public override string ApplicationName { get; set; }

    public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) {
        ApplicationName = config["applicationName"];
        base.Initialize(name, config);
    }
}

The second step will be to implement the Create, Update and Delete methods by implementing the Membership base class.

public override MembershipUser CreateUser(
string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) {
    try {
        MembershipUser user = GetUser(email, false);
        Contact contact;
        if (user == null) {
            contact = new Contact();
        }
        else {
            contact = Contact.GetFromContactIdentificatonKeys(new string[] { email, ApplicationName });

            if (contact.SubscriptionStatus == SubscriptionStatus.OptedIn) {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }
        }

        TcmUri addressBookUri = GetAddressBookId();
        contact.AddressBookId = addressBookUri.ItemId;
        contact.EmailAddress = email;
        contact.ExtendedDetails["NAME"].Value = username;

        Crypto crypto = new Crypto();
        contact.ExtendedDetails["PASSWORD"].Value = crypto.Encrypt(password);

        contact.ExtendedDetails["IDENTIFICATION_KEY"].Value = email;
        contact.ExtendedDetails["IDENTIFICATION_SOURCE"].Value = ApplicationName;
        contact.ExtendedDetails["LAST_ACCESS_DATE"].Value = DateTime.Now;

        contact.Save();
        status = MembershipCreateStatus.Success;

        return GetUser(email, false);
    }
    catch (Exception ex) {
        throw ex;
    }
}

public override void UpdateUser(MembershipUser user) {
    Contact contact;
    try {
        contact = Contact.GetFromContactIdentificatonKeys(new string[] { user.UserName, ApplicationName });
        contact.EmailAddress = user.Email;
        contact.ExtendedDetails["EMAIL"].Value = user.Email;
        contact.SubscriptionStatus = SubscriptionStatus.OptedIn;

        contact.ExtendedDetails["LAST_ACCESS_DATE"].Value = DateTime.Now;

        contact.Save();
    }
    catch {
   }
}

public override bool ValidateUser(string username, string password) {
    Contact contact;
    try {
        if ((contact = Contact.GetFromContactIdentificatonKeys(new string[] { username, ApplicationName })) != null) {
            Crypto crypto = new Crypto();
            if (string.Compare(crypto.Encrypt(password), contact.ExtendedDetails["PASSWORD"].StringValue) == 0 && contact.SubscriptionStatus == SubscriptionStatus.OptedIn) {
                return true;
            }
        }
        return false;
    }
    catch {
        return false;
    }
}

The third step will be to configure the Membership provider. It will be configured in the Web.config file.

<membership defaultProvider="AudienceManagerMembershipProvider">
  <providers>
    <clear />
    <add name="AudienceManagerMembershipProvider"
          type="AudienceManagerMembershipProvider"
          enablePasswordRetrieval="true"
          enablePasswordReset="true"
          requiresQuestionAndAnswer="false"
          applicationName="MyApplication"
          requiresUniqueEmail="true"
          passwordFormat="Encrypted"
          maxInvalidPasswordAttempts="100"
          minRequiredPasswordLength="5"
          minRequiredNonalphanumericCharacters="0"
          passwordAttemptWindow="10"
          passwordStrengthRegularExpression="" />
  </providers>
</membership>


Roles Provider

A Roles provider will provide standard functionality to specify roles for a given user as well as validate if a given user belongs or not a group or set of groups. In this sample I will implement AddUsersToRoles, GetUserRoles and IsUserInRole. Additionally since this is an Audience Manager implementation I will use some Tridion features to define roles, In my sample I am defining them as Keywords in a Category called Roles so that we can assign roles (Keywords) to an AudienceManager contact that can be used later for personalization and segmentation.

The first step will be to create a class that inherits from RoleProvider.

public class AudienceManagerRolesProvider : RoleProvider {
    public override string ApplicationName { get; set; }
    private ConfigItems Config;
    private string RolesCategoryUri;

    public override void Initialize(string name, NameValueCollection config) {
        ApplicationName = config["applicationName"];
        RolesCategoryUri = GetRolesCategoryId();

        base.Initialize(name, config);
    }
}

The second step will be to implement AddUsersToRoles, GetUSerRoles and IsUserInRole.

public override void AddUsersToRoles(string[] usernames, string[] roleNames) {
    string userName = usernames.First();

    TaxonomyFactory factory = new TaxonomyFactory();
    IEnumerable<Keyword> roles = factory.GetTaxonomyKeywords(RolesCategoryUri).KeywordChildren.Cast<Keyword>();

    Contact contact;
    if ((contact = Contact.GetFromContactIdentificatonKeys(new string[] { userName, ApplicationName })) != null) {
        foreach (string roleName in roleNames) {
            Keyword role = roles.Where(w => w.KeywordName.Equals(roleName)).ElementAtOrDefault(0);

            if (role != null && !contact.Keywords.Contains(role.KeywordUri)) {
                contact.Keywords.Add(role.KeywordUri);
            }
        }
        contact.Save();
    }
}

public override string[] GetRolesForUser(string username) {
    Contact contact;
    if ((contact = Contact.GetFromContactIdentificatonKeys(new string[] { username, ApplicationName })) != null) {
        TaxonomyFactory factory = new TaxonomyFactory();

        IEnumerable<string> roleIds =
            contact.Keywords.OfType<string>().
            Where(w => factory.GetTaxonomyKeyword(w).TaxonomyUri.Equals(RolesCategoryUri));

        return roleIds.Select(s => factory.GetTaxonomyKeyword(s).KeywordName).ToArray();
    }
    return null;
}

public override bool IsUserInRole(string username, string roleName) {
    Contact contact;
    if ((contact = Contact.GetFromContactIdentificatonKeys(new string[] { username, ApplicationName })) != null) {
        TaxonomyFactory factory = new TaxonomyFactory();

        return contact.Keywords.OfType<string>().Any(a => {
            Keyword contactKeyword = factory.GetTaxonomyKeyword(a);
            return contactKeyword.TaxonomyUri.Equals(RolesCategoryUri) && contactKeyword.KeywordName.Equals(roleName);
        });
    }
    return false;
}

The third step will be to configure the Roles Provider.

<roleManager enabled="true" defaultProvider="AudienceManagerRolesProvider">
  <providers>
    <clear />
    <add name="AudienceManagerRolesProvider" type="AudienceManagerRolesProvider" applicationName="MyApplication" />
  </providers>
</roleManager>


Profile Provider

A profile provider will provide standard functionality to manage user profiles, as you may noticed we are just working with user names, passwords and roles, but how about the user profile like Salutation, Title, City, Age, Birth Date… fortunately all this is managed by AudienceManager and can be integrated beautifully with a Profile Provider. In the following sample I will show how to configure a user profile and how to implement read and write operations against the profiles repository (Audience Manager).

The first step will be to configure and define a profile in the Web.config file.

<profile defaultProvider="AudienceManagerProfileProvider">
  <providers>
    <clear />
    <add name="AudienceManagerProfileProvider" applicationName="MyApplication" type="AudienceManagerProfileProvider" />
  </providers>
  <properties>
    <add name="Salutation" type="string" allowAnonymous="true" customProviderData="AudienceManager" />
    <add name="Name" type="string" allowAnonymous="true" customProviderData="AudienceManager" />
    <add name="Surname" type="string" allowAnonymous="true" customProviderData="AudienceManager" />
    <add name="Birth_Date" type="datetime" allowAnonymous="true" customProviderData="AudienceManager" />
  </properties>
</profile>

Once we have configured it, ASP .NET will generate a dynamic type called ProfileCommon which will have defined properties that map directly with the profile properties, that means that we will have properties for Salutation, Name, Surname, Birth_Date. In the following sample I will be implementing methods to read and write properties from/to the profiles repository which in this case will be Audience Manager.

The second step in this sample will be to inherit from ProfileProvider class.

public class AudienceManagerProfileProvider : ProfileProvider {
    public override string ApplicationName { get; set; }

    public override void Initialize(string name, NameValueCollection config) {
        ApplicationName = config["applicationName"];
        base.Initialize(name, config);
    }
}

The third and last step will be to implement the SetPropertyValues and GetPropertyValues methods.

public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection) {
    string userName = Convert.ToString(context["UserName"]);
    try {
        Contact contact = Contact.GetFromContactIdentificatonKeys(new string[] { userName, ApplicationName });

        IEnumerable<SettingsPropertyValue> audienceManagerProperties =
            collection.Cast<SettingsPropertyValue>().Where(w => w.Property.Attributes["CustomProviderData"].Equals("AudienceManager"));

        for (IEnumerator<SettingsPropertyValue> e = audienceManagerProperties.Cast<SettingsPropertyValue>().GetEnumerator(); e.MoveNext(); ) {
            string detailName = e.Current.Name.ToUpper();
            DetailType detailType = contact.ExtendedDetails[detailName].FieldType;

            switch (detailType) {
                case DetailType.Date:
                    DateTime dateValue;
                    if (DateTime.TryParse(Convert.ToString(e.Current.PropertyValue), out dateValue)) {
                        contact.ExtendedDetails[detailName].Value = dateValue;
                    }
                    break;
                case DetailType.Decimal:
                    decimal decimalValue;
                    if (decimal.TryParse(Convert.ToString(e.Current.PropertyValue), out decimalValue)) {
                        contact.ExtendedDetails[detailName].Value = decimalValue;
                    }
                    break;
                case DetailType.Integer:
                    int intValue;
                    if (int.TryParse(Convert.ToString(e.Current.PropertyValue), out intValue)) {
                        contact.ExtendedDetails[detailName].Value = intValue;
                    }
                    break;
                default:
                    contact.ExtendedDetails[e.Current.Name.ToUpper()].Value = Convert.ToString(e.Current.PropertyValue);
                    break;
            }
        }

        contact.Save();
    }
    catch (Exception ex) {
        throw ex;
    }
}

public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection) {
    try {
        SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
        string userName = Convert.ToString(context["UserName"]);

        Contact contact = Contact.GetFromContactIdentificatonKeys(new string[] { userName, ApplicationName });

        IEnumerable<SettingsProperty> audienceManagerProperties = collection.Cast<SettingsProperty>().Where(w => w.Attributes["CustomProviderData"].Equals("AudienceManager"));
        IEnumerable<SettingsProperty> dbProperties = collection.Cast<SettingsProperty>().Where(w => !w.Attributes["CustomProviderData"].Equals("AudienceManager"));

        for (IEnumerator<SettingsProperty> e = audienceManagerProperties.GetEnumerator(); e.MoveNext(); ) {
            SettingsPropertyValue propertyValue = new SettingsPropertyValue(e.Current);
            propertyValue.PropertyValue = contact.ExtendedDetails[e.Current.Name.ToUpper()].Value;
            values.Add(propertyValue);
        }

        return values;
    }
    catch (Exception ex) {
        throw ex;
    }
}

1 comment:

  1. Great stuff, Eric!

    Glad to see some Audience Manager articles out there - and this is a fantastic example of how to integrate AM with your .NET website.
    You even included the encryption of passwords that we (used to) omit from our samples :)

    ReplyDelete