- 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.
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.
#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.
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)
{
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
.
protected override IClaimsIdentity GetOutputClaimsIdentity(IClaimsPrincipal principal, RequestSecurityToken request, Scope scope)
{
if (null == principal)
{
throw new ArgumentNullException("principal");
}
ClaimsIdentity outputIdentity = new ClaimsIdentity();
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.
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:
#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
.
public void SignInAsync(string userName, string password)
{
this.SessionManager.SignInUsernameAsync(userName, password);
this.IsBusy = true;
}
public void SignOutAsync()
{
this.SessionManager.SignOutAsync();
this.IsBusy = true;
}
private void _sessionManager_SignInComplete(object sender, SignInEventArgs e)
{
this.IsBusy = false;
if (this.SignInCompleted != null)
this.SignInCompleted(this, e);
}
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
.
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;
this._issueVisionModel.GetCurrentUserProfileResetAsync();
}
}
}
}
private void _authenticationModel_SignOutCompleted(object sender, SignOutEventArgs e)
{
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.
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.
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:
OriginalValueMembers originalValueMembers =
new OriginalValueMembers(allMetadataLoaded, metadataWorkspace, ef);
to the following:
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:
context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);
to the following:
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:
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()))
{
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
- Juval Lowy, Programming WCF Services, Third Edition, O'Reilly Media, 2010
- Julia Lerman, Programming Entity Framework, Second Edition, O'Reilly Media, 2010
- Vittorio Bertocci, Programming Windows Identity Foundation, Microsoft Press, 2010
- Larry Tenny and Zeeshan Hirani, Entity Framework 4.0 Recipes: A Problem-Solution Approach, Apress, 2010
- Identity Developer Training Kit
- T4 Template Tutorials and Solutions
- Combining Entity Framework Self-Tracking, Extensible Metadata, and Data Annotations
- Implementing Data Validation in Silverlight with INotifyDataErrorInfo
- WCF ExceptionHandling in Silverlight
History
- April, 2011 - Initial release.