Tuesday, April 20, 2010

Silverlight Custom Authentication Domain Service with RIA Services

Silverlight Data and Controls Series

Introduction

One of the next things that I would like to cover is how to do a custom log in with Silverlight 4. Many of the examples and demonstrations out there show how to do windows integrated or Forms Based Authentication along with the ASP.net Authentication. These examples are not really that great when you need to hook into a custom username and password repository that is not Active Directory or SQL Server based. You could try to come up with some sort of SSO solution or write your own ASP.net custom authentication provider. However sometimes you just do not need to fight those battles because doing that is not in scope for the implementation that you have in front of you.

In the MIX 2010 conference a speaker brushed over this topic for about five minutes in his presentation (blog) and this is exactly what I needed. I was able to download the code, pull it into my solution and I was off and running. This solution gave me exactly what I wanted because it was simple, straight forward and is very flexible. His example is the basis for the solution I will present to you.

This solution will use the HTTP context along with the Silverlight’s authentication mechanism to secure all RIA Service calls. This will effectively make sure your application is somewhat secure in that no data can be retrieved unless the user session has been created via a custom authentication domain service..

Getting Started

I am going to take the MVVM solution that I created in an earlier blog and start from there. I recommend review that blog to get an understanding of the solution.

We will need to add several things to this solution to get this working:

  • Added LogonView.xaml to the View folder for the logon control.
  • Added both LogonCommand.cs and LogonViewModel.cs to the ViewModel folder.
  • Added both FormsAuthenticationService.cs and CustomAuthenticationService.cs to the Services folder. Please note that when adding this two new services to the domain model we will not be using the Authentication Domain Service, instead just use Domain Service template.
  • Added UserDTO.cs to the Model folder.
  • Added a Global.asax file which will use to instanciate domain services with the domain object.

Here is a screenshot of my updated solution.

Untitled3

User Class

Let us first start out with the user class. There is really not much to it; all we are using it for is to make accessible other data that we might retrieve as part of the Logon in process. In this case we are going to keep email address and display name for the user in the HTTP session so we do not have to continually retrieve that information to display it to the screen.

namespace MVVM.Web.Model
{
public class UserDTO : UserBase
{
public string Email { get; set; }

public string DisplayName { get; set; }
}
}

Also take note that we inherit from System.ServiceModel.DomainServices.Server.ApplicationServices.UserBase.

Forms Authentication Domain Service

Next we have the class that was provided in this presentation from the MIX 2010 conference. I made only one minor modification to it to return an error in the HTTPContext if there is an issue logging in.

This Domain Service is an abstract class that has must be implemented. It has two methods (GetCurrentUser and ValidateCredentials) that the concrete domain service must implement. As you will see the purpose of this class is to implement the System.ServiceModel.DomainServices.Server.ApplicationServices.IAuthentication interface which will be used in correlation with the RIA Services to check to see if a user object has been properly created for the session.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using System.ServiceModel.DomainServices.Server.ApplicationServices;
using System.Web;
using System.Web.Security;
using System.Security.Principal;

namespace MVVM.Web.Service
{
public class FormsAuthenticationLogonException : System.Exception
{
public FormsAuthenticationLogonException(string message) : base(message) { }
}

public abstract class FormsAuthenticationService<TUser> : DomainService, IAuthentication<TUser> where TUser : UserBase
{

protected abstract TUser GetCurrentUser(string name, string userData);

protected virtual TUser GetDefaultUser()
{
return null;
}

protected abstract TUser ValidateCredentials(string name, string password, string customData, out string userData);

public TUser GetUser()
{
IPrincipal currentUser = ServiceContext.User;
if ((currentUser != null) && currentUser.Identity.IsAuthenticated)
{
FormsAuthenticationTicket ticket = null;

FormsIdentity userIdentity = currentUser.Identity as FormsIdentity;
if (userIdentity != null)
{
ticket = userIdentity.Ticket;
if (ticket != null)
{
return GetCurrentUser(currentUser.Identity.Name, ticket.UserData);
}
}
}

return GetDefaultUser();
}

public TUser Login(string userName, string password, bool isPersistent, string customData)
{
string userData;
TUser user = ValidateCredentials(userName, password, customData, out userData);

if (user != null)
{
FormsAuthenticationTicket ticket =
ticket = new FormsAuthenticationTicket(/* version */ 1, userName,
DateTime.Now, DateTime.Now.AddMinutes(30),
isPersistent,
userData,
FormsAuthentication.FormsCookiePath);

string encryptedTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);

HttpContextBase httpContext = (HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
httpContext.Response.Cookies.Add(authCookie);
}
else
{
HttpContextBase httpContext = (HttpContextBase)ServiceContext.GetService(typeof(HttpContextBase));
httpContext.AddError(new FormsAuthenticationLogonException("Username or password is not correct."));
}

return user;
}

public TUser Logout()
{
FormsAuthentication.SignOut();
return GetDefaultUser();
}

public void UpdateUser(TUser user)
{
throw new NotImplementedException();
}
}

}

