If you are using AppFabric Access Control Services to authenticate users when they log in to your community site using Live ID, Google or some other popular identity provider, you need more than AuthorizeAttribute to make sure that users can access the content that is there for authenticated users only. In this posting I will show you hot to extend the AuthorizeAttribute so users must also have user profile filled.
Semi-authorized users
When user is authenticated through external identity provider then not all identity providers give us user name or other information we ask users when they join with our site. What all identity providers have in common is unique ID that helps you identify the user.
Example. Users authenticated through Windows Live ID by AppFabric ACS have no name specified. Google’s identity provider is able to provide you with user name and e-mail address if user agrees to publish this information to you. They both give you unique ID of user when user is successfully authenticated in their service.
There is logical shift between ASP.NET and my site when considering user as authorized.
For ASP.NET MVC user is authorized when user has identity. For my site user is authorized when user has profile and row in my users table. Having profile means that user has unique username in my system and he or she is always identified by this username by other users.
My solution is simple: I created my own action filter attribute that makes sure if user has profile to access given method and if user has no profile then browser is redirected to join page.
Illustrating the problem
Usually we restrict access to page using AuthorizeAttribute. Code is something like this.
[Authorize]
public ActionResult Details(string id)
{
var profile = _userRepository.GetUserByUserName(id);
return View(profile);
}
If this page is only for site users and we have user profiles then all users – the ones that have profile and all the others that are just authenticated – can access the information. It is okay because all these users have successfully logged in in some service that is supported by AppFabric ACS.
In my site the users with no profile are in grey spot. They are on half way to be users because they have no username and profile on my site yet. So looking at the image above again we need something that adds profile existence condition to user-only content.
[ProfileRequired]
public ActionResult Details(string id)
{
var profile = _userRepository.GetUserByUserName(id);
return View(profile);
}
Now, this attribute will solve our problem as soon as we implement it.
ProfileRequiredAttribute: Profiles are required to be fully authorized
Here is my implementation of ProfileRequiredAttribute. It is pretty new and right now it is more like working draft but you can already play with it.
public class ProfileRequiredAttribute : AuthorizeAttribute
{
private readonly string _redirectUrl;
public ProfileRequiredAttribute()
{
_redirectUrl = ConfigurationManager.AppSettings["JoinUrl"];
if (string.IsNullOrWhiteSpace(_redirectUrl))
_redirectUrl = "~/";
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
var httpContext = filterContext.HttpContext;
var identity = httpContext.User.Identity;
if (!identity.IsAuthenticated || identity.GetProfile() == null)
if (filterContext.Result == null)
httpContext.Response.Redirect(_redirectUrl);
}
}
All methods with this attribute work as follows:
- if user is not authenticated then he or she is redirected to AppFabric ACS identity provider selection page,
- if user is authenticated but has no profile then user is by default redirected to main page of site but if you have application setting with name JoinUrl then user is redirected to this URL.
First case is handled by AuthorizeAttribute and the second one is handled by custom logic in ProfileRequiredAttribute class.
GetProfile() extension method
To get user profile using less code in places where profiles are needed I wrote GetProfile() extension method for IIdentity interface. There are some more extension methods that read out user and identity provider identifier from claims and based on this information user profile is read from database. If you take this code with copy and paste I am sure it doesn’t work for you but you get the idea.
public static User GetProfile(this IIdentity identity)
{
if (identity == null)
return null;
var context = HttpContext.Current;
if (context.Items["UserProfile"] != null)
return context.Items["UserProfile"] as User;
var provider = identity.GetIdentityProvider();
var nameId = identity.GetNameIdentifier();
var rep = ObjectFactory.GetInstance<IUserRepository>();
var profile = rep.GetUserByProviderAndNameId(provider, nameId);
context.Items["UserProfile"] = profile;
return profile;
}
To avoid round trips to database I cache user profile to current request because the chance that profile gets changed meanwhile is very minimal. The other reason is maybe more tricky – profile objects are coming from Entity Framework context and context has also HTTP request as lifecycle.
Conclusion
This posting gave you some ideas how to finish user profiles stuff when you use AppFabric ACS as external authentication provider. Although there was little shift between us and ASP.NET MVC with interpretation of “authorized” we were easily able to solve the problem by extending AuthorizeAttribute to get all our requirements fulfilled. We also write extension method for IIdentity that returns as user profile based on username and caches the profile in HTTP request scope.
View Comments (3)
Thanks you for your great posts. Helped me a lot to get around WIF and Live ID missing Name property.
I too have a "Registered User without a profile problem". However, before all the MS stuff the registration and profile where just one row in a table. (less secure but that is the past).
So to get the user's profile filled out because it's info is used in my website, I built a SQL Server View on the database with the 2 tables, AspNetUsers, and "AccountProfile" left joined on AspNetUsers Id field. Now, user logs in and is directed to their "Account" page that has both. Or they can go to the menu and pick "Edit Profile". In my personal need, this works dandy. when the call to the Controller to update profile with new or changed info, I just update the fields in "AccountProfile". The SQL Server view server multiple purposed. I can just display public view of Accounts with limited information.
The SQL Server view serves multiple purposes.
Is what I was trying to type.
public see only part of SQL Server View
registered user sees also a limited display of all his compadres
registered user can see his own profile and edit it.
same SQL Sever View with different portions used.