Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / XAML

A Silverlight Sample Built with Self-Tracking Entities and WCF Services - Part 4

Rate me:
Please Sign up or sign in to vote.
4.60/5 (4 votes)
12 Apr 2011CPOL10 min read 38K   18   4
Part 4 of a series describing creation of a Silverlight business application using Self-tracking Entities, WCF Services, WIF, MVVM Light toolkit, MEF, and T4 Templates.
  • Please visit this project site for the latest releases and source code.

Article Series

This article is the last part of a series on developing a Silverlight business application using Self-tracking Entities, WCF Services, WIF, MVVM Light toolkit, MEF, and T4 Templates.

IssueVisionSTPart4.jpg

Contents

Introduction

Windows Identity Foundation (WIF) is a set of .NET Framework classes for building identity-aware applications. It provides us with a rich set of API for handling authentication, authorization, customization and any identity-related tasks. Furthermore, WIF enables .NET developers to externalize authentication and authorization by configuring application to rely on an identity provider to perform some or all those functions. In the first half of this article, we will experiment how to implement the login/logout functionality using WIF. After that, we will go over some remaining topics before we finish this article series.

Before we continue, there are two things I need to clarify: First, we will only cover the login/logout functionality using WIF from a Silverlight developer point of view. This means that we are not going to cover much on how the Security Token Service(STS) project IssueVision.ST_Sts is setup. Also, we will not go over how to configure project IssueVision.ST.Web to work with the STS project IssueVision.ST_Sts. As I mentioned above, one of the advantages of WIF is that we can outsource authentication and authorization to an identity provider (project IssueVision.ST_Sts) so that we, as Silverlight developers, can focus on how to implement our own business logic and let someone else who is expert in WIF to work on any WIF-related stuff. If you have interests in learning more about WIF. I would recommend Vittorio Bertocci's book "Programming Windows Identity Foundation" and a set of hands-on labs called "Identity Developer Training Kit". In fact, project IssueVision.ST_Sts directly models after one of the samples (OutOfBrowserApplications) from the training kit without much modification.

Second, as stated in book "Programming Windows Identity Foundation": currently, there is no WIF assembly in Silverlight 4.0 or any native claims-support capabilities. The OutOfBrowserApplications sample from "Identity Developer Training Kit" adds WIF-like capabilities to Silverlight applications, but it is largely experimental and very likely will change with the next release of Silverlight. So, please keep that in mind.

Custom Adapter Modules

To add WIF-like capabilities to our sample application, we need two custom adapter modules from the OutOfBrowserApplications sample. They are SL.IdentityModel and SL.IdentityModel.Server.

Module SL.IdentityModel

SL.IdentityModel is an assembly containing the claims object model, and it is a provisional assembly that allows us to use a subset of the WIF programming model. The source code included in our sample is from the OutOfBrowserApplications sample with a few bug fixes. For example, the class ClaimsIdentitySessionManager is modified as follows so that events are properly registered and unregistered.

C#
#region IApplicationService
 
public void StartService( ApplicationServiceContext context )
{
  Application.Current.Resources.Add( "ClaimsIdentitySessionManager", Current );
 
  _authenticationServiceClient = new AuthenticationServiceClient(
      new CustomBinding(
          new BinaryMessageEncodingBindingElement(),
          new HttpsTransportBindingElement()
          ), new EndpointAddress( this.AuthenticationServiceEndPoint ) );
 
  _authenticationServiceClient.SignInCompleted += AuthenticationServiceClient_GetClaimsIdentityComplete;
  _authenticationServiceClient.SignInWithIssuedTokenCompleted += AuthenticationServiceClient_SignInWithIssuedTokenCompleted;
  _authenticationServiceClient.SignOutCompleted += AuthenticationServiceClient_SignOutCompleted;
 
  this.User = new ClaimsPrincipal( new ClaimsIdentity() );
 
  if ( Current.IdentityProvider is WSFederationSecurityTokenService )
  {
    this.GetClaimsIdentityAsync();
  }
}
 
public void StopService()
{
  _authenticationServiceClient.SignInCompleted -= AuthenticationServiceClient_GetClaimsIdentityComplete;
  _authenticationServiceClient.SignInWithIssuedTokenCompleted -= AuthenticationServiceClient_SignInWithIssuedTokenCompleted;
  _authenticationServiceClient.SignOutCompleted -= AuthenticationServiceClient_SignOutCompleted;
}
 
