Optionally provide private feedback to help us improve this article...

Thank you for your feedback!


Integration with Orchard CMS

Orchard is a popular open source ASP.NET MVC based CMS system. You can learn more about Orchard on the Orchard projects web site. This article is intended to help developers integrating InstantForum into an existing Orchard based web site to provide a seamless single sign on experience for end users.

Extending the Orchard Membership Provider

The code below shows how to extend the default Orchard membership service / provider to create a user within InstantForum and to also validate the user exists within InstantForum.

´╗┐using System;
using Orchard.Data;
using Orchard.Logging;
using Orchard.Models;
using Orchard.Security;
using Orchard.Users.Models;

namespace Orchard.Users.Services {

public class MembershipService : IMembershipService {

private readonly IModelManager _modelManager;
private readonly IRepository<UserRecord> _userRepository;

public MembershipService(IModelManager modelManager, IRepository<UserRecord> userRepository) {
_modelManager = modelManager;
_userRepository = userRepository;
Logger = NullLogger.Instance;
}

public ILogger Logger { get; set; }

public void ReadSettings(MembershipSettings settings) {
// accepting defaults
}

public IUser CreateUser(CreateUserParams createUserParams) {

Logger.Information("CreateUser {0} {1}", createUserParams.Username, createUserParams.Email);

var user = _modelManager.New("user");
user.As<UserModel>().Record = new UserRecord {
UserName = createUserParams.Username,
Email = createUserParams.Email
};
_modelManager.Create(user);

// add new user to InstantForum tables
// we don't need to store the password as this is managed by Orchard
int userID = InstantASP.InstantForum.Business.User.InsertUpdateUser(
new InstantASP.InstantForum.Components.User()
{
Username = createUserParams.Username,
EmailAddress = createUserParams.Email,
Password = "",
PrimaryRoleID = 3
});

// insert was successful
if (userID > 0)
{
// adding the user was OK no dupklcate email or usernames
}
else if (userID == -1)
{
throw new MembershipCreateUserException("The username already exists!");
}
else if (userID == -2)
{
throw new MembershipCreateUserException("The email address already exists!");
}

return user.As<IUser>();
}

public IUser GetUser(string username) {
var userRecord = _userRepository.Get(x => x.UserName == username);
if (userRecord == null) {
return null;
}
return _modelManager.Get(userRecord.Id).As<IUser>();
}

public IUser ValidateUser(string username, string password) {
var user = GetUser(username);
if (user != null) {
var forumUser = new InstantASP.InstantForum.Components.User(user.UserName);
if (forumUser.UserID > 0) {
return user;
}
}
}
}
}

Sharing the Forms Authentication Cookie Created By Orchard with InstantForum

To ensure the forms authentication cookie created by Orchard can be read and accessed by InstantForum you should ensure each web.config has identical <machineKey> and <authentication> elements. For further information please se Single Sign On Considerations.

By default Orchard stores the UserID from the Orchard users table within the forms authentication cookie to persist user authentication. This code can be found within the Orchard.Security.Providers.FormsAuthenticationService class as shown below...

public void SignIn(IUser user, bool createPersistentCookie) {
var now = _clock.UtcNow.ToLocalTime();
var userData = Convert.ToString(user.Id);

var ticket = new FormsAuthenticationTicket(
1 /*version*/,
user.UserName,
now,
now.Add(ExpirationTimeSpan),
createPersistentCookie,
userData,
FormsAuthentication.FormsCookiePath);

var encryptedTicket = FormsAuthentication.Encrypt(ticket);

var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.HttpOnly = true;
cookie.Secure = FormsAuthentication.RequireSSL;
cookie.Path = FormsAuthentication.FormsCookiePath;
if (FormsAuthentication.CookieDomain != null) {
cookie.Domain = FormsAuthentication.CookieDomain;
}

_httpContext.Response.Cookies.Add(cookie);
}

Orchard then retrieves the authenticated user from the same Orchard.Security.Providers.FormsAuthenticationService class using the value stored within the forms authentication cookie as shown below...

public IUser Authenticated() {
if (!_httpContext.Request.IsAuthenticated || !(_httpContext.User.Identity is FormsIdentity)) {
return null;
}

var formsIdentity = (FormsIdentity)_httpContext.User.Identity;
var userData = formsIdentity.Ticket.UserData;
int userId;
if (!int.TryParse(userData, out userId)) {
Logger.Fatal("User id not a parsable integer");
}
return _modelManager.Get(userId).As<IUser>();

}

Notice how Orchard is casting the value within the forms authentication ticket into an int.

