Reporting Services Single Sign On (SSO) Authentication - Part 2

The first post in this series focused on creating some core validation logic to validate a user request.  With the core validation completed the next step is to wire up all the pieces required by SQL Reporting Services.  This includes implementation of the security extension interfaces and configuration files.  The IAuthenticationExtension interface requires implementing the GetUserInfo, IsValidPrincipalName, LogonUser and SetConfiguration methods.  Leveraging the ValidationManager logic from the previous post the logic for these methods becomes simple.  A lot of the logic below is from the Forms Authentication sample.  IsValidPrincipalName and LogonUser use the ValidationManager.

    1: public class Authentication : IAuthenticationExtension {
    2:  
    3:     #region IAuthenticationExtension Members
    4:     /// <summary>
    5:     /// Required by IAuthenticationExtension. The report server calls the 
    6:     /// GetUserInfo methodfor each request to retrieve the current user 
    7:     /// identity.
    8:     /// </summary>
    9:     /// <param name="userIdentity">represents the identity of the current 
   10:     /// user. The value of IIdentity may appear in a user interface and 
   11:     /// should be human readable</param>
   12:     /// <param name="userId">represents a pointer to a unique user identity
   13:     /// </param>
   14:     public void GetUserInfo(out System.Security.Principal.IIdentity userIdentity, out IntPtr userId) {
   15:         // If the current user identity is not null,
   16:         // set the userIdentity parameter to that of the current user 
   17:         if ( HttpContext.Current != null
   18:               && HttpContext.Current.User != null ) {
   19:             userIdentity = HttpContext.Current.User.Identity;
   20:         } else {
   21:             // The current user identity is null. This happens when the user attempts an anonymous logon.
   22:             // Although it is ok to return userIdentity as a null reference, it is best to throw an appropriate
   23:             // exception for debugging purposes.
   24:             // To configure for anonymous logon, return a Gener
   25:             System.Diagnostics.Debug.Assert( false, "Warning: userIdentity is null! Modify your code if you wish to support anonymous logon." );
   26:             throw new NullReferenceException( "Anonymous logon is not configured. userIdentity should not be null!" );
   27:             //userIdentity = new GenericIdentity( "anon" );
   28:         }
   29:  
   30:         // initialize a pointer to the current user id to zero
   31:         userId = IntPtr.Zero;
   32:     }
   33:  
   34:  
   35:     /// <summary>
   36:     /// The IsValidPrincipalName method is called by the report server when 
   37:     /// the report server sets security on an item. This method validates 
   38:     /// that the user name is valid for Windows.  The principal name needs to 
   39:     /// be a user, group, or builtin account name.
   40:     /// </summary>
   41:     /// <param name="principalName">A user, group, or built-in account name
   42:     /// </param>
   43:     /// <returns>true when the principle name is valid</returns>
   44:     public bool IsValidPrincipalName(string principalName) {
   45:         ValidationManager mgr = new ValidationManager();
   46:         return mgr.ValidatePrincipalName( principalName);
   47:     }
   48:  
   49:     /// <summary>
   50:     /// Indicates whether a supplied username and password are valid.
   51:     /// </summary>
   52:     /// <param name="userName">The supplied username</param>
   53:     /// <param name="password">The supplied password</param>
   54:     /// <param name="authority">Optional. The specific authority to use to
   55:     /// authenticate a user. For example, in Windows it would be a Windows 
   56:     /// Domain</param>
   57:     /// <returns>true when the username and password are valid</returns>
   58:     public bool LogonUser(string userName, string password, string authority) {
   59:         ValidationManager mgr = new ValidationManager();
   60:         return mgr.ValidateUserInfo(HttpContext.Current.Request.Headers);
   61:     }
   62:  
   63:     #endregion
   64:  
   65:     #region IExtension Members
   66:     /// <summary>
   67:     /// You must implement LocalizedName as required by IExtension
   68:     /// </summary>
   69:     public string LocalizedName {
   70:         get { 
   71:             //throw new NotImplementedException();
   72:             return null;
   73:         }
   74:     }
   75:  
   76:     /// <summary>
   77:     /// You must implement SetConfiguration as required by IExtension
   78:     /// </summary>
   79:     /// <param name="configuration">Configuration data as an XML
   80:     /// string that is stored along with the Extension element in
   81:     /// the configuration file.</param>
   82:     public void SetConfiguration(string configuration) {
   83:         //XmlDocument doc = new XmlDocument();
   84:  
   85:         //doc.LoadXml(configuration);
   86:         //if ( doc.DocumentElement.Name == "SecurityConfiguration" ) {
   87:         //    foreach ( XmlNode child in doc.DocumentElement.ChildNodes ) {
   88:         //        if ( child.Name == "ConnectionString" ) {
   89:         //            _userValidationConnectionString = child.InnerText;
   90:         //        } else {
   91:         //            throw new FormatException( "Security configuration element missing from config file" );
   92:         //        }
   93:         //    }
   94:         //} else {
   95:         //    throw new FormatException( "SecurityConfiguration element expected" );
   96:         //}
   97:  
   98:     }
   99:  
  100:     #endregion
  101: }