#endregion

Module SL.IdentityModel.Server

SL.IdentityModel.Server is an assembly that is referenced inside project IssueVision.ST.Web, and it contains logic that can trigger authentication when necessary. One of the major classes inside this assembly is class AuthenticationService and we will discuss how it is used later.

Server-side Setup

Server-side authentication and authorization logic exists in both projects IssueVision.ST.Web and IssueVision.ST_Sts. Let us start with project IssueVision.ST_Sts first.

IssueVision.ST_Sts Project Setup

When a user signs in, a call to project IssueVision.ST_Sts to validate user name and password will first hit function ValidateToken(SecurityToken token) of class CustomUserNamePasswordTokenHandler.

C#
public override ClaimsIdentityCollection ValidateToken(SecurityToken token)
{
  UseNameSecurityToken usernameToken = token as UserNameSecurityToken;
 
  if (usernameToken == null)
  {
    throw new ArgumentException("usernameToken", "The security token is not a valid username security token.");
  }
 
  using (AuthenticationEntities context = new AuthenticationEntities())
  {
    User foundUser = context.Users.FirstOrDefault(n => n.Name == usernameToken.UserName);
 
    if (foundUser != null)
    {
      // generate password hash
      string passwordHash = HashHelper.ComputeSaltedHash(usernameToken.Password, foundUser.PasswordSalt);
 
      if (string.Equals(passwordHash, foundUser.PasswordHash, StringComparison.Ordinal))
      {
        IClaimsIdentity identity = new ClaimsIdentity();
        identity.Claims.Add(new Claim(WSIdentityConstants.ClaimTypes.Name, usernameToken.UserName));
 
        return new ClaimsIdentityCollection(new IClaimsIdentity[] { identity });
      }
      else
        throw new UnauthorizedAccessException("The username/password is incorrect");
    }
    else
      throw new UnauthorizedAccessException("The username/password is incorrect");
  }
}

This function validates the user name and password by querying the database for a matching password hash. If no match is found, an exception would be thrown. Otherwise, the signing-in process will continue and hit the next function GetOutputClaimsIdentity() of class CustomSecurityTokenService.

C#
protected override IClaimsIdentity GetOutputClaimsIdentity(IClaimsPrincipal principal, RequestSecurityToken request, Scope scope)
{
  if (null == principal)
  {
    throw new ArgumentNullException("principal");
  }
 
  ClaimsIdentity outputIdentity = new ClaimsIdentity();
 
  // Issue custom claims.
  using (AuthenticationEntities context = new AuthenticationEntities())
  {
    User foundUser = context.Users.FirstOrDefault(n => n.Name == principal.Identity.Name);
 
    if (foundUser != null)
    {
      outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name));
      if (foundUser.UserType == "A")
      {
        outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Admin"));
      }
      else if (foundUser.UserType == "U")
      {
        outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "User"));
      }
      return outputIdentity;
    }
    else
      throw new UnauthorizedAccessException("The username/password is incorrect");
  }
}

This function will return a ClaimsIdentity object with both ClaimTypes.Name and ClaimTypes.Role, and all the custom claim information will be returned back to the client side. After that, the next step is a call to the AuthenticationService class of project IssueVision.ST.Web.

IssueVision.ST.Web Project Setup

For project IssueVision.ST.Web to work with WIF, we need to first add a reference to module SL.IdentityModel.Server. By adding this module, the class AuthenticationService will be available on the server-side, and one of the functions inside this class, SignInWithIssuedToken(), is used during the login process. We are going to cover that a little later.

After adding this new reference, our next task is to add the file AuthenticationService.svc inside the Service folder. Its content is listed as follows:

<%@ ServiceHost Language="C#" Debug="true" 
Factory="SL.IdentityModel.Server.AuthenticationServiceServiceHostFactory" 
Service="SL.IdentityModel.Server.SL.IdentityModel.Server"
%>

The Factory attribute points to class AuthenticationServiceServiceHostFactory inside module SL.IdentityModel.Server, and it is used to instantiate the custom service host for AuthenticationService. The Service attribute is never used but cannot be left empty.

The last step on the server-side setup is to configure AuthenticationService so that it is available to the Silverlight client. Here are the relevant sections in Web.config file.