Custom Authentication Domain Service

Next is our Custom Authentication Domain Service which inherits form the Forms Authentication Domain Service we just created. It implements both getting and validating a user. Getting the user basically gets data from the session and sets the data in to UserDTO object. The validate method is where you will need to add all custom code to go out to a custom repository of usernames and passwords to validate the user. If UserDTO that is returned is null, then you will ensured that the Silverlight call to the RIA Services will fail, not allowing the user access to data.

The example below is just checking against some hard coded values as part of a proof of concept. Please change it to fit your needs.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using MVVM.Web.Model;

namespace MVVM.Web.Service
{
[EnableClientAccess()]
public class CustomAuthenticationService : FormsAuthenticationService<UserDTO>
{
protected override UserDTO GetCurrentUser(string name, string userData)
{
string[] userDataParts = userData.Split(':');

return new UserDTO()
{
Name = name,
DisplayName = userDataParts[0],
Email = userDataParts[1]
};
}

protected override UserDTO ValidateCredentials(string name, string password, string customData, out string userData)
{
UserDTO user = null;
userData = null;

//TODO: Add code to authenticate user against custom repository of username and passwords
//Below is just checking against two hard coded values and if that succeeds, then
//create the user object.

if (name == "japergis" && password == "Password!")
{
//Create the user object
user = new UserDTO();
user.Name = name;
user.DisplayName = "Apergis, Jason";
user.Email = "japergis@foo.com";
}

if (user != null)
{
//Set custom data fields for HTTP session
userData = user.DisplayName + ":" + user.Email;
}

return user;
}
}
}

Logon View Model

In this example I continue to use the MVVM pattern that I presented in an earlier blog.

The following is a ViewModel object I created for the Logon User control. There is nothing exciting except for the code within the Logon method itself. You will notice there is no code to actually go off and call a logon RIA Service “method”. Instead, you use the System.ServiceModel.DomainServices.Client.WebContextBase to log in. If there is an error with logging in, you will have to put some code in here to send a message to the user through whatever means you want.

using System;
using System.Windows.Input;
using System.ComponentModel;
using System.Collections.Generic;
using System.ServiceModel.DomainServices.Client;
using System.ServiceModel.DomainServices.Client.ApplicationServices;

using MVVM.Web;
using MVVM.Web.Model;
using MVVM.Web.Service;

namespace MVVM.ViewModel
{
public class LogonViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _username;
private string _password;
private EmployeeViewModel _evm;

public string Username {
get {
return _username;
}
set {
_username = value;
RaisePropertyChanged("Username");
}
}

public string Password {
get {
return _password;
}
set {
_password = value;
RaisePropertyChanged("Password");
}
}

public EmployeeViewModel EmployeeViewModel {
get {
return _evm;
}
set {
_evm = value;
}
}

public ICommand LogonCommand
{
get
{
return new LogonCommand(this);
}
}

public void Logon() {
LoginParameters loginParameters =
new LoginParameters(Username, Password);

WebContextBase.Current.Authentication.Login(loginParameters,
delegate(LoginOperation operation)
{
if (operation.HasError)
{
operation.MarkErrorAsHandled();

//Raise message to UI that Log in did not succeed
return;
}
else {
_evm.LoadEmployees();
}
}, null);
}

private void RaisePropertyChanged(string propertyname)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
}
}
}
}

Logon Command

Here is the code for the logon on command so that it can be wired up to the button; nothing new here.

using System;
using System.Windows.Input;

namespace MVVM.ViewModel
{
public class LogonCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private LogonViewModel _lvm = null;

public LogonCommand(LogonViewModel lvm) {
_lvm = lvm;
}

public bool CanExecute(object parameter)
{
return true;
}

public void Execute(object parameter)
{
_lvm.Logon();
}
}
}

Logon User Control

Finally here is the xaml code for the logon that uses both the LogonViewModel and LogonCommand to perform the actual logon.

<UserControl x:Class="MVVM.View.LogonView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="48" d:DesignWidth="349">

<Grid x:Name="LayoutRoot" Background="White">
<TextBox Height="23" HorizontalAlignment="Left" Margin="12,12,0,0"
Name="txtUsername" VerticalAlignment="Top" Width="120"
Text="{Binding Username, Mode=TwoWay}"/>
<PasswordBox Height="23" HorizontalAlignment="Left" Margin="138,12,0,0"
Name="txtPassword" VerticalAlignment="Top" Width="120"
Password="{Binding Password, Mode=TwoWay}"/>
<Button Content="Logon" Height="23" HorizontalAlignment="Left"
Margin="264,12,0,0" Name="btnLogon" VerticalAlignment="Top"
Width="75" Command="{Binding LogonCommand}"/>
</Grid>
</UserControl>

