Introduction
I will describe windows authentication service, which is became as part of the Microsoft Framework 4.0 and how to use it in differnt applications such as
ASP.NET, MVC, Console, Silverlight application project types and create them in Visual Studio. I will also show how to use Authentication service from IOS (iPhone/ iPad) but
lets start from the beginning. You can find many articles about it but I will try to cover as much as possible in one place. I will aslo cover what is going on during authentication (extended version of the
http://support.microsoft.com/kb/910443 with printscreens :-) ).
You can find determination of the authentication at wiki page, such as "Authentication is the act of confirming the truth of an attribute of a datum or entity".
http://en.wikipedia.org/wiki/Authentication
Microsoft developed perfect framework and ASP.NET server controls, which allowed easily develop application with different authentication mechanisms
by using built-in authentication providers and create new. The most used/popular authentication mechanism in web sites is Forms authentication - when user has to enter user name/password/custom data. Of course, there is no perfect system to protect, but at least framework allowes to create your own authentication provider based on common interfaces. You can find how to create custom authentication provider at MSDN site.
Nowadays we use differnt platforms and clients to access to the same data by using different applications.
It brought point to have Single Sign On services. SOA architecture fits those requirements.
|
SSO sample |
Microsoft extended existing framework with Application Services, such as Authentication/Membership and Profile service, which is covered in this article:
http://msdn.microsoft.com/en-us/library/bb386582.aspx
I will describe all of 3 services and their impact on existing applications/network infrastructure, but as I said above, this article describe authentication service only.
Application infrastructure/configuration:
Multi-layered application:
front-end->service grid/application layer->data layer (3 layered application infrastructure). No limit in layers nowadays.
So, what Microsoft actually made and how to use it in application?
Talking about previous diagram we can say that application, using authentication service will be looking like this
<front-end 1/2...>-<authentication service>->Credential database/<Administrative portal>
|
SSO: multi-layered diagram |
Lets start describe application architecture from data level to front-end.
1.Database configuration
You can use MS SQL/Oracle/MySql ... databases. I will be focused on MS SQL database. Here are steps to create and configure database:
1) Create database
Im am not going to cover using Microsoft SQL Management Studio and providing T-SQL script to create one (path to data file and log file can be diffenet on your environment):
CREATE DATABASE [SSO_DB] ON PRIMARY
( NAME = N'SSO_DB', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\SSO_DB.mdf' , SIZE = 10240KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
LOG ON
( NAME = N'SSO_DB_log', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\SSO_DB_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
2) Create account, which will have access to database
I created user sso_user with db_owner permissions to database SSO_DB and password <sso_password>. I will use it to update database schema and use it in connection string.
It is better to use windows authentication, but I did it for sample.
3) create database schema
I will use tool, which comes with Microsoft .NET framework:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regsql.exe
when you run it, you have to select "Configure SQL Server for application services",
|
aspnet_regsql.exe: Configure database |
enter database instance, database "SSO_DB" and enter user name/password of sql user, which we created in step 2.
|
aspnet_regsql.exe: Enter credentials |
after you finish all steps, you can check database schema:
|
aspnet_regsql.exe: database schema |
aspnet_Users table will contain list of users. It also contain important field: ApplicationId, which is refernced to aspnet_Applications.
It means that database stores users from many applications, and you have to provide correct application name to make sure that your user will be authenticated.
Database configuration is done.
2. Administrative web site
Main task for the admin portal is to manage users/roles. You have 2 options how to fo it:
a) use Visual Studio ASP.NET configuration. You just need create empty web site, configure authentication in web.config and start do it.
b) create custom administrative portal with it's own pages and flows. You can find many samples in internet how to do it by using Membership namespace.
By my opinion, I think that Microsoft would create web site template long time ago for those purpose but it didn't do it yet.
I will be focused on first approach.
a) create web site application in Visual StudioI created solution solution with name POC.Authentication (POC - prove of concept) and created empty ASP.NET Empty Web application with name POC.Authentication.Web.Admin.
|
MS Visual Studio: create ASP.NET empty application |
b) update web.config to enable forms authentication
there are following parts to enable forms authentication
connection string - I used account, which was described in database topic
set authentication to forms
configure AspNetSqlMembershipProvider providerweb.config should looks like this
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<clear/>
<add name="LocalSqlServer"
connectionString="Data Source=.;Initial Catalog=SSO_DB;User ID=sso_user;Password=sso_password"
providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<authentication mode="Forms" />
<membership>
<providers>
<clear/>
<add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="LocalSqlServer"
enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>
</system.web>
</configuration>
All parameters for AspNetSqlMembershipProvider are very important, but at this point of time the most important parameter is applicationName.
Technically speaking one database created earlier can serve many applications. You can check database schema and will find that "aspnet_Users" table has foreign key to "aspnet_Applications" on ApplicationId column.
|
Authentication database: sql select statements |
c) create usersTo start cerate users, you have to select web application project in Visual Studio solution explorer and click on "ASP.NET configuration"
|
ASP.NET Configuration |
ASP.NET configuration uses connection string with name "LocalSqlServer" by default. but nobody stops you to change name later.
New web site will be started. Click on "Security" link.
click on "Create user" link and on next page create couple of users. Based on configuration of the "AspNetSqlMembershipProvider" you can see different field.
|
ASP.NET Configuration: create user |
you can check database content now and you will see something like that:
|
Authentication database: database content |
User creation is done.
3. Service grid - Authentication service
Next step is configure WCF service, which will be responsible for the user authentication.
Create new project with type "WCF Service Application" and name is "POC.Authentication.Services"
|
MS Visual Studio: Create WCF Application |
delete IService1.cs and Service1.svc - we are not going to use it. Instead, we will create new service with name "AuthenticationService.svc" (service name can be any), inherited from "System.Web.ApplicationServices.AuthenticationService"
Just delete IAuthenticationService.cs, AuthenticationService.svc.cs and modify AuthenticationService.svc to have this code:
<%@ ServiceHost Language="C#" Service="System.Web.ApplicationServices.AuthenticationService" Debug="true" %>
The solution should look like
|
MS Visual Studio: Solution Explorer |
one more thing: make
web.config changes to enable service
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<clear/>
<add name="LocalSqlServer"
connectionString="Data Source=.;Initial Catalog=SSO_DB;User ID=sso_user;Password=sso_password"
providerName="System.Data.SqlClient" />
</connectionStrings>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<authentication mode="Forms" />
<membership>
<providers>
<clear/>
<add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="LocalSqlServer"
enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>
</system.web>
<system.web.extensions>
<scripting>
<webServices>
<authenticationService enabled="true" requireSSL="false" />
</webServices>
</scripting>
</system.web.extensions>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding>
<!-- this is for demo only. Https/Transport security is recommended -->
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
<services>
<!-- this enables the WCF AuthenticationService endpoint -->
<service name="System.Web.ApplicationServices.AuthenticationService">
<endpoint binding="basicHttpBinding"
bindingNamespace="http://asp.net/ApplicationServices/v200"
contract="System.Web.ApplicationServices.AuthenticationService"/>
</service>
</services>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
what was changed from the original configuration file:
a) Connections and membership were taken from previous project.
b) <system.web> has been extended with <authentication mode="Forms" />
c) Section <system.web.extensions> has been added. authenticationService has attribute requireSSL, which should be enabled in production
d) <system.serviceModel> has been extended with <Bindings> - it is optional, but I enabled anonymous access to service for testing purposes
e) <system.serviceModel> has been extended with <services>, which describe service (I didn't create behaviourName, so it uses default one)
f) <system.serviceModel> has <serviceHostingEnvironment> node, which was extended with attribute aspNetCompatibilityEnabled="true". It is very iportant parameter for this service, because it is relying on ASP.NET session.
To test service, you can set "POC.Authentication.Services" as strartup project, set "AuthenticationService.svc" as startup page and press F5.
It will run my lovely "WCF Test client". If you see following app, everything is set up properly.
|
WCF Test Client: Authentication Service |
If you don't like port, which Visual Studio assigned for service, you can change it in project properties.
There is one more thing, which should be done if you want use clients, like silverlight and your wcf domain/port is different then silverlight app:
You have to enable cross-domain access to your wcf service. There eis goos article about it:
http://msdn.microsoft.com/en-us/library/cc197955(v=vs.95).aspx
in our case just add 2 files as shown below:
clientaccesspolicy.xml
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="SOAPAction">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
crossdomain.xml
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-http-request-headers-from domain="*" headers="SOAPAction,Content-Type"/>
</cross-domain-policy>
4. Client application
Client application can be any application, which will consume Authentication WCF service. I will be focus on some of them.
DAL - Data access layer
I will create multiple applications, so I will create one DAL class library, which will contain service reference to authentication service and helpers to work with cookies.
One of the conditions of current sample:
1) Authentication cookie name at service should match client form authentication cookie name
2) Encryption should be the same at service and client side
If conditions above are not accomplished, you have to create custom method to extract service authentication ticket and set it to client cookies and use that ticket every time you call services.
In Visual Studio create new class library project "POC.Authentication.DAL". Delete "Class1.cs" file. Add refernce to "System.ServiceModel".
Add service reference to existing authentication service with name "serviceAuthentication".
|
MS Visual Studio: Add WCF Service Reference |
Add new class "ServiceHelper.cs"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.ServiceModel.Channels;
using System.ServiceModel;
namespace POC.Authentication.DAL
{
public static class ServiceHelper
{
public static string uriString = @"http://mypoc.com";
public static CookieContainer GetCookies(OperationContext oc)
{
HttpResponseMessageProperty httpResponseProperty =
(HttpResponseMessageProperty)oc.IncomingMessageProperties[HttpResponseMessageProperty.Name];
if (httpResponseProperty != null)
{
CookieContainer cookieContainer = new CookieContainer();
string header = httpResponseProperty.Headers[HttpResponseHeader.SetCookie];
if (header != null)
cookieContainer.SetCookies(new Uri(uriString), header);
return cookieContainer;
}
return null;
}
public static void SetCookies(OperationContext oc, CookieContainer cookieContainer)
{
HttpRequestMessageProperty httpRequestProperty = null;
if (oc.OutgoingMessageProperties.ContainsKey(HttpRequestMessageProperty.Name))
httpRequestProperty = oc.OutgoingMessageProperties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty;
if (httpRequestProperty == null)
{
httpRequestProperty = new HttpRequestMessageProperty();
oc.OutgoingMessageProperties.Add(HttpRequestMessageProperty.Name,
httpRequestProperty);
}
httpRequestProperty.Headers.Add(HttpRequestHeader.Cookie, cookieContainer.GetCookieHeader(new Uri(uriString)));
}
public static string GetCookieHeader(CookieContainer cookieContainer)
{
return cookieContainer.GetCookieHeader(new Uri(uriString));
}
}
}
The project will look like:
|
MS Visual Studio: Authentication DAL project |
4.a) ASP.NET web site
In case of web site, client should support cookies, otherwise it will not work.
I am going to create simple web site with 2 pages only: default.aspx and login.aspx.
In Visual studio solution create ASP.NET empty application with name "POC.Authentication.Client.Web" and add 2 mentioned pages.
Add reference to "POC.Authentication.DAL" project and refernce to "System.ServiceModel".
Update web.config following way:
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<authentication mode="Forms">
<forms loginUrl="~/Login.aspx" path="/"/>
</authentication>
<authorization>
<deny users="?" />
</authorization>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_1" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None" proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost.com:56511/AuthenticationService.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_1"
contract="serviceAuthentication.AuthenticationService" name="BasicHttpBinding_AuthenticationService" />
</client>
</system.serviceModel>
</configuration>
here are some comments about configuration:
a) <system.web> extended with <authentication> node, which sets forms authentication
b) <system.web> extended with <authorization>, which allow access to web site to authenticated users only
c) <system.serviceModel> defines WCF binding and <client> configuration to connect to authentication service. In endpoint address you shoudl set port, which was defined in authentication service project.
Update
Default.aspx by adding
Welcome: <asp:LoginName ID="LoginName1" runat="server" />
Update
Login.aspx by adding
<asp:Login ID="Login1" runat="server" onauthenticate="Login1_Authenticate"/>
Update Login.aspx.cs by adding
protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
{
System.Net.CookieContainer cookieContainer = null;
bool bLogin = false;
POC.Authentication.DAL.serviceAuthentication.AuthenticationServiceClient authService = new DAL.serviceAuthentication.AuthenticationServiceClient();
string customCredential = "Not used by the default membership provider.";
using (new System.ServiceModel.OperationContextScope(authService.InnerChannel))
{
bLogin = authService.Login(Login1.UserName, Login1.Password, customCredential, Login1.RememberMeSet);
cookieContainer = POC.Authentication.DAL.ServiceHelper.GetCookies(System.ServiceModel.OperationContext.Current);
}
if (bLogin == true)
{
//set authentication cookies
Response.AddHeader("Set-Cookie", POC.Authentication.DAL.ServiceHelper.GetCookieHeader(cookieContainer));
string strRedirect;
strRedirect = Request["ReturnUrl"];
if (strRedirect == null)
strRedirect = "default.aspx";
Response.Redirect(strRedirect, true);
}
}
Now you can test it by seting "POC.Authentication.Client.Web" as startup project, "Default.aspx" as startup page and pressing F5.
Enter user name/password on login page.
|
ASP,NET Application: test authentication |
If everything is ok, you will be redirected on default.aspx with welcome message.
Done.
What actually happening on web site? To understand it, I will use my favorite tool Fiddler.
I will do following changes to catch all http requests/responses coming to and from authentication service.
a) I will update web.config with putting this under <Configuration>
<!--capture all requests from front-end to WCF services-->
<system.net>
<defaultProxy>
<proxy proxyaddress="http://127.0.0.1:8888" usesystemdefault="False"/>
</defaultProxy>
</system.net>
It will allow send all outgoing requests via Fiddler proxy (By default, Fiddler captures all requests, coming from current user, but if your application uses different credentials it will not work. Method, described above allows catch everything coming from current web application).
b) I will update hosts file, located at <Windows folder>\System32\drivers\etc\hosts with this entry
127.0.0.1 localhost.com
it will make service accessible by localhost and localhost.com
and will update wcf service url from
http://localhost:56511/AuthenticationService.svc to
http://localhost.com:56511/AuthenticationService.svc
(update port number with yours)
Now: start Fiddler and repeat the same testing steps, which you did before.
you should see following requests in Fiddler.
|
Fiddler: Authentication requests |
If you double click on "Authentication.svc" request you will see request/response from service:
|
Fiddler: Authentication request/resposne |
if you check cookies of service the response, you will see that service sends cookie, which contain authentication ticket:
|
Fiddler: Authentication service response - cookie |
That is why we created helper class in DAL to extract cookies from service operational context.
if you double click on "default.aspx" request after redirection, you will see the same cookie, which were sent by authentication service.
|
Fiddler: Authentication Service Request - cookies |
The achievement is: user is authenticated on web site + we can extract authentication ticket from cookies to authorize user for other service calls (I will describe that process in next article on the sample of Role Service).
4.b) MVC web site
MVC application is based on Model->View->Controller pattern. The application configuration is pretty same but implementation is slightly different.
Add new ASP.NET MVC 3 project "POC.Authentication.Client.WebMVC" to the existing solution.
|
MS Visual Studio: Create MVC3 Application |
on the next step you will be prompted to select MVC project type. The easiest way for our scenario is to select MVC project with forms authentication:
|
MS Visual Studio: MVC3 Application creation properties |
You can review existing project and you will find that it uses basic authentication and web.config has settings for the membership provider to connect to database.
Instead of direct database connection we will use authentication WCF service.
Project contains functionality, related with registration, but I will nor cover it in this artice and focusting on authentication process only.
We will add reference to existing DAL project, which was used in previous project:
|
MS Visual Studio: Add project reference to DAL |
update web.config by removing <connectionstrings>, <membership>, <profile>, <rolemanager> and adding <system.serviceModel>, which we used in previous sample.
web.config should looks like:
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="webpages:Version" value="1.0.0.0"/>
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>
</compilation>
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
<pages>
<namespaces>
<add namespace="System.Web.Helpers" />
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Web.WebPages"/>
</namespaces>
</pages>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_1" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None" proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost.com:56511/AuthenticationService.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_1"
contract="serviceAuthentication.AuthenticationService" name="BasicHttpBinding_AuthenticationService" />
</client>
</system.serviceModel>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Update "AccountController.cs" action "public ActionResult LogOn(LogOnModel model, string returnUrl)" by following way:
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
System.Net.CookieContainer cookieContainer = null;
bool bLogin = false;
POC.Authentication.DAL.serviceAuthentication.AuthenticationServiceClient authService = new DAL.serviceAuthentication.AuthenticationServiceClient();
string customCredential = "Not used by the default membership provider.";
using (new System.ServiceModel.OperationContextScope(authService.InnerChannel))
{
bLogin = authService.Login(model.UserName, model.Password, customCredential, model.RememberMe);
cookieContainer = POC.Authentication.DAL.ServiceHelper.GetCookies(System.ServiceModel.OperationContext.Current);
}
if (bLogin == true)
{
//set authentication cookies
//Response.AddHeader("Set-Cookie", POC.Authentication.DAL.ServiceHelper.GetCookieHeader(cookieContainer));
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName);
cookie.Value = cookieContainer.GetCookies(new Uri(POC.Authentication.DAL.ServiceHelper.uriString))[FormsAuthentication.FormsCookieName].Value;
Response.SetCookie(cookie);
if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
&& !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
return Redirect(returnUrl);
else
return RedirectToAction("Index", "Home");
}
else
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
// If we got this far, something failed, redisplay form
return View(model);
}
as you can see, we deleted FormAuthentication methods with calling wcf service. There is one more change: instead of using Response.AddHeader we use
Response.SetCookie(cookie); because previous method doesn't work when you use MVC RedirectToAction.
//TODO: create helper
Run application, click on "Logon" link at the upper right corner of the web page and on appeared page enter user name/password.
|
MVC Application: Test authentication |
Click "Log on" button and you will be redirected to home page, where you can see signed in user name. Uer name is rendered by _LogOnPartial.cshtml partial view by accessing User.Identity.Name.
Done.
4.c) Silverlight application
Current sample is not perfect solution but minimum configuration and minimum UI. In Silverlight there is new technology: RIA services, which is preferrebale for the Silverlight, but I want show that WCF is also good solution in Silverlight.
In Visual studio create new Silverlight application with name "POC.Authentication.Client.Silverlight".
|
MS Visual Studio: create Silverlight application |
On the second step of the project creation, visual studio will prompt to select hosting for the silverlight. You hav an option to select existing web project or create new one.
I selected to create new one with name "POC.Authentication.Client.Silverlight.Web" - it will be used for silverlight testing:
|
MS Visual Studio: New Silverlight Application parameters |
Next step is make referece to existing WCF authentication service. I would use DAL project, but I cannot make refernce to it from silverlight project - sucks.
Ok, back to the reality - when you add service reference with name "serviceAuthentication" you will see solution like this:
ServiceReferences.ClientConfig configuration file will contain wcf binding.
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_AuthenticationService" maxBufferSize="2147483647"
maxReceivedMessageSize="2147483647">
<security mode="None" />
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:56511/AuthenticationService.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_AuthenticationService"
contract="serviceAuthentication.AuthenticationService" name="BasicHttpBinding_AuthenticationService" />
</client>
</system.serviceModel>
</configuration>
Next step is update
MainPage.xaml
<UserControl x:Class="POC.Authentication.Client.Silverlight.MainPage"
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="300" d:DesignWidth="400" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
<Grid x:Name="LayoutRoot" Background="White">
<Button Content="Login" Height="23" HorizontalAlignment="Left" Margin="325,0,0,0" Name="btnLogin" VerticalAlignment="Top" Width="75" Click="loginControl_LoginClick" />
<sdk:Label Height="17" HorizontalAlignment="Left" Margin="12,25,0,0" Name="lblUserName" VerticalAlignment="Top" Width="102" Content="User Name:" />
<sdk:Label Height="17" HorizontalAlignment="Left" Margin="12,67,0,0" Name="lblPassword" VerticalAlignment="Top" Width="102" Content="Password:" />
<PasswordBox Height="23" HorizontalAlignment="Left" Margin="149,67,0,0" Name="txtPassword" VerticalAlignment="Top" Width="131" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="149,25,0,0" Name="txtUserName" VerticalAlignment="Top" Width="130" />
<sdk:Label Height="28" Margin="149,0,52,0" Name="statusText" VerticalAlignment="Top" Content="Not Signed in" />
<sdk:Label Height="28" HorizontalAlignment="Left" Margin="12,0,0,0" Name="label3" VerticalAlignment="Top" Width="120" Content="Status:" />
</Grid>
</UserControl>
and code behind "
MainPage.xaml.cs":
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using POC.Authentication.Client.Silverlight.serviceAuthentication;
namespace POC.Authentication.Client.Silverlight
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
checkIfUserLoggedIn();
}
private void loginControl_LoginClick(object sender, RoutedEventArgs e)
{
serviceAuthentication.AuthenticationServiceClient client = new serviceAuthentication.AuthenticationServiceClient();
if (btnLogin.Content.Equals("Login"))
{
client.LoginCompleted += new EventHandler<serviceAuthentication.LoginCompletedEventArgs>(client_LoginCompleted);
client.LoginAsync(txtUserName.Text, txtPassword.Password, "", true, txtUserName.Text);
}
else
{
client.LogoutAsync();
client.LogoutCompleted += new EventHandler<System.ComponentModel.AsyncCompletedEventArgs>(client_LogoutCompleted);
}
}
void client_LogoutCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
checkIfUserLoggedIn();
}
void checkIfUserLoggedIn()
{
serviceAuthentication.AuthenticationServiceClient client = new serviceAuthentication.AuthenticationServiceClient();
client.IsLoggedInAsync("user1");
client.IsLoggedInCompleted += new EventHandler<IsLoggedInCompletedEventArgs>(client_IsLoggedInCompleted);
}
void client_LoginCompleted(object sender, LoginCompletedEventArgs e)
{
ShowHideLogin(e.UserState,e.Result);
bool loggedIn = e.Error == null;
if (e.Error != null) statusText.Content = e.Error.ToString();
}
private void ShowHideLogin(object userState, bool loggedIn)
{
Visibility b = System.Windows.Visibility.Visible;
if (loggedIn)
{
b = System.Windows.Visibility.Collapsed;
statusText.Content = userState + " logged In result:" + loggedIn.ToString();
btnLogin.Content = "Loged out";
}
else
{
statusText.Content = "Not logged In";
btnLogin.Content = "Login";
}
txtUserName.Visibility = b;
txtPassword.Visibility = b;
lblUserName.Visibility = b;
lblPassword.Visibility = b;
}
void client_IsLoggedInCompleted(object sender, IsLoggedInCompletedEventArgs e)
{
ShowHideLogin("", e.Result);
if (e.Error != null) statusText.Content = e.Error.ToString();
}
}
}
The coding is done.
Test Silverlight solution
To set silverlight solution, set "POC.Authentication.Client.Silverlight.Web" as startup project and "POC.Authentication.Client.SilverlightTestPage.aspx" as start page.
When you run application, you will see following UI:
Just eneter user name/password and press login button. You will see logged in result as true nd you will see user name.
Every time application loaded i call additional method to check if user is logged in.
Silverlight Application - Authentication process analysis in a fiddler:
To understand what is happening with application, I will review fiddler in silverlight application, which was created in previous sample.
1) on Application startup you can see following requests:
|
Fiddler: Silverlight Application - isLoggedIn request |
as you can see, we have following requests to server:
http://localhost:8595/POC.Authentication.Client.SilverlightTestPage.aspx
http://localhost:56511/clientaccesspolicy.xml - client access policy for wcf service
http://localhost:56511/AuthenticationService.svc - calls isLoggedIn method. (fisrt time user is not logged in)
2) Signin process:
you will see just one call:
http://localhost:56511/AuthenticationService.svc - to call Login method.
You will be able to see user name and password. That is why is so important to use https protocol, so nobody can catch user name/password in plan format.
|
Fiddler: Silverlight Application - authentication process (requests) |
If you will check response headers, you will see that response sets client cookies:
|
Fiddler: Authentication service response - set cookies |
3) Authentication magic
For every next call, client will pass client cookies and you can use it as user authentication on wcf side. If you refresh browser page by Ctrl+F5 (Internet Explorer) you will see that next call will contain authentication cookies in request:
http://localhost:56511/AuthenticationService.svc - calls isLoggedIn method. (User is logged in)
|
Fiddler: Silverlight Application - passing authentication cookies in request |
5.d) Apple IOS (iPhone/iPad)
For this type of application you should use MAC OS with installed XCode development tool. Your iPad/iPhone should have wired/wireless connection to the same network or internet where your service is located.
I published service to my company public domain and use internet connection for testing - real life scenario. You have many options to call WCF services from objective C. You should remember one thing: If you want create reusable code between MAC OS and IOS you should select framework carefully. MAC OS has one tool, called
WSMakeStubs but it works very bad with complex services and it doesn't work with IOS.
You have following options here:
1) use old fashion way to send http request and parse response from server by converting response body to Xml and parse it to extract values
2) you can use 3rd party tool to generate proxy classes. The sad part of this - Xcode doesn't have built-in tool to do it, like Microsoft Visual Studio does. I checked some of libraries and will use WSDL2ObjC tool (version 0.7). It based on 1st method and generates proxy classes (at least it works with Authentication service without any problem).
ok. Here are steps to create iPhone application, which will consume WCF service:
Run Xcode and created iPhone Application:
|
XCode: Create iPhone App |
Please, uncheck "Use Automatic reference Counting". The version of WSDL2ObjC is generating classes with no "ACR" - hope that next version will do it.
|
XCode: Project Sumary |
Generate Proxy classes:
Run WSDL2ObjC application. You have to insert url to your WCF service wsdl in the first input text box and select path where generated classes will be located.
|
WSDL2ObjC |
I created "Services" subfolder to keep generated classes out of application functionality. I added all generated files to the project, so you can see, what was generated in the result:
|
WSDL2ObjC: generated classes |
Prepare iPhone application to use generated proxy classes:
As I mentioned before, WSDL2OBJC is based on work with generating http requests and parsing Response as an XML to classes. To make it work you have to make couple settings in project, which described here:
http://code.google.com/p/wsdl2objc/wiki/UsageInstructions
following print screens show what should be done:
|
XCode: Linked Frameworks and Libraries |
|
XCode: Build Settings - Header Search Paths |
|
XCode: Build Settings - Other Linker Flags |
Your project is compilable now.
Pepare iPhone Application UI:
I am using Xcode version 4.3.2 and my Ui will be based on StoryBoard. I will update home view as shown below
|
XCode: home view |
Default View contains user name label and Sign-in button if user is not signed in. When user will click on "Sign-in" button he will see login screen. I created another view this way.
|
XCode: login view |
For the Login View I created another controller with name "
LoginViewController", which will contain login functionality.
|
XCode: add view controller |
Next step is to create Seques between Views (views flow).
First seque will be from "Sign-in" button to "LoginView". I simply clicked on "Sign-in" button once and dragged arrow to Login View. I also set name "PerformLoginSeque" to created seque.
|
XCode: create seque |
Another seque will be back from login view back to home view. In this case links will be from view to view directly (no controls involved). I set name "PerformBackToMainSeque" for that one.
Update ViewControllers with code:
ViewController
a) ViewController.h source code
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController <UIActionSheetDelegate, UIAlertViewDelegate>
@property (retain) NSString *userName;
@property (retain) NSHTTPCookie *authToken;
@property (retain, nonatomic) IBOutlet UILabel *lblUserName;
@property (retain, nonatomic) IBOutlet UIButton *btnSignIn;
@end
b) ViewController.m source code
#import "ViewController.h"
#import "LoginViewController.h"
@interface ViewController ()
@end
@implementation ViewController
@synthesize lblUserName;
@synthesize btnSignIn;
@synthesize userName;
@synthesize authToken;
- (void)viewDidLoad
{
[super viewDidLoad];
//Check if user is Authenticated
//TODO: add logic to call service by applying authToken
NSString* usertext=@"";
if (self.userName==NULL)
{
usertext = @"You are not signed in.";
}
else {
btnSignIn.hidden = true;
usertext =[NSString stringWithFormat:@"%@ %@ \nToken: %@", @"You successfully signed in as user:", self.userName, self.authToken];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Info" message:usertext delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil , nil];
[alertView show];
}
[lblUserName setText: usertext ];
}
- (void)viewDidUnload
{
[self setLblUserName:nil];
[self setBtnSignIn:nil];
[super viewDidUnload];
// Release any retained subviews of the main view.
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (void)dealloc {
[lblUserName release];
[btnSignIn release];
[super dealloc];
}
@end
LoginViewController
To have access to controls at view level from controller you have to inherit view from
LoginViewController.
|
XCode: associate view with controller |
Make sure "Assistance Editor" is visible.
|
XCode: display Assistant Editor |
Next step is to create IBOutlets for every control, which you will have access to.
|
Xcode: Make textbox as public property of the controller |
|
Xcode: Specify connection type |
To handle events from "Login" and "Cancel" button you have to create IBActions. Do the same as was described in previous step, but use IBAction in connection type.
As for LoginViewController:
In my sample i will make asynchronous call, so it is very important to make delegation from "
BasicHttpBinding_AuthenticationServiceBindingResponseDelegate" class. I will also pass user name and authentication cookie (token) to home form. That is why I implemented
prepareForSegue method
.
Don't forget to add
#import "AuthenticationServiceSvc.h"
Full source code for "LoginViewController.h" and "LoginViewController.m"is shown below.
c) LoginViewController.h source code
#import <UIKit/UIKit.h>
#import "AuthenticationServiceSvc.h"
@interface LoginViewController : UIViewController <BasicHttpBinding_AuthenticationServiceBindingResponseDelegate, UIAlertViewDelegate>
@property (retain, nonatomic) IBOutlet UITextField *txtUserName;
@property (retain, nonatomic) IBOutlet UITextField *txtPassword;
@property BOOL isAuthenticated;
@property (assign) NSHTTPCookie *authToken;
- (IBAction)btnCancel_Click:(UIButton *)sender;
- (IBAction)btnLogin_Click:(UIButton *)sender;
@end
c) LoginViewController.m source code
#import "LoginViewController.h"
#import "ViewController.h"
#import "AuthenticationServiceSvc.h"
@interface LoginViewController ()
@end
@implementation LoginViewController
@synthesize txtUserName;
@synthesize txtPassword;
@synthesize isAuthenticated;
@synthesize authToken;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)viewDidUnload
{
[txtUserName release];
[txtPassword release];
[super viewDidUnload];
// Release any retained subviews of the main view.
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
- (IBAction)btnLogin_Click:(UIButton *)sender {
BasicHttpBinding_AuthenticationServiceBinding *b = nil;
@try {
b = [[AuthenticationServiceSvc BasicHttpBinding_AuthenticationServiceBinding] retain];
b.logXMLInOut = YES;
b.authUsername = @"";
b.authPassword = @"";
b.cookies = nil;
AuthenticationServiceSvc_Login *cRequest = [[AuthenticationServiceSvc_Login alloc] autorelease];
cRequest.username = txtUserName.text;; //@"user1";
cRequest.password =txtPassword.text; //@"Pwd2";
cRequest.isPersistent = NO;
//do asynchronious call
[b LoginAsyncUsingParameters:cRequest delegate:self];
}
@catch (NSException * e) {
NSLog(@"Exception: %@", e);
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Alert" message:e.description delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil , nil];
[alertView show];
}
@finally {
[b release];
}
}
//receive async call to Authenticate service
- (void) operation:(BasicHttpBinding_AuthenticationServiceBindingOperation *)operation completedWithResponse:(BasicHttpBinding_AuthenticationServiceBindingResponse *)response
{
@try {
//check reponse error
if (response!=nil)
{
if (response.error!=nil)
{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Alert" message:response.error.description delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil , nil];
[alertView show];
return;
}
NSArray *responseHeaders = response.headers;
NSArray *responseBodyParts = response.bodyParts;
for(id bodyPart in responseBodyParts) {
/****
* SOAP Fault Error
****/
if ([bodyPart isKindOfClass:[SOAPFault class]]) {
//Display message that error happened
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Alert" message:((SOAPFault *)bodyPart).simpleFaultString delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil , nil];
[alertView show];
break;
}
//get response
AuthenticationServiceSvc_LoginResponse *loginRespponse = (AuthenticationServiceSvc_LoginResponse *)bodyPart;
if (loginRespponse.LoginResult.boolValue)
{
self.isAuthenticated = YES; //mark that user is authenticated
//extract authentiction token from header
for (NSHTTPCookie *cookie in operation.binding.cookies) {
//NSLog(@"Cookie:\n%@-%@",cookie.name, cookie.value); //NSHTTPCookie
NSLog(@"Cookie:\n%@",cookie.description);
self.authToken = cookie;
}
for(id header in responseHeaders) {
// here do what you want with the headers, if there's anything of value in them
}
//check if response is ok and redirect user back to main page
[self performSegueWithIdentifier:@"PerformBackToMainSeque" sender:self];
}
else {
//Display message that error happened
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Check user name/password and try again." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil , nil];
[alertView show];
}
}
}
}
@catch (NSException * e) {
NSLog(@"Exception: %@", e);
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Alert" message:e.description delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil , nil];
[alertView show];
}
}
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:@"PerformBackToMainSeque"])
{
if (self.isAuthenticated) //check if user is authenticated
{
ViewController *vc = [segue destinationViewController];
vc.userName =txtUserName.text; //pass user name to main view
vc.authToken = self.authToken; //token should be taken from service call
}
}
}
- (IBAction)btnCancel_Click:(UIButton *)sender {
[self performSegueWithIdentifier:@"PerformBackToMainSeque" sender:self];
}
@end
iPhone application coding is Done.
Run iPhone Application to test Authentication service:
To test application make sure you set "iPhone 5.1 simulator" (where 5.1 is my current IOS version on iPhone and iPad. Yours can be different).
|
XCode: select simulator |
Run application by pressing "Command+R". You will see home screen with message that user is not authenticated. Click on "Sign-in" button and you will see login screen.
|
XCode: Test application - Home view |
On login screen enter user name and password, which you used in previous samples. You have 2 cases here: successful authentication and failure (failure can happen by different reasons, like user name/password is not valid or technical problems, like service/network connection unavailable)
|
XCode: Test application - Login View |
In case of technical problems, like network issue you can see something like this:
|
XCode: Test application - No Internet Connection |
If everything is ok, you will be redirected to home view. Home view will display message, which contains "Authentication Cookie", generated by service and extracted by application from response cookies. You can use that cookie to pass as authentication token in futures calls.
|
XCode: Test application - successful login |
Current sample shows basics how to use Authentication service, but you can extend this idea for your own purposes.