V první části jsme tvorbu naší ukázkové webové aplikace pro přístup k souborům skončili tím, že jsme vytvořili Mater Page a dvě stránky Default a Login, které budeme postupně doplňovat. V této části dokončíme stránku Login, přidáme kód pro přihlášení a provedeme potřebné nastavení ve Web.config souboru.
Stránka Login
Začneme aspx kódem stránky pro přihlášení, stránka bude umožňovat zadat uživatelské jméno a heslo, případně zvolit, zda chceme přihlášení zapamatovat. Kód bude vypadat takto:
<%@ Page Title="File Access Web - Login" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="FileAccessWeb.Login" %>
<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="server">
<title>Login | File Access Web</title>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<div class="LoginContainer">
<div class="LoginTitle">
<asp:Label ID="LoginTitleLabel" Text="Zadejte uživatelské jméno a heslo." runat="server" />
</div>
<table class="UsernamePasswordTable">
<tr>
<td>
<span class="Label"><asp:Label ID="UsernameLabel" Text="Uživatelské jméno: " runat="server" /></span>
</td>
<td>
<asp:TextBox runat="server" ID="txtUsername"></asp:TextBox>
</td>
</tr>
<tr>
<td>
<span class="Label"><asp:Label ID="HesloLabel" Text="Heslo: " runat="server" /></span>
</td>
<td>
<asp:TextBox runat="server" ID="txtPassword" TextMode="Password"></asp:TextBox>
</td>
</tr>
<tr>
<td colspan="2">
<asp:CheckBox ID="chckRememberMe" runat="server" Text="Zůstat přihlášen(a)" />
</td>
</tr>
<tr>
<td colspan="2">
<div class="ErrorTextRow">
<asp:Label ID="ErrorTextLabel" runat="server" Text="" Visible="False"></asp:Label>
</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="SubmitButtonRow">
<asp:Button ID="SubmitButton" runat="server" Text="Přihlásit se" OnClick="SubmitButton_Click"/>
</div>
</td>
</tr>
</table>
</div>
</asp:Content>
Dále do projektu doplníme soubor Css/StyleSheet.css pro styly, které v login stránce používáme (odkaz na tento soubor máme již z minula ve stránce Site.Master připraven). Do souboru StyleSheet.css umístíme následující obsah:
/*Login*/
.LoginContainer
{
padding: 5px 20px 5px 20px;
border: solid 1px #cccccc;
min-height: 150px;
}
.LoginTitle {
margin: 8px 0px 12px;
font-size: 1.1em;
color: #00f;
}
.UsernamePasswordTable {
margin-left: auto;
margin-right: auto;
}
.UsernamePasswordTable td {
padding: 3px;
}
.UsernamePasswordTable .Label {
margin-left: 2px;
margin-right: 2px;
text-align: left;
}
.ErrorTextRow {
margin: 5px 2px 0px 2px;
height: 18px;
color: #EE2037;
text-align: left;
}
.SubmitButtonRow {
margin: 0px 0px 13px 0px;
text-align: right;
}
input[type="text"], input[type="password"] {
width: 200px;
border: solid 1px #707070;
padding-left: 5px;
padding-right: 5px;
}
input[type="submit"] {
background-color: #f7f3f7;
border: solid 1px #707070;
padding: 0px 8px 0px 8px;
margin: 5px 0 5px 0;
font-size: 100%;
height: 1.6em;
width: auto;
}
Událost SubmitButton_Click zatím ponecháme prázdnou, stránka vypadá po spuštění nějak takto:
AuthenticationManager
Dále se vrhneme na něco trochu zajímavějšího – doplníme potřebný C# kód. Jak už jsem naznačil minule, pro přihlášení použijeme Windows autentizaci a ověření budeme volat pomoci technologie WIF. Jako předlohu si vezmeme příklad z příspěvku Windows Identity Foundation: Náhrada za Forms autentizaci.
Do projektu vložíme potřebnou referenci na assembly Microsoft.IdentityModel.dll a System.IdentityModel.dll, dále převezmeme třídu WIFAuthentication.
Logiku ověření uživatele naimplementujeme do třídy AuthenticationManager.cs. Její kód bude následující:
internal static class AuthenticationManager
{
#region action methods
public static IClaimsPrincipal Authenticate(string userName, string password)
{
if (string.IsNullOrEmpty(userName))
{
throw new ArgumentNullException("userName");
}
var principal = AuthenticateWindowsUser(userName, password);
//Check if user is member of required groups
if (!principal.IsInRole("Administrators") && !principal.IsInRole("Remote Desktop Users"))
{
throw new AuthenticationFailedException("Nemáte požadovaná oprávnění pro přihlášení.");
}
var inputIdentity = (IClaimsIdentity)principal.Identity;
//Get Windows user DisplayName and add it to Claims
var SID = ((System.Security.Principal.WindowsIdentity)inputIdentity).User;
var user = new IMP.Security.ActiveDirectoryUser(SID);
var outputIdentity = new ClaimsIdentity(inputIdentity.AuthenticationType);
outputIdentity.Claims.Add(new Claim(ClaimTypes.Name, inputIdentity.Name));
outputIdentity.Claims.Add(new Claim("DisplayName", user.DisplayName));
return ClaimsPrincipal.CreateFromIdentity(outputIdentity);
}
#endregion
#region private member functions
private static WindowsClaimsPrincipal AuthenticateWindowsUser(string userName, string password)
{
try
{
SecurityToken securityToken = new UserNameSecurityToken(userName, password);
var handlers = FederatedAuthentication.ServiceConfiguration.SecurityTokenHandlers;
//Uses default WindowsUserNameSecurityTokenHandler
var principal = ClaimsPrincipal.CreateFromIdentities(handlers.ValidateToken(securityToken));
return (WindowsClaimsPrincipal)principal;
}
catch (SecurityTokenValidationException ex)
{
throw new AuthenticationFailedException("Chybné uživatelské jméno nebo heslo.", ex);
}
}
#endregion
}
[Serializable]
public class AuthenticationFailedException : Exception
{
public AuthenticationFailedException() : base("Authentication failed.") { }
public AuthenticationFailedException(string message) : base(message) { }
public AuthenticationFailedException(Exception innerException) : base("Authentication failed.", innerException) { }
public AuthenticationFailedException(string message, Exception innerException) : base(message, innerException) { }
protected AuthenticationFailedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
Kromě vlastního přihlášení pomoci FederatedAuthentication ještě můžeme provést test, zda je uživatel například pouze z požadovaných Windows skupin, pokud není, tak mu přístup do aplikace neumožníme. Provedeme to pomoci volání IsInRole na získaném objektu WindowsClaimsPrincipal. Zde samozřejmě můžete zvolit jinou (složitější) logiku pro ověření pouze požadovaných uživatelů.
Dále kód ještě vytáhne plné jméno uživatele pro jeho zobrazení. K tomu zde používám pomocnou třídu ActiveDirectoryUser. Jedná se o třídu, která pomoci SIDu uživatele načte jeho informace z Active directory (používá System.DirectoryServices, třídu zde uvádět nebudu, můžete si jí převzít z archivu celého příkladu). Následně se vytvoří výstupní ClaimsIdentity, do kterého získané jméno následně přidáme jako custom Claim pod jménem DisplayName.
A konečně ještě doplníme kód události SubmitButton_Click na stránce Login.
protected void SubmitButton_Click(object sender, EventArgs e)
{
try
{
var claimsPrincipal = AuthenticationManager.Authenticate(txtUsername.Text, txtPassword.Text);
WIFAuthentication.SetAuthCookie(claimsPrincipal, chckRememberMe.Checked);
Response.Redirect("~/");
}
catch (AuthenticationFailedException ex)
{
ErrorTextLabel.Text = ex.Message;
ErrorTextLabel.Visible = true;
}
}
Nastavení ve Web.config
Aby nám kód pro přihlašování fungoval, tak nejprve musíme do Web.config souboru přidat potřebná nastavení technologie WIF samotné (v tomto případě konkrétně zavedení modulu SessionAuthenticationModule), dále nastavit požadovanou autentizaci a nakonec nastavit práva k jednotlivých stránkám aplikace.
Nastavení pro SessionAuthenticationModule bude následující:
<configuration>
<configSections>
<section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</configSections>
<system.web>
...
<httpModules>
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</httpModules>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules>
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
</modules>
...
</system.webServer>
</configuration>
Protože výchozí hodnota doby zapamatování přihlášení DefaultTokenLifetime je 10 hodin, můžeme jí prodloužit tímto nastavením (například na 60 dnů):
<microsoft.identityModel>
<service>
<securityTokenHandlers>
<remove type="Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add type="Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<sessionTokenRequirement lifetime="60.00:00" />
</add>
</securityTokenHandlers>
</service>
</microsoft.identityModel>
Dále v sekci system.web nastavíme authentication na Forms, a uvedeme odkaz na naší Login stránku (její URL routou):
<authentication mode="Forms">
<forms loginUrl="~/Login"/>
</authentication>
Nyní nám již sice funguje přihlášení, ale na hlavní stránku se dostaneme i pokud přihlášeni nejsme, to ošetříme nastavením práv pro jednotlivé stránky a adresáře webu. (Zakázání stránky default pak automaticky způsobí přechod na nastavenou stránku Login.) Provedeme to tak, že celému webu globálně zakážeme přístup pro anonymního (neověřeného) uživatele (<deny users="?" /> v sekci authorization), a pak následně pomoci <allow users="*" /> v sekcích location povolíme ty umístění a stránky, které potřebujeme zpřístupnit i nepřihlášeným uživatelům. Konkrétně to nyní budou Css, Images, favicon.ico a stránka Login.aspx.
V této fázi by celé dosavadní nastavení v souboru Web.config mělo vypadat takto:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</configSections>
<microsoft.identityModel>
<service>
<securityTokenHandlers>
<remove type="Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add type="Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<sessionTokenRequirement lifetime="60.00:00" />
</add>
</securityTokenHandlers>
</service>
</microsoft.identityModel>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
<authentication mode="Forms">
<forms loginUrl="~/Login"/>
</authentication>
<identity impersonate="false"/>
<authorization>
<deny users="?" />
</authorization>
<globalization requestEncoding="utf-8" responseEncoding="utf-8" culture="auto:cs-CZ" uiCulture="auto:cs-CZ"/>
<httpModules>
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</httpModules>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules>
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
</modules>
<handlers>
<add name="AspxBlockHandler" path="*.aspx" verb="*" type="IMP.Web.NotFoundHandler" />
</handlers>
<defaultDocument>
<files>
<clear />
<add value="Default" />
</files>
</defaultDocument>
</system.webServer>
<location path="Css">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
<location path="Images">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
<location path="favicon.ico">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
<location path="Login.aspx">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
</configuration>
Master Page
Zbývá nám doplnit pár maličkostí do naší Master Page. Nejprve kód pro obsluhu události loginStatus_LoggingOut pro funkčnost odkazu odhlásit:
protected void loginStatus_LoggingOut(object sender, LoginCancelEventArgs e)
{
WIFAuthentication.SignOutAndRefresh();
}
A ještě přidáme Page_Load pro nastavení jména uživatele z našeho custom Claimu DisplayName:
protected void Page_Load(object sender, EventArgs e)
{
if (Page.User.Identity.IsAuthenticated)
{
string displayName = Page.User.FindFirst("DisplayName").Value;
var userNameLabel = (Label)loginView.FindControl("UserNameLabel");
userNameLabel.Text = string.Format("Uživatel: <span class='bold'>{0}</span>", displayName);
}
}
Kód využívá extension metodu FindFirst nad objektem IPrincipal. Třída s těmito extension metodami je popsána zde.
Tím máme druhou část hotovou, můžete vyzkoušet dosavadní funkčnost přihlášení a odhlášení.
Příště do aplikace doplníme vlastní obsah stránky default a handler na stahování souborů.
Pozn.: Může se stát, že narazíte na následující chybu:
System.InvalidOperationException
ID1074: A CryptographicException occurred when attempting to encrypt the cookie using the ProtectedData API (see inner exception for details). If you are using IIS 7.5, this could be due to the loadUserProfile setting on the Application Pool being set to false.
To může například nastat pokud používáte ApplicationPoolIdentity, a ten ve výchozím nastavení nemá vytvořen souborový profil. Oprava je v tomto případě jednoduchá (a dokonce souhlasí s tím co říká chybová zpráva), stačí v nastavení poolu zapnout volbu Load User Profile na True (více také v sekci User Profile zde).