Web.config

Next we need to do some configuration. First we need to simply modify the web.config to use forms authentication.

<authentication mode="Forms" />

App.xaml

Next we will need to add the following to the App.xaml file in the project. For more information read this - http://msdn.microsoft.com/en-us/library/ee707353(VS.91).aspx. What this basically does is make sure the Silverlight application is using forms based authentication for the login.

<Application.ApplicationLifetimeObjects>
<app:WebContext>
<app:WebContext.Authentication>
<appsvc:FormsAuthentication></appsvc:FormsAuthentication>
</app:WebContext.Authentication>
</app:WebContext>
</Application.ApplicationLifetimeObjects>

If you do not add this to the App.xmal class you will get an error like the following when you try to do the login:

No contexts have been added to the Application's lifetime objects. For WebContextBase.Current to resolve correctly, add a single context to the lifetime objects.

Using User Object on RIA Service

One of the first things that you will want to do is use the user object in your RIA Services. A very simple approach would be to System.Web.HttpContext.Current.User to get information about the user. You can absolutely do that because all that data is right there for you. However if you want to actually use the UserDTO object here is a simple approach.

What we want to do is when a domain service is instantiated, have it receive a UserDTO object that has been loaded form the HTTP context. To do this we need to implement a DomainServiceFactory. First add a Global.asax file to the RIA Services project and in it put the following code.

You will see there is a new class called DomainServiceFactory that implements the IDomainServiceFactory interface. In the CreatedDomainService method it will check to see of the service is of a particular type; in this case an EmployeeDomainService. If so, it will create an instance of the CustomAuthenticationService, get the UserDTO object and set it into the constructor of the EmployeeDomainService.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;
using System.ServiceModel.DomainServices.Server;
using MVVM.Web.Service;
using MVVM.Web.Model;

namespace MVVM.Web
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
if (!(DomainService.Factory is DomainServiceFactory))
{
DomainService.Factory =
new DomainServiceFactory(DomainService.Factory);
}
}


internal sealed class DomainServiceFactory : IDomainServiceFactory
{
private IDomainServiceFactory _defaultFactory;

public DomainServiceFactory(IDomainServiceFactory defaultFactory)
{
_defaultFactory = defaultFactory;
}

public DomainService CreateDomainService(Type domainServiceType, DomainServiceContext context)
{
if ((domainServiceType == typeof(EmployeeDomainService)))
{
DomainServiceContext authServiceContext =
new DomainServiceContext(context, DomainOperationType.Query);

CustomAuthenticationService authService =
(CustomAuthenticationService)_defaultFactory.CreateDomainService(typeof(CustomAuthenticationService), authServiceContext);

UserDTO currentUser = authService.GetUser();

DomainService domainService = (DomainService)Activator.CreateInstance(domainServiceType, currentUser);
domainService.Initialize(context);

return domainService;
}
else
{
return _defaultFactory.CreateDomainService(domainServiceType, context);
}
}

public void ReleaseDomainService(DomainService domainService)
{
if ((domainService is EmployeeDomainService))
{
domainService.Dispose();
}
else
{
_defaultFactory.ReleaseDomainService(domainService);
}
}
}

}
}

Then we just need to modify the EmployeeDomainService so that the constructor looks like the following. Another VERY IMPORTANT note is that I have now decorated the EmployeeDomainService with the [RequiresAuthentication()] attribute. Having this is what ultimately locks down the domain service requiring that a user session be created.

    [EnableClientAccess()]