The other extension is the IAuthorizationExtension.  The implementation here is the same as that from the Forms Authentication sample, so it won’t be repeated here.  There is also an anonymous version of this extension that exists and was documented by James Wu in this blog post.

The last piece is configuration and changing the various configuration files in SSRS so that the custom security extension works.  Most of this is documented in the forms authentication sample, but will be repeated here.  The core configuration files are the Report Manager and Report Server web.config files and the rsreportserver.config file.  Report Manager requires changes to how impersonation is handled and, in our scenario, some additions to the appSettings section.  If you have also implemented an HTTPModule in order to validate the request and redirect to an authentication server that would have to be registered as well.  The changes to the web.config file are shown below.  This is located by default in the C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportManager directory. 

Note that we set identity impersonate to false.  The various <appSettings /> keys are used throughout the code modules for various things.  The important ones here are the AuthKey, which is a key added to our header to ensure that the request is coming from a trusted source.  Again, there are more secure methods to use than this, but has been placed unencrypted in the configuration file for this example.  The ReportMainConnectionString is used to connect to the user database and validate the UserToken that is passed in the header.

    1: <?xml version="1.0" encoding="utf-8"?>
    2: <configuration>
    3:     ...
    4:   <system.web>
    5:     ...
    6:     <identity impersonate="false" />
    7:     ...
    8:     <httpModules>
    9:       ...
   10:       <!-- REGISTER YOUR HTTP MODULE HERE IF REQUIRED -->
   11:     </httpModules>
   12:   </system.web>
   13:   <appSettings>
   14:     ...
   15:     <!-- your-server-name should be replaced with the value for your server -->
   16:     <add key="ReportServer" value="your-server-name" />
   17:     <add key="ReportServerInstance" value="RS_MSSQLSERVER" />
   18:     <add key="ReportServerWebServiceUrl" value="http://your-server-name/reportserver" />
   19:     <add key="AuthKey" value="1010101010" />
   20:     <add key="ReportMainConnectionString" value="SERVER=your-server-name;Initial Catalog=UserAuthenticationStore;Integrated Security=SSPI;Trusted_Connection=Yes" />
   21:   </appSettings>
   22:     ...
   23: </configuration>

Similar changes are made to the Report Server web.config file.  By default this is located in the C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer directory.  See the sections that change below.

    1: <?xml version="1.0" encoding="utf-8"?>
    2: <configuration>
    3:   <system.web>
    4:     ...
    5:     <authentication mode="Forms" />
    6:     <authorization>
    7:         <deny users="?" />
    8:     </authorization>
    9:     <identity impersonate="false" />
   10:    ...
   11:   </system.web>
   12:   <appSettings>
   13:       <add key="ReportServerWebServiceUrl" value="http://you-server-name/reportserver" />
   14:       <add key="AuthKey" value="1010101010" />
   15:       <add key="ReportMainConnectionString" value="SERVER=your-server-name;Initial Catalog=UserAuthenticationStore;Integrated Security=SSPI;Trusted_Connection=Yes" />
   16:   </appSettings>
   17:     ...
   18: </configuration>