<configuration>
  ......
  <location path="Service/AuthenticationService.svc">
    <system.web>
      <authorization>
        <allow users="*"/>
      </authorization>
    </system.web>
  </location>
  ......
  <system.serviceModel>
    <bindings>
      <customBinding>
        <binding name="AuthenticationService.customBinding">
          <binaryMessageEncoding />
          <httpTransport />
        </binding>
      </customBinding>
    </bindings>
    ......
    <services>
      <service name="AuthenticationService">
        <endpoint address=""
          binding="customBinding" bindingConfiguration="AuthenticationService.customBinding"
          contract="AuthenticationService" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      </service>
    </services>
    ......
  </system.serviceModel>
  ......
</configuration>

The <location> section allows anyone to access the AuthenticationService service because it is called during the login process when no one is authenticated yet. The rest of the settings simply configure the AuthenticationService service that pretty much like any other service setup, and we will not cover that in detail. The Web.config file also contains a section called <microsoft.identityModel>. This section is WIF-related and should be set up by someone who also configures the WIF settings of project IssueVision.ST_Sts.

So far, we have covered all the server-side setup a Silverlight developer should know. Next, we will move on to cover the client side.

Client-side Setup

Module SL.IdentityModel is added as a reference to all client-side projects except project IssueVision.WCFService. Inside this module, our focus is mainly on class ClaimsIdentitySessionManager.

Class ClaimsIdentitySessionManager

First, we need to modify file App.xaml of IssueVision.Client project so that we have a global instance of this class:

 <Application.ApplicationLifetimeObjects>
  <id:ClaimsIdentitySessionManager
    ApplicationIdentifier=" "
    AuthenticationServiceEndPoint="https://localhost/IssueVision.ST/Service/AuthenticationService.svc">
    <id:ClaimsIdentitySessionManager.IdentityProvider>
      <id:WSTrustSecurityTokenService
        Endpoint="https://localhost/IssueVision.ST_Sts/Service.svc/IWSTrust13"
        CredentialType="Username" />
    </id:ClaimsIdentitySessionManager.IdentityProvider>
  </id:ClaimsIdentitySessionManager>
</Application.ApplicationLifetimeObjects>

The ApplicationIdentifier attribute is used for communicating to project IssueVision.ST_Sts for what application a token is being requested, and the AuthenticationServiceEndPoint attribute points to the AuthenticationService available within project IssueVision.ST.Web. Inside this ClaimsIdentitySessionManager object, there is a property called IdentityProvider. This property takes two attributes Endpoint and CredentialType. During the login process, we need to know which protocol and credential types to use for performing user authentication. Therefore, the Endpoint is set to use WS-Trust and CredentialType is set to use Username.

Within class ClaimsIdentitySessionManager, the functions SignInUsernameAsync(string username, string password) and SignOutAsync() are used in our sample for signing in and out. When SignInUsernameAsync() gets called, it first contacts IssueVision.ST_Sts to check whether the user name and password are correct. If this step of authentication passes, IssueVision.ST_Sts will create and pass a security token back to ClaimsIdentitySessionManager, and ClaimsIdentitySessionManager will continue the signing-in process by calling the AuthenticationService of project IssueVision.ST.Web. More specifically, function SignInWithIssuedToken(string xmlToken) of class AuthenticationService will get called with the security token from IssueVision.ST_Sts as the only parameter. This function will then verify the security token, and if successful, a session cookie will be created and user claims will be passed back so that they are also available on the client side.

The sign out process is relatively simple and it begins with SignOutAsync(). This function will call the SignOut() function of class AuthenticationService, which clears the session cookie created during the sign-in process.

Model Class AuthenticationModel

After we finished discussing class ClaimsIdentitySessionManager, let us talk about how this class is used by our Model class AuthenticationModel. AuthenticationModel implements interface IAuthenticationModel, which mainly provides functions for signing in and out.

C#
 public interface IAuthenticationModel : INotifyPropertyChanged
{
  void SignInAsync(string userName, string password);
  event EventHandler<SignInEventArgs> SignInCompleted;
  void SignOutAsync();
  event EventHandler<SignOutEventArgs> SignOutCompleted;
 
  Boolean IsBusy { get; }
}

Inside class AuthenticationModel, we first define a protected property called SessionManager as follows:

C#
 #region "Protected Propertes"
protected ClaimsIdentitySessionManager SessionManager
{
  get
  {
    if (_sessionManager == null)
    {
      _sessionManager = ClaimsIdentitySessionManager.Current;
 
      _sessionManager.SignInComplete += _sessionManager_SignInComplete;
      _sessionManager.SignOutComplete += _sessionManager_SignOutComplete;
    }
    return _sessionManager;
  }
}
#endregion "Protected Propertes"

