onsdag 8 september 2010

Authorized Images in an FBA Login Page

My first post. I intend to use this blog mostly just to remind myself about useful stuff I've done, but if it can help anybody else I'll be happy.
A customer wants to a have a login page but still using authentication from their AD. First I thought, not possible since NTLM just allows the normal windows login popup. A former colleague said that I maybe could consider making it a forms based authentication application using the ActiveDirectoryMembershipProvider together with SSL. It was ok for the customer but maybe that will be another post.
The Big problem was to design the login page. With their current oracle-based solution it would take up to two weeks to get a change in the login page. Requests to the hosting company, decisions, meetings etc, and ok they accept to change the . to a , .
Now a user can edit the the login page using an announcement item in a list. The login page runs a code with elevated privileges to get the latest announcement and places the Title and Body fields in the page in the Render method. Nice.
But after a while they noticed that they couldn't use an image in the body. Hmmm... why not? Aha, the image is a link to an attachment in the item. Damn! The login page gets the correct html, but since the user isn't logged in yet, the img-tags can't load the images.
First I thought a solution would be to take a "snap shot" of the body including the images and put it in the LAYOUTS folder and set the location as allowed in web.config. But when I tried to write the stuff to LAYOUTS I noticed that not even the Application Pool account have write access.
Ok, another idea. Replace the url to the image with a url to an aspx-file that takes the image's url in the query string, runs code with elevated privileges to get the image and in the Page.Response writes the image as a byte[].
I still had to set that the aspx-file is allowed non-authenticated access in web.config. But hey, it worked! But just to be careful I made the url in the query string as a Base64Encoded string (of course I could have made a somewhat more safe encoding, but time is money).

So, the solution was:
An aspx-file namned getimage.aspx placed in the LAYOUTS folder containing

<%@ Assembly Name="MyProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3334a3f90ce5bdd0" %>
<%@ Page Language="C#" Inherits="MyProject.Code.ApplicationPages.GetImage" %>
 
The class MyProject.Code.ApplicationPages.GetImage
 
public class GetImage : Microsoft.SharePoint.WebControls.UnsecuredLayoutsPageBase

{
protected override void OnLoad(EventArgs e)
{
  if (string.IsNullOrEmpty(Request.QueryString["url"]))
    return;

  SPSecurity.RunWithElevatedPrivileges(() =>
  {
    string siteUrl = string.Format("{0}://{1}", HttpContext.Current.Request.Url.Scheme, HttpContext.Current.Request.Url.Host);
    string url = Base64Decode(Request.QueryString["url"]);
    using (SPSite site = new SPSite(siteUrl))
    {
      using (SPWeb web = site.OpenWeb())
      {
        SPFile file = web.GetFile(url);
        Page.Response.BinaryWrite(file.OpenBinary(SPOpenBinaryOptions.None));
      }
    }
  });
}
private static string Base64Decode(string sBase64String)
{
  byte[] sBase64String_bytes = Convert.FromBase64String(sBase64String);
  return UnicodeEncoding.UTF8.GetString(sBase64String_bytes);
}

And the really tricky part: correcting the urls in the body in the login page. It's a little part in the Render method (The class is called MyProject.Code.ApplicationPages.LoginPage and inherits from Microsoft.SharePoint.ApplicationPages.LoginPage):

protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
  SPSecurity.RunWithElevatedPrivileges(() =>
  {
    string url = string.Format("{0}://{1}", HttpContext.Current.Request.Url.Scheme, HttpContext.Current.Request.Url.Host);
    using (SPSite site = new SPSite(url))
    {
      using (SPWeb web = site.RootWeb)
      {
        var list = web.GetList("/lists/loginmessages");
        var query = new SPQuery();
        query.Query = "<Where><Geq><FieldRef Name=\"Expires\"/><Value Type=\"DateTime\"><Today/></Value></Geq></Where><OrderBy><FieldRef Name=\"Expires\" Ascending=\"FALSE\"/></OrderBy>";
        var items = list.GetItems(query);
        if (items.Count > 0)
        {
          lblTitle.Text = items[0].Title;
          string body = Convert.ToString(items[0][SPBuiltInFieldId.Body]);
          Regex regex = new System.Text.RegularExpressions.Regex("(?<=\\<img.*src=\")((/lists/loginmessages/attachments/\\d*/)[-a-zA-Z0-9@:%_\\+.~#?&//=]+)\\.(jpg|jpeg|gif|png|bmp|tiff|tga|svg)", RegexOptions.IgnoreCase);
          body = regex.Replace(body, "/_layouts/getimage.aspx?url=$&");
          regex = new Regex("(?<=\\<img.*src=\"/_layouts/getimage\\.aspx\\?url=)((/lists/loginmessages/attachments/\\d*/)[-a-zA-Z0-9@:%_\\+.~#?&//=]+)\\.(jpg|jpeg|gif|png|bmp|tiff|tga|svg)", RegexOptions.IgnoreCase);
          body = regex.Replace(body, new MatchEvaluator(LoginPage.ReplaceEncode));
          litBody.Text = body;
        }
      }
    }
  });
}

and the methods to do the encoding in the login page:

private static string ReplaceEncode(Match match)
{
  return match.Result(Base64Encode(match.Value));
}
private static string Base64Encode(string sString)
{
  byte[] sString_bytes = System.Text.UnicodeEncoding.UTF8.GetBytes(sString);
  return Convert.ToBase64String(sString_bytes);
}

Inga kommentarer:

Skicka en kommentar