Configuration changes also need to be made to the rsreportserver.config file.  This is the core configuration file for reporting services.  The changes for that file are shown below.  There are two sections that may need additional explanation.  In the UI/CustomAuthenticationUI/loginUrl element the page used to redirect an unauthenticated request is provided.  In the example here the UILogon.aspx page from the previous post will handle taking an unauthenticated request, validating the data and issuing the authentication token. 

The Extensions/Security section details the security extension developed for handling SSO security in the sample.  The first section provides the authorization extension.  The AdminConfiguration/UserName element contains the username that will be considered the System Administrator on the SQL Reporting Services instance.  Review of the Authorization extension shows that the extension reads in this configuration element and then makes decisions based on the value.  This provides a means for someone to enter other users that could be considered System Users or System Administrators.  The Extensions/Authentication section defines the authentication extension to use for the SSRS instance.

    1: <Configuration>
    2:     ...
    3:     <Authentication>
    4:         <AuthenticationTypes>
    5:             <Custom/>
    6:         </AuthenticationTypes>
    7:         ...
    8:     </Authentication>
    9:     ...
   10:     <UI>
   11:         <CustomAuthenticationUI>
   12:             <loginUrl>/Pages/UILogon.aspx</loginUrl>
   13:             <UseSSL>False</UseSSL>
   14:         </CustomAuthenticationUI>
   15:         <ReportServerUrl>http://your-server-name/reportserver</ReportServerUrl>
   16:         <PageCountMode>Estimate</PageCountMode>
   17:     </UI>
   18:     <Extensions>
   19:         ...
   20:         <Security>
   21:             <Extension Name="Forms" Type="Sample.ReportingServices.Security.Authorization, Sample.ReportingServices.Security, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d04ec164f4506a18">
   22:                 <Configuration>
   23:                     <AdminConfiguration>
   24:                         <UserName>administrator@sample.com</UserName>
   25:                     </AdminConfiguration>
   26:                 </Configuration>
   27:             </Extension>
   28:         </Security>
   29:         <Authentication>
   30:             <Extension Name="Forms" Type="Sample.ReportingServices.Security.Authentication, Sample.ReportingServices.Security, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d04ec164f4506a18" />
   31:         </Authentication>
   32:         ...
   33: </Configuration>

The final piece is deployment.  Deployment consists of deploying the assembly, deploying our custom page and changing additional configuration files.  The page and assembly are easy.  Deploy the assembly to the Report Manager\bin and Report Server\bin directories.  Deploy the UILogon.aspx page to the Report Manager\Pages directory.

Code Access Security policy, CAS, also needs to be addressed.  By default assemblies in the bin directory run with limited trust.  To grant full trust to your assembly open the rssrvpolicy.config file.  This file is located in the Report Server directory of the SSRS installation.  In the file find the <CodeGroup /> element that has a Url membership of $CodeGen and add the following <CodeGroup /> element afterwards.  This example uses a Url condition.  Best practice would dictate using a strong name membership condition by using a public key blob.  To extract the public key blob (not the public key token) use the Secutil.exe tool as follows: secutil.exe -hex -s MyAssemblyName.dll.

    1: <CodeGroup
    2:     class="UnionCodeGroup"
    3:     version="1"
    4:     Name="SecurityExtensionCodeGroup"
    5:     Description="Code group for the custom security extension"
    6:     PermissionSetName="FullTrust">
    7:     <IMembershipCondition 
    8:         class="UrlMembershipCondition"
    9:         version="1"
   10:         Url="C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin\Sample.ReportingServices.Security.dll"
   11:     />
   12: </CodeGroup>

The Report Manager policy file, rsmgrpolicy.config, also needs to be updated.  This file is located in the Report Manager installation directory.  In this file the MyComputer zone should be updated to have a FullTrust PermissionSetName.  

    1: <CodeGroup 
    2:     class="FirstMatchCodeGroup" 
    3:     version="1" 
    4:     PermissionSetName="FullTrust"
    5:     Description="This code group grants MyComputer code Execution 
    6:     permission. ">
    7:     <IMembershipCondition 
    8:         class="ZoneMembershipCondition"
    9:         version="1"
   10:         Zone="MyComputer" />

That’s it.  Your sample should now work.  A full Visual Studio 2010 project is available for download and modification.