In contrast by default InstantForum requires the users email address or username to exist within the forms authentication ticket not the UserID from the Orchard database. We consider this to be more secure as the email address or username is typically more unique than a sequential user identifier. However if you need to support duplicate usernames and / or email addresses within your database using a unique User ID or GUID may be the best approach. InstantForum by default enforces unique usernames and email addresses due to the fact we store one of these values within the forms authentication cookie to identity users.

InstantForum is not aware of the Orchard UserID so when InstantForum attempts to get the currently authenticated user from our database based on the Orchard UserID stored in the forms authentication cookie this will return null so single sign on won't persist. The code InstantForum uses the retrieve the currently authenticated user from the forms authentication cookie is shown below...

public override Components.User GetAuthenticatedUser(Enumerations.EnumLoginUsing loginUsing = Enumerations.EnumLoginUsing.EmailAddress, bool deleteAuthCookieIfUserNotFound = true)
{

System.Security.Principal.IIdentity identity =
System.Web.HttpContext.Current.User.Identity;

if (identity.IsAuthenticated)
{

Components.User User = new Components.User(identity.Name, loginUsing);

// check to ensure we retrieved user from successfully
if (((User != null) & User.UserID > 0))
{
return User;
}

}

return null;

}

Making InstantForum Aware Of the Orchard UserID

If you have your InstantForum and Orchard database within the same instance of SQL Server you can modify a stored procedure within InstantForum to get the user email address from your Orchard users table based on the Orchard UserID. We can then use this email address to populate our user object.

ALTER PROCEDURE [if_sp_SelectUser] (
@strIdentity nvarchar(255)
) AS

SET NOCOUNT ON

-- @Identity comes in as the Orrchard UserID from the forms auth cookioe
-- We perform a look-up and change this to the users email address
-- for the rest of the stored procedure to obtain user details from the InstantForum tables.
-- The users email address must be identitical between your Ordchard tables & InstantASP_Users
SET @strIdentity = (SELECT EmailAddressOrUsername FROM
[orchardcms].[dbo].[orchardtableprefix_Orchard_Users] WHERE UserID = @strIdentity);


EXEC iasp_sp_SelectUserData @strIdentity, '', '', 0, 0

DECLARE @intLocalUserID int
SET @intLocalUserID = (
SELECT IU.UserID
FROM InstantASP_Users IU (nolock)
WHERE (IU.Username = @strIdentity OR IU.UsernameEncoded = @strIdentity OR IU.EmailAddress = @strIdentity OR IU.OpenID = @strIdentity)
)

/* Selects a users extended profile information from the username within InstantASP_Users */

SELECT InstantForum_Users.* FROM InstantASP_Users (nolock) LEFT OUTER JOIN InstantForum_Users ON InstantASP_Users.UserID = InstantForum_Users.UserID WHERE InstantASP_Users.UserID = @intLocalUserID

-- select achievements for user
EXEC if_sp_SelectUserAchievements @intLocalUserID

-- select subscription plans for user
EXEC if_sp_SelectUserSubscriptionPlans @intLocalUserID

We've recently moved to a new user identity provider model for our latest InstantForum 2015-1 release so developers can more easily extend the InstantASP user storage and authentication without modifying our core code base. As part of this work we've also introduced support for ASP.NET Identity / OWIN. You can read more here.

Changing Orchard to store the email address within the forms authentication cookie

If your using an alternative database provider for Orchard such as MySQL or SQL CE it may not be possible to modify the stored procedure as shown above.

In this instance we would suggest changing the Orchard.Security.Providers.FormsAuthenticationService class as shown below. This will require that users email addresses are unique as these will be used to identify a user from the forms authentication cookie.

public void SignIn(IUser user, bool createPersistentCookie) {
var now = _clock.UtcNow.ToLocalTime();
var userData = user.Email;

var ticket = new FormsAuthenticationTicket(
1 /*version*/,
user.UserName,
now,
now.Add(ExpirationTimeSpan),
createPersistentCookie,
userData,
FormsAuthentication.FormsCookiePath);

var encryptedTicket = FormsAuthentication.Encrypt(ticket);

var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.HttpOnly = true;
cookie.Secure = FormsAuthentication.RequireSSL;
cookie.Path = FormsAuthentication.FormsCookiePath;
if (FormsAuthentication.CookieDomain != null) {
cookie.Domain = FormsAuthentication.CookieDomain;
}

_httpContext.Response.Cookies.Add(cookie);
}

Then to restive the authenticated user...

public IUser Authenticated() {
if (!_httpContext.Request.IsAuthenticated || !(_httpContext.User.Identity is FormsIdentity)) {
return null;
}

var formsIdentity = (FormsIdentity)_httpContext.User.Identity;
var userData = formsIdentity.Ticket.UserData;
return _modelManager.Get(userData).As<IUser>();

}