[RequiresAuthentication()]
public class EmployeeDomainService : DomainService
{
UserDTO _user;

public EmployeeDomainService(UserDTO user)
{
_user = user;
}

Note if you had not done what I have just shown and tried to reference the CustomAuthenticationService directly in the constructor of the EmployeeDomainService, you will get errors stating that the CustomAuthenticationService has not been properly initialized. This is considered the best practice for controlling the way your RIA Domain Services are to be created.

Using User Object on Silverlight Client

The last thing you will want to do is potentially get the UserDTO in your Silverlight controls. Well there is no difference. All you need to do is:

public void LoadEmployees() {
//Load the user
CustomAuthenticationContext authContext = new CustomAuthenticationContext();
authContext.Load<UserDTO>(_authContext.GetUserQuery(), UserLoaded, false);
}

private void UserLoaded(LoadOperation<UserDTO> lo)
{
foreach (var user in lo.Entities)
{
System.Diagnostics.Debug.WriteLine(user.Email);
System.Diagnostics.Debug.WriteLine(user.DisplayName);
}
}

Custom Authentication Providers

If you want do something really cool – you can also check into building your own custom authentication provider….

References

16 comments:

hehe said...

something is wrong in line "if ((currentUser != null) currentUser.Identity.IsAuthenticated)"

Maybe a && ?

Jason Apergis said...

Good catch - sorry about that - fixed.

Unknown said...

were to place:
public void LoadEmployees() {
//Load the user
CustomAuthenticationContext authContext = new CustomAuthenticationContext();
authContext.Load(_authContext.GetUserQuery(), UserLoaded, false);
}

Jason Apergis said...

Call it from the constructor of your ViewModel.

Jason

claire said...

Hi Jason, I need some help getting this running please.

When I run the application I am not getting the LogonView screen. Instead it goes straight to the empty datagrid with the following error in the IE statusbar:


Message: Unhandled Error in Silverlight Application Load operation failed for query 'GetEmployees'. Access to operation 'GetEmployees' was denied. at System.ServiceModel.DomainServices.Server.DomainService.ValidateMethodPermissions(DomainOperationEntry domainOperationEntry, Object entity)
at System.ServiceModel.DomainServices.Server.DomainService.ValidateMethodCall(DomainOperationEntry domainOperationEntry, Object[] parameters, ICollection`1 validationResults)
at System.ServiceModel.DomainServices.Server.DomainService.Query(QueryDescription queryDescription, IEnumerable`1& validationErrors, Int32& totalCount)
at System.ServiceModel.DomainServices.Hosting.QueryProcessor.Process[TEntity](DomainService domainService, DomainOperationEntry queryOperation, Object[] parameters, ServiceQuery serviceQuery, IEnumerable`1& validationErrors, Int32& totalCount)
at System.ServiceModel.DomainServices.Hosting.QueryOperationBehavior`1.QueryOperationInvoker.InvokeCore(Object instance, Object[] inputs, Object[]& outputs)
Line: 1
Char: 1
Code: 0
URI: http://localhost:1645/MVVMTestPage.aspx

How do I get the LogonView hooked up?

Many Thanks,

Shane

Jason Apergis said...

Shane - hard to tell but it looks like you may not be passing up the creditentials correctly. Review the last part of the blog.

J

claire said...

Thanks for your reply Jason. I think my problem is to do with the references in the app.xaml file. Can you post the correct references for the

xaml please?

Also, I have put the:

public void LoadEmployees()
{
//Load the user
CustomAuthenticationContext authContext = new CustomAuthenticationContext();
authContext.Load(authContext.GetUserQuery(), UserLoaded, false);
}

in the EmployeeViewModel.cs class. Note I had to change _authContext.GetUserQuery() to authContext.GetUserQuery() to make it build.

Many thanks,

Shane

GShock said...

hi, thanks for the great posts. i followed along your posts and been reading silverlight 4 in action book. i think this and the other blog post i read make a good code sample that clearly shows MVVM, WCF RIA, and new Stuff in Silverlight 4.

i'm working on some brazilian code, does anybody here suggest steps for troubleshooting control binding to ICommand? thanks for the post!

Unknown said...

The constructor of the DomainServiceFactory is called but it never calls CreateDomainService

Any ideas why?

Unknown said...

Do you have any working source code that you can share?

Jason Apergis said...

Russ - tons of people have asked for the source code in this blog series. Unfortunately the code is on a VM which is long gone by now.

Waleed said...

Hello Jason,

Great post with much details...!!
I have two questions here, if you have time to reply will be just great.

Q1: Do I need to add the LoadEmployees (which check for authentication) to every viewmodel I have and check the result ...?

Q2: where & how can I re-direct the un-authorized users to the login page?

Best regards
Waleed

Jason Apergis said...

Waleed,

I did this a long time ago. If I recall correctly:

1 - Use inheritance. You can create a hierarchy of ViewModels to centralize your code. The authentication and loading of data has to occur everytime a page is loaded.

2 - We had a solution for that but I do not have the code anymore. I recall as part of the central check we would redirect the user away from pages to a page that was not authenticated. I really wish I had that code still.

Thanks,
Jason

Yannick said...

Hello Jason,

First, thank you for this post very helpful. I have a question about custom errors.
I would return errors like these : "bad username" or "bad password", but my LoginOperation.Error instance doesn't recognize the error that i add with "httpContext.AddError(new FormsAuthenticationLogonException("Username or password is not correct."));"
So i have another error instead:
"Load operation failed for query 'Login' ... Not found."
I would get my error in the LoginOperation Instance.

Thanks for your help,
Yannick.

Gerardo said...

Hi

I'm trying to use your example, but on the GetUser method ServiceContext.User it's always null or currentUser.Identity.IsAuthenticated = false.

Any Idea?

Anonymous said...

Gerardo did you find an answer? thanks in advance