Property SessionManager gives us access to the singleton object of class ClaimsIdentitySessionManager, and by using its functions SignInUsernameAsync() and SignOutAsync(), we can easily implement interface IAuthenticationModel.

C#
 /// <summary>
/// Authenticate a user with user name and password
/// </summary>
/// <param name="userName">user name</param>
/// <param name="password">password</param>
public void SignInAsync(string userName, string password)
{
  this.SessionManager.SignInUsernameAsync(userName, password);
  this.IsBusy = true;
}
 
/// <summary>
/// Logout
/// </summary>
public void SignOutAsync()
{
  this.SessionManager.SignOutAsync();
  this.IsBusy = true;
}
 
/// <summary>
/// Event handler for SignInComplete
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _sessionManager_SignInComplete(object sender, SignInEventArgs e)
{
  this.IsBusy = false;
  if (this.SignInCompleted != null)
    this.SignInCompleted(this, e);
}
 
/// <summary>
/// Event handler for SignOutComplete
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _sessionManager_SignOutComplete(object sender, SignOutEventArgs e)
{
  this.IsBusy = false;
  if (this.SignOutCompleted != null)
    this.SignOutCompleted(this, e);
}

ViewModel Classes

Model class AuthenticationModel is used by both ViewModel class MainPageViewModel and LoginFormViewModel, which eventually provide the login/logout functionality to end user. Following are the event handlers for events SignInCompleted and SignOutCompleted inside class MainPageViewModel.

C#
 /// <summary>
/// Event handler for SignInCompleted
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _authenticationModel_SignInCompleted(object sender, SL.IdentityModel.Services.SignInEventArgs e)
{
  if (e.Error == null)
  {
    if (e.User != null)
    {
      this.IsLoggedIn = e.User.Identity.IsAuthenticated;
      this.IsLoggedOut = !(e.User.Identity.IsAuthenticated);
      this.IsAdmin = e.User.IsInRole(IssueVisionServiceConstant.UserTypeAdmin);
 
      if (e.User.Identity.IsAuthenticated)
      {
        this.WelcomeText = "Welcome " + e.User.Identity.Name;
        // check whether the user needs to reset profile
        this._issueVisionModel.GetCurrentUserProfileResetAsync();
      }
    }
  }
}
 
/// <summary>
/// Event handler for SignOutCompleted
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _authenticationModel_SignOutCompleted(object sender, SignOutEventArgs e)
{
  // even if e.HasError is True, we still set logout done.
  this.IsLoggedIn = false;
  this.IsLoggedOut = true;
  this.IsAdmin = false;
  this.WelcomeText = string.Empty;
}

This concludes our discussion on how to implement the login/logout functionality using WIF. As far as authentication and authorization is concerned, we can see that modules SL.IdentityModel and SL.IdentityModel.Server provide very similar functionality as what currently is available from WCF RIA Services. One missing feature, however, is that error messages do not get propagated from project IssueVision.ST_Sts to Silverlight client properly. So, for example, if we typed a wrong password, we would always get this annoying "The remote server returned an error: NotFound" error message.

IssueVisionSTPart4_NotFound.jpg

We can easily fix a similar issue when an error message is from project IssueVision.ST.Web (not from project IssueVision.ST_Sts), and that is our next topic.

Module SL.WcfExceptionHandling

Module SL.WcfExceptionHandling is created to make WCF service exceptions available on Silverlight client-side, and the original idea comes from this post with some of my own enhancements. In order to use it, we can take one of the two following approaches.

First, you need to include module SL.WcfExceptionHandling as a reference. After that, simply add attribute SilverlightFaultBehavior to any WCF service class like the following sample code:

[SilverlightFaultBehavior]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class IssueVisionService : IIssueVisionService
{
......
}

The other approach is adding a behavior extension configuration. This can be very useful when we have no access to the source code of a WCF service class. In our sample application, we use this second approach for class PasswordResetService by simply modifying the Web.config file as follows.

