beaucrawford.net

Give me data or give me death

About the author

Author Name is someone.
E-mail me Send mail

Recent comments

Don't show

Authors

Tags

Don't show

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

    © Copyright 2010

    IIS mixed mode authentication for ASP.NET Applications

    The situation: You have a Website that is available to both Intranet users and Internet users.  You want users from the Intranet to be authenticated automatically using their domain credentials and Internet users to be authenticated using basic ASP.NET forms security.

    The problem:  When you configure IIS to allow both Anonymous Access and Integrated Windows Authentication, you cannot get the remote users NTLM name.

    The fix:  Configure IIS to use Anonymous Access for the entire site, with the exception of a single page.  This page will be set to use Integrated Windows Authentication.

    The gotcha is that, when you check "Anonymous Access" in IIS, you cannot get the user's NTLM name (even if you check "Integrated Windows Authentication").  The key is to specify "Integrated Windows Authentication" for exactly one page in your application.  When Intranet users hit this page, we can get their NTLM name out of Request.ServerVariables["LOGON_USER"] and programmatically store that in the FormsAuthentication context.  External Internet users can even hit this page and manually specify their domain credentials if they like, but these users will normally go through the standard login process.

    STEP 1 - Create a new virtual directory in IIS.  In the application root, only check "Anonymous Access".  Do not check "Integrated Windows Authentication".  This should look like:



    STEP 2 - In your Web application, create a new form named "WinLogin.aspx" under the app's root.  Also add a page named "Login.aspx" under the app's root and put an instance of the standard System.Web.UI.WebControls.Login control on it, e.g.:

    <asp:Login id="login" runat="server" />

    STEP 3 - In IIS manager, select WinLogin.aspx and navigate to the "File Security" tab.  Here disable "Anonymous Access" but enable "Integrated Windows Authentication" (this is crucial).   This should look like:


    STEP 4 - In your web application's web.config, add the following:

    <!-- Allow anonymous access to this page. --> 
    
      <location path="Login.aspx">
        <system.web>
          <authorization>
            <allow users="?,*" />
          </authorization>
        </system.web>
      </location> 
    
      <!-- Allow anonymous access to this page.  IIS will prompt external users.  Intranet users will be authenticated automatically when they hit this page --> 
    
      <location path="WinLogin.aspx">
        <system.web>
          <authorization>
            <allow users="?,*" />
          </authorization>
        </system.web>
      </location> 
    
       <system.web> 
    
        <authentication mode="Forms">
          <forms loginUrl="Login.aspx"/>
        </authentication> 
    
        <!-- Require authentication for all other pages --> 
    
        <authorization>
          <deny users="?" />
          <allow users="*" />
        </authorization> 
    
        <!-- custom membership provider to handle forms authentication --> 
    
        <membership defaultProvider="CustomMembershipProvider">
          <providers>
            <clear />
            <add name="CustomMembershipProvider" type="DualAuthenticationExample.CustomMembershipProvider, DualAuthenticationExample" />
          </providers>
        </membership>
    
        <!-- OTHER SETTINGS --> 
    
       </system.web>

    STEP 5 - In ~/Login.aspx add the following code:

    namespace DualAuthenticationExample
    {
        public partial class LoginPage : System.Web.UI.Page
        {
            private bool IsIntranetRequest(string ip)
            {
                // TODO: Check for whatever interanet ip addresses, ranges, etc here... 
    
                return !string.IsNullOrEmpty(ip) && Regex.IsMatch(ip, "^127");
            } 
    
            protected void Page_Load(object sender, EventArgs e)
            {
                if (IsIntranetRequest(Request.ServerVariables["REMOTE_ADDR"]))
                {
                    Response.Redirect("~/WinLogin.aspx");
                }
            }
        }
    } 

    STEP 6 - In ~/WinLogin.aspx add the following code:

    namespace DualAuthenticationExample
    {
        public partial class WinLoginPage : System.Web.UI.Page
        {
            protected void Page_Load(object sender, EventArgs e)
            {
                string user = Request.ServerVariables["LOGON_USER"]; 
    
                if (string.IsNullOrEmpty(user))
                {
                    Response.Redirect("~/Login.aspx");
                }
                else
                {
                    FormsAuthentication.SetAuthCookie(user, false);
                    Response.Redirect("~/Default.aspx");
                }
            }
        }
    }
    So, as specified in the web.config, all users will be bounced to the Login.aspx page.  If the user is on the intranet, they'll get bounced again to the WinLogin.aspx page, which will attempt to authenticate them with their NTLM name.  If that is successful, then we programmatically log them into the FormsAuthentication context.  From here, the user's name will be availble via the standard Page.User.Identity.Name property (as well as HttpContext.Current.User.Identity.Name)

    If you know the IP range(s) for the Intranet requests you can really streamline what I have here and make it seamless to the user.  This is technically not a requirement to get this to work but, without it, external users will be prompted with the authentication box (I believe the behavior for this varies between browsers and also depends on the user's current browser settings).

    Note: in the above example, I use my own custom MembershipProvider.  You can obviously use any type that derives from System.Web.Security.MembershipProvider.


    Categories: C# | ASP.NET
    Posted by Beau on Tuesday, July 15, 2008 6:25 PM
    Permalink | Comments (7) | Post RSSRSS comment feed

    Injecting an IRenderingExtension instance into the ReportViewer control

    In a previous post I discussed how it is possible to enable HTML rendering for local reports firing via a ReportViewer instance.  I did that because I was recently in a situation where we were using the ReportViewer in local mode had to support HTML reports.  Running reports in local mode with the ReportVIewer affords you many luxuries, but HTML report generation is not one of them.  Desperate times call for desperate measures.

    I also mentioned the fact that rendering extensions, presumably for a good reason, are not extensible by default for local reports.  By “extensible”  I mean that there is no public object model exposed nor are their .config options to alllow, for example, your own custom rendering extension types to be loaded.  Of course, as an absolute last resort, you can use Reflector to disassemble the code and hack into the object model using Reflection.   I will be the first to tell you that this is not a good idea (for a number of obvious reasons) but the main one is that the component’s code can change (a new version comes out, etc).  When that happens, private field names can change and cause your references obtained via Reflection to be null.  That can lead to big problems and hard-to-solve bugs.

    As I looked at the code from my previous post a little more I soon realized that the internal list of rendering extensions holds instances of type Microsoft.ReportingServices.ReportRendering.IRenderingExtension.  With this interface you could create your own extension and then inject it into the internal list using Reflection.

    Here’s some code do that:

       1: private static void AddRenderingExtension(ReportViewer viewer, string formatName, Type renderingExtension)
       2: {
       3:     const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
       4:  
       5:     FieldInfo m_previewService = viewer.LocalReport.GetType().GetField
       6:     (
       7:         "m_previewService",
       8:         Flags
       9:     );
      10:  
      11:     MethodInfo ListRenderingExtensions = m_previewService.FieldType.GetMethod
      12:     (
      13:         "ListRenderingExtensions",
      14:         Flags
      15:     );
      16:  
      17:     object previewServiceInstance = m_previewService.GetValue(viewer.LocalReport);
      18:  
      19:     Type type = Type.GetType("Microsoft.Reporting.LocalRenderingExtensionInfo, Microsoft.ReportViewer.Common, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
      20:  
      21:     ConstructorInfo ctor = type.GetConstructor
      22:     (
      23:         BindingFlags.NonPublic | BindingFlags.Instance,
      24:         null,
      25:         new Type[] { typeof(string), typeof(string), typeof(bool), typeof(Type), typeof(bool) },
      26:         null
      27:     );
      28:  
      29:     IList extensions = ListRenderingExtensions.Invoke(previewServiceInstance, null) as IList;
      30:  
      31:     object instance = ctor.Invoke(new object[]
      32:     {
      33:             formatName,
      34:             formatName,
      35:             true, 
      36:             renderingExtension, 
      37:             true
      38:     });
      39:  
      40:     extensions.Add(instance);
      41: }

    An example call would be:

    AddRenderingExtension(reportViewerInstance, "HTMLOWC", typeof(HtmlOWCRenderingExtension));

    I chose HtmlOWCRenderingExtension simply because it is a type that is not already in the internal list (not sure why) but yet implements IRenderingExtension.  I am not sure of the actual differences between it and its parent type, Html40RenderingExtesion.  As the name alludes to, I assume it deals with OWC (Office Web Components) and some special rendering considerations they have for HTML.  Regardless, if you use the “Derived Types” capability in Reflector you will see that there are a few other Types that implement this interface:

     

    As you can see above, in addition to the Excel and PDF extensions provided by the viewer by default, there are additional extensions available for images (which are available in server reports but not local reports).  You could, of course, code your own implementation and then inject that as well.  At this point, I am not sure of the complexities involved with that.  A rendering extension that would be extremely useful would be one that is able to generate CSV files.  Interestingly enough, server reports have that available as an export option.  Local reports do not.  You can probably guess where I’m going with this…


    Categories: C# | Reflection
    Posted by Beau on Wednesday, July 02, 2008 12:58 AM
    Permalink | Comments (2) | Post RSSRSS comment feed

    Enable HTML in ReportViewer LocalReport

    HTML is available as a rendering extension in server reports rendered by the ReportViewer control (Microsoft.Reporting.WebForms.ReportViewer).  By “server report” I mean a report that is rendered via the ServerReport property (which makes a Web service call to an actual Reporting Services installation).  The report is rendered on the server and sent back to you as a byte[] array. 

    The bummer is that, by default, there is no support for HTML in local reports (reports that have the .rdlc extension and are rendered via the LocalReport property).  In fact, you will get a LocalProcessingException if you attempt to use the “HTML4.0” format name.  In Reporting Services, the rendering extensions are completely extensible via a .config file.  Not so in the Web server control (and the Win Forms control).  In fact, the rendering extensions are hard coded in the class and there are no public extensibility points.  I found this extremely annoying, so I started poking around in Reflector and finally drilled to the following code:


       1: public override IEnumerable<LocalRenderingExtensionInfo> ListRenderingExtensions()
       2: {
       3:     if (this.m_renderingExtensions == null)
       4:     {
       5:         List<LocalRenderingExtensionInfo> list = new List<LocalRenderingExtensionInfo>();
       6:         Html40RenderingExtension extension = new Html40RenderingExtension();
       7:         list.Add(new LocalRenderingExtensionInfo("HTML4.0", extension.LocalizedName, false, typeof(Html40RenderingExtension), false));
       8:         Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer renderer = new Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer();
       9:         list.Add(new LocalRenderingExtensionInfo("Excel", renderer.LocalizedName, true, typeof(Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer), true));
      10:         RemoteGdiReport report = new RemoteGdiReport();
      11:         list.Add(new LocalRenderingExtensionInfo("RGDI", report.LocalizedName, false, typeof(RemoteGdiReport), false));
      12:         ImageReport report2 = new ImageReport();
      13:         list.Add(new LocalRenderingExtensionInfo("IMAGE", report2.LocalizedName, false, typeof(ImageReport), true));
      14:         PdfReport report3 = new PdfReport();
      15:         list.Add(new LocalRenderingExtensionInfo("PDF", report3.LocalizedName, true, typeof(PdfReport), true));
      16:         this.m_renderingExtensions = list;
      17:     }
      18:     return this.m_renderingExtensions;
      19: }


    So, as I mentioned, the rendering extensions are hard coded.  Above, on line 6, you can see that an instance of “Html40RenderingExtension” is created and then passed into the list via a wrapper instance of type LocalRenderingExtensionInfo.  If we look at the constructor for this class we can see that it looks like:

       1: internal LocalRenderingExtensionInfo(string name, string localizedName, bool isVisible, Type type, bool isExposedExternally) : this(name, localizedName, isVisible)
       2: {
       3:     this.m_type = type;
       4:     this.m_isExposedExternally = isExposedExternally;
       5: }

    I found the “isExposedExternally” boolean value somewhat interesting.  In fact, I dug around and found the aforementioned LocalProcessingException is thrown if the rendering extension is not set to be exposed via this field.  Hmmmmm…. that’s interesting.  Well, with the help of some Reflection magic, we can reach in and grab the private “m_isExposedExternally” field and simply updates its value to true.

    The code to do that is as follows:

    Note:  This is for the Microsoft.Reporting.WebForms.ReportViewer type.
       1: private static void EnableFormat(ReportViewer viewer, string formatName)
       2: {
       3:     const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
       4:  
       5:     FieldInfo m_previewService = viewer.LocalReport.GetType().GetField
       6:     (
       7:         "m_previewService",
       8:         Flags
       9:     );
      10:  
      11:     MethodInfo ListRenderingExtensions = m_previewService.FieldType.GetMethod
      12:     (
      13:         "ListRenderingExtensions",
      14:         Flags
      15:     );
      16:  
      17:     object previewServiceInstance = m_previewService.GetValue(viewer.LocalReport);
      18:  
      19:     IList extensions = ListRenderingExtensions.Invoke(previewServiceInstance, null) as IList;
      20:  
      21:     PropertyInfo name = extensions[0].GetType().GetProperty("Name", Flags);
      22:  
      23:     foreach (object extension in extensions)
      24:     {
      25:         if (string.Compare(name.GetValue(extension, null).ToString(), formatName, true) == 0)
      26:         {
      27:             FieldInfo m_isVisible = extension.GetType().GetField("m_isVisible", BindingFlags.NonPublic | BindingFlags.Instance);
      28:             FieldInfo m_isExposedExternally = extension.GetType().GetField("m_isExposedExternally", BindingFlags.NonPublic | BindingFlags.Instance);
      29:             m_isVisible.SetValue(extension, true);
      30:             m_isExposedExternally.SetValue(extension, true);
      31:             break;
      32:         }
      33:     }
      34: }

    Once you do this on your instance of ReportViewer you can then call LocalReport.Render and pass in the format name of “HTML4.0”.  That will obviously generate HTML and any images contained in the report can be captured by using normal <DeviceInfo> options that are passed to the Render method.  Given the fact that HTML rendering is not directly supported, there are a few minor annoyances with how images are processed.  I’ll blog on that someday.

    Enjoy!

    Categories: C# | SSRS
    Posted by Beau on Tuesday, July 01, 2008 9:19 PM
    Permalink | Comments (10) | Post RSSRSS comment feed