<system.serviceModel>
  <extensions>
    <behaviorExtensions>
      <add name="SilverlightFaultBehavior" 
      type="SL.WcfExceptionHandling.SilverlightFaultBehavior, SL.WcfExceptionHandling, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
    </behaviorExtensions>
  </extensions>
  <behaviors>
    <serviceBehaviors>
      ......
      <behavior name="PasswordResetServiceBehavior">
        <serviceMetadata httpGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="false" />
        <SilverlightFaultBehavior />
      </behavior>
    </serviceBehaviors>
  </behaviors>
  ......
  <services>
    ......
    <service behaviorConfiguration="PasswordResetServiceBehavior"
             name="IssueVision.Service.PasswordResetService">
      <endpoint address="https://localhost/IssueVision.ST/Service/PasswordResetService.svc"
                binding="customBinding" bindingConfiguration="PasswordResetService.customBinding"
                contract="IssueVision.Service.IPasswordResetService" />
      <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
    </service>
  </services>
</system.serviceModel>

The screen shot below shows that error message from server-side is now properly displayed on Silverlight client-side.

IssueVisionSTPart4_UpdateFailed.jpg

About Updating All Columns in Self-Tracking Entities

Our last topic is about why we update all columns when updating a self-tracking entity to database. Inside book "Entity Framework 4.0 Recipes: A Problem-Solution Approach", there is a section on "Preventing the Update of All Columns in Self-Tracking Entities". To achieve that, we need the following two steps:

1) Edit the IssueVisionModel.tt template file. Change the following lines:

C#
OriginalValueMembers originalValueMembers =
    new OriginalValueMembers(allMetadataLoaded, metadataWorkspace, ef);

to the following:

C#
OriginalValueMembers originalValueMembers =
    new OriginalValueMembers(false, metadataWorkspace, ef);

This step changes the first parameter of OriginalValueMembers() to false, which tells the class that all properties should record an original value when they are changed.

2) Edit the IssueVisionModel.Context.tt template file. Change the following line:

C#
context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);

to the following:

C#
context.ObjectStateManager.ChangeObjectState(entity, EntityState.Unchanged);

The problem of this updating only modified properties is that it does not work in cases when a property's type is Nullable<T>  (T is a value type such as Int32) and its original value is null. In file IssueVisionModel.Context.Extensions.cs, we can see there is one function SetValue() defined as follows:

C#
 private static void SetValue(this OriginalValueRecord record, EdmProperty edmProperty,
  object value)
{
  if (value == null)
  {
    Type entityClrType = ((PrimitiveType)edmProperty.TypeUsage.EdmType).ClrEquivalentType;
    if (entityClrType.IsValueType &&
       !(entityClrType.IsGenericType && typeof(Nullable<>) == 
         entityClrType.GetGenericTypeDefinition()))
    {
      // Skip setting null original values on non-nullable CLR types because the
      // ObjectStateEntry won't allow this
      return;
    }
  }
 
  int ordinal = record.GetOrdinal(edmProperty.Name);
  record.SetValue(ordinal, value);
}

In our sample application, if we choose any issue with an original platform ID as null, and we update its value to one of the choices available (a non-null value), because the PlatformID is of type Nullable<int>, function SetValue() will skip recording the original value, and eventually will cause the system to skip saving the updated PlatformID. We can easily verify this by catching the update statement in SQL Server Profiler as follows:

exec sp_executesql N'update [dbo].[Issues]
set [LastChange] = @0
where ([IssueID] = @1)
',N'@0 datetime,@1 bigint',@0='2011-04-07 22:03:43.9030000',@1=10

To avoid this problem, we have to update all columns in self-tracking entities, which means that we need to revert the step 2 described above, but still keep the step 1. In fact, this is exactly what we implemented in both IssueVisionModel.tt and IssueVisionModel.Context.tt template files.

This concludes our discussion. I hope you find this article series useful, and please rate and/or leave feedback below. Thank you!

Bibliography

History

  • April, 2011 - Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

Comments and Discussions

 
GeneralPrerrequisites Pin
kiquenet.com26-Apr-11 4:53
professionalkiquenet.com26-Apr-11 4:53 
GeneralRe: Prerrequisites Pin
Weidong Shen26-Apr-11 6:56
Weidong Shen26-Apr-11 6:56 
Generalupdate codeplex? Pin
maird119-Apr-11 3:36
maird119-Apr-11 3:36 
GeneralRe: update codeplex? Pin
Weidong Shen19-Apr-11 6:41
Weidong Shen19-Apr-11 6:41 
Sorry, not quite sure of your question. The codeplex site should have the same source code as this site has.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.