Saturday, January 16, 2010

Report viewer control authentication – Part 2 – Forms Authentication

If the reporting services configured to use forms authentication and you need to show the reports in the custom developed applications then the need of processing authentication the report viewer control through the code.
Report viewer control authentication using windows authentication is described in the part 1 here. Now, we will discuss the authenticating forms authentication through C# code.
If we are are developing windows application and want to use report viewer control, then we need to implement the below logic to authenticate.
reportViewer1.ServerReport.ReportServerCredentials.SetFormsCredentials(null, "userName", "password", "");
this.reportViewer1.RefreshReport();
Where as in forms authentication simply assigning the credentials not enough. The reasons behind are, security, code efficiency, performance, response time etc everything come into the picture. So, we need to write code which supports everything.
What are the major tasks?
  • Report viewer control excepts the credential of type IReportServerCredentials. So, we need to create an object which inherits from this interface and pass this to the report viewer control to authenticate.
  • Handling cookie. Based on the login and request is successful we will write the cookie to browser and keep that in browser for further requests processing. The advantage is if cookie is available then won’t make any request to report server for authenticating.
  • To create the cookie related information we actually need of hijack the request and response and get the cookie information and save it to browser. So, how to catch the request and response which made to report server? We will discuss this later in this article.
  • To actually communicate to the report server, we need to make the communication with it. The best way to do that is using web services. Everyone knows that reports in SSRS gets with two sites. One is report manager means report web application [/reports] and the report server means a report web service[/reportserver]. So, we will use the web service, write a proxy and implement the existing functions in it.

I think, it is little bit complex to understand the above tasks. But actually they are very simple to implement.


Code needed: We need two classes to get what we need. These are custom classes and add them in single file named ReportServerCredentials.cs somewhere in the project.
  • Classes needed - ReportServerCredentials and ReportingService.
public class ReportServerCredentials : IReportServerCredentials
{
private Cookie m_authCookie;
public ReportServerCredentials(Cookie authCookie)
{
m_authCookie = authCookie;
}
public WindowsIdentity ImpersonationUser
{
get
{
return null;  // Use default identity.
}
}
public ICredentials NetworkCredentials
{
get
{
return null;  // Not using NetworkCredentials to authenticate.
}
}
public bool GetFormsCredentials(out Cookie authCookie,
out string user, out string password, out string authority)
{
authCookie = null;
user = ConfigurationManager.AppSettings["ReportServerUserName"];
password = ConfigurationManager.AppSettings["ReportServerPassword"];
authority = "";
return true;  // Use forms credentials to authenticate.
}
}
public class MyReportingService : rsExecutionReference.ReportExecutionService
{
private Cookie m_authCookie;
public Cookie AuthCookie
{
get
{
return m_authCookie;
}
}
protected override WebRequest GetWebRequest(Uri uri)
{
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.CookieContainer = new CookieContainer();
if (m_authCookie != null)
request.CookieContainer.Add(m_authCookie);
return request;
}
protected override WebResponse GetWebResponse(WebRequest request)
{
WebResponse response = base.GetWebResponse(request);
string cookieName = response.Headers["RSAuthenticationHeader"];
if (cookieName != null)
{
HttpWebResponse webResponse = (HttpWebResponse)response;
m_authCookie = webResponse.Cookies[cookieName];
}
return response;
}
}
A small explanation:
ReportServerCredentials class is the class which is inheriting from the IReportServerCredentials interface.
  • If we are using the impersonation then the first property will be used.
  • If we are passing the custom network credentials then the second property will be used.
  • If we are using forms authentication then the method GetFormsCredentials() will be used.
ReportingService is the class which is inheriting from the ReportExecutionService web service. The question in your mind now is what is the "rsExecutionReference". This is the web reference to the report server web service. To get this, right click on the web application/web site and select the option "Add web reference". Now, in the input box, enter the report server url ending with "ReportExecution2005.asmx". For example, if your report server is at http://localhost/reportserver then the web service location will be http://localhost/reportserver/ReportExecution2005.asmx. Add the web reference to project or site. Now, we have the web service proxy created in the solution. So, in my code rsExecutionReference is nothing but the web service proxy for the report server web service.
The web service has two methods already implemented GetWebRequest() and GetWebResponse(). So, we are overriding them in our code to catch the cookie ticket information which is returned by the report server. If you observe the GetWebResponse() code, we are actually checking for the header information from the response and in the response we are catching the cookie we need. If cookie is null means the request authentication failed and means invalid credentials passed.
I think, till now what I have mentioned is clear for you and we are done with contacting or communicating with the web service. Now, where we are calling the web service or in other words where the authentication request started? It's not yet. Check the below code and notes for it.
I have a Utility class in my application where I place all the util related code in it. For example purpose, I am assuming this method you have placed in Utility class  [If no Utility class in your application then create one and place this below method in it].
public static HttpCookie AuthenticateReportServerAccess()
{
MyReportingService svc = new MyReportingService();
svc.Url = ConfigurationManager.AppSettings["ReportServerWebSeriveUrl"];
HttpCookie hcookie = null;
try
{
svc.LogonUser(ConfigurationManager.AppSettings["ReportServerUserName"],
ConfigurationManager.AppSettings["ReportServerPassword"], null);
Cookie myAuthCookie = svc.AuthCookie;
if (myAuthCookie == null)
{
//Message.Text = "Logon failed";
}
else
{
hcookie = new HttpCookie(myAuthCookie.Name, myAuthCookie.Value);
HttpContext.Current.Response.Cookies.Add(hcookie);
}
}
catch (Exception ex)
{
//Message.Text = "Logon failed: " + ex.Message;
}
return hcookie;
}
What this method is doing? A little explanation is here. We need to make or send a request to report server for authenticating the current request to load a report. So, I am using this method for that. I have a proxy class as described above and based on that I have created my own custom class named ReportingService. So, I am using that to make the call by using the built in function exists in the web service named LogonUser(). Which is actually takes three parameters named username, password and authority. Authority is optional, the specific authority to use when authenticating a user. For example, a Windows domain, some name to make distinct the call for that user. Pass a value of null to omit this argument. This will make call to the report server. Because of we override the request and response methods in proxy, it will come to those method and executes all the code in them. So, in the method AuthenticateReportServerAccess() we are making request and getting the cookie. If wrong credentials passed then it means cookie is null. So, you can write your own logic there like throw exception or show some message, If you have any login page implemented for reporting then redirecting the user there etc… If cookie exist then Add that cookie to the response object.
Now, we are done with making a call and processing it and loading cookie. Now what? What we left with… See below.
In the page where we have placed the report viewer control, there add the below code. The below code you can write may be in onclick event of something or when page loads depends on your requirement.
HttpCookie cookie = Request.Cookies["sqlAuthCookie"];
if (cookie == null)
{
cookie = Util.AuthenticateReportServerAccess();
}
if(cookie != null)            {
//Add logic to pass parameters if needed.

reportViewer1.ServerReport.ReportServerUrl = new Uri("reportserverurl");
reportViewer1.ProcessingMode = ProcessingMode.Remote;
reportViewer1.ServerReport.ReportPath = "reportPath";
Cookie authCookie = new Cookie(cookie.Name, cookie.Value);
authCookie.Domain = Request.Url.Host;
reportViewer1.ServerReport.ReportServerCredentials =
new ReportServerCredentials(authCookie);           
//If any parameters then reportViewer1.ServerReport.SetParameters(reportParams);
}
What is happening in the code? We are checking whether the cookie already exist means user is already authenticated and if it null then we are sending request to report server and loading the cookie to browser. If cookie exists then it will go to second if condition and loads the report in the report viewer control by passing the cookie information to report server.
OHHH… What else? we are done with complete coding. We are left with testing the code.
Note: If you observe the code, there are some places we are getting the appsettings keys from the web.config.
Hope you are well understood and not with many questions in mind at this stage. Please feel free to ask me if you have any questions. Love to hear comments.

20 comments:

  1. Hi Praveen

    I've been using the recommendations in your article, although I run into an error at the line:

    svc.LogonUser(ConfigurationManager.AppSettings["ReportServerUserName"], ConfigurationManager.AppSettings["ReportServerPassword"], null)

    At which point I get 'The request failed with HTTP status 401: Unauthorized'. The credentials I'm supplying work when logging onto http://localhost/reports on the remote report server. My solution is a web-application (being developed and tested locally) that connects to our remote report server. The solution is written in VB.NET and I converted your C# into VB.

    Is there anything you could suggest?

    Thanks

    Brian

    ReplyDelete
  2. Try to change the url from http://localhost/reports to http://localhosy/reportserver.
    reportserver is the web service. And check once you have added the correct reference to the web service. Please let me know, if that didn't solve your problem.

    ReplyDelete
  3. Thanks for the quick reply!

    On the report server itself I browsed to 'http://localhost/reportserver' and sucessfully authenticated with the credentials.

    In the solution (on my local machine) the svc.url is 'http://[REPORTSERVERIPADDRESS]/reportserver/reportservice2005.asmx'

    But still no luck.

    Brian

    ReplyDelete
  4. OK, I think it's better to place everything in the web.config and refer them in the code. But that's ok.
    I mentioned the web service file should be ReportExecution2005.asmx instead of reportservice2005.asmx. So, please add this web reference to your project and give that file path only in your code.

    Btw, did you configure Forms authentication correctly on SSRS? Are you able to login to SSRS through browser?

    thanks
    -Praveen

    ReplyDelete
  5. Ok, I've changed all references to reportservice to executionservice in the project. But still no luck.

    I am able to access 'http://[reportServerIPAddress]/repors' and 'http://[reportServerIPAddress]/reporserver' from my local machine with the credentials.

    Brian

    ReplyDelete
  6. OK. This is the time we need to do investigation.
    1. Report server configured with Forms authentication Right?
    2. You are able to successfully loggedin to the report server from browser and have access to the reports to the user you logged in.
    3. You have placed all the code I have mentioned and you are able to successfully call the web service.
    4. The username and password you are passing from the application is correct?
    5. Debug the code and send me the line where it is failing.

    Just answer me inline for each point mentioned above and get me back asap.

    thanks
    -Praveen

    ReplyDelete
  7. Hey, great article. It worked for me with a webapplication project, but I am getting timeout errors inside a website. What do I need to change to correct this?

    ReplyDelete
  8. Did you follow all the steps which I have mentioned? Can you tell me that are you able to login to the report site from browser? If you are able to then only you can authenticate from web application.

    ReplyDelete
  9. Hello, I know you posted this article a few months ago, but I have a question, could this be implemented as a webpart to be used in moss 2007?

    ReplyDelete
  10. Yes. This is simple C# code. So, you can do it in web part and deploy it to SharePoint.
    Let me know, if you need any help/

    -Praveen.

    ReplyDelete
  11. Hello,

    I used information from your post. Seems to be working fine, except for some cases where the authentication extension throws an exception like:

    The Authentication Extension threw an unexpected exception or returned a value that is not valid: identity==null. (rsAuthenticationExtensionError)

    It seems that the cookie is not properly sent in this case.

    Does it have to do with the cookies domain? Or with the timeout of the cookie on the Report Server?
    Could you provide with some hints?

    I saw some simmilar comment here: http://blogs.infosupport.com/blogs/marks/archive/2010/03/29/report-builder-2-0-security-extension-issues.aspx

    ReplyDelete
  12. Hi Praveen,

    We are using Custom Authentication (against LDAP) on our reports and it works fine when we browse through the web browser (https://reportserverhost/reports). When I try to implement this solution we are getting an error. In the Log files it shows ERROR: Throwing Microsoft.ReportingServices.Diagnostics.Utilities.InternalCatalogException: An internal error occurred on the report server. See the error log for more details., ;
    Info: Microsoft.ReportingServices.Diagnostics.Utilities.InternalCatalogException: An internal error occurred on the report server. See the error log for more details. ---> System.NullReferenceException: Object reference not set to an instance of an object.

    Any idea?

    Thanks
    Chocks

    ReplyDelete
  13. Thanks for this post, has been really helpful and clear.
    Keep doing things like this.
    Peter

    ReplyDelete
  14. Thanks Peter. That's a worth comment to boost me up. :)

    -Praveen.

    ReplyDelete
  15. He Praveen

    I've been using the recommendations in your article, although I run into an error at the line:

    System.NullReferenceException: Anonymous logon is not configured. userIdentity should not be null!

    This error is throw when GetUserInfo method is called in security extension.

    Can you please suggest what should be done?

    ReplyDelete
  16. I think this article is for SSRS 2005 Forms Authentication. We already have application working using SSRS 2005 with Forms Authentication. Can you please tell what are the steps involved to migrate from SSRS 2005 to SSRS 2008r2 using Forms Authentication.

    ReplyDelete
  17. Hello All,

    I created login page using "http://www.codeproject.com/Articles/675943/SSRS-Forms-Authentication" article for SSRS report with forms Authentication login and it’s working fine.

    Now i want to control folder access, currently all user can view all folder, but I want to give particular user to access particular folder.

    Ex :- suppose I create 4 Users (AUser as Admin User , BUser as Normal user, CUser as Normal user and DUser as Normal user) and 4 folder with the same name. So want to give particular folder access to particular Users (AUser can view all folder, B can view A & B folder , C can view C folder and D can view D folder).

    Can someone help me on the same.

    Thanks in advance.

    Regards,
    Rajkumar Vishwakarma

    ReplyDelete
  18. I am also getting the same error.
    he request failed with HTTP status 401: Unauthorized.

    I checked and set up looks correct. When i try to access the reports via Browser, it works with the same username password i have set in the application.

    ReplyDelete
  19. I use ReportService

    http://DEVRPS/ReportServer/ReportService2005.asmx?WSDL

    I use powershell script por deploy RDL files in SSRS 2008 server

    and I get error

    Message: Exception calling "DeleteItem" with "1" argument(s): "The request failed with HTTP status 401: Unauthorized."

    With the same credential, in IE is ok http://DEVRPS/ReportServer/ReportService2005.asmx?WSDL


    In Report Services 2008 by default it uses its own built-in server, not IIS. You'll need to check in the reporting services directory on your server (its in the usual MSSQL directory in Program Files) to see the configuration files and make changes (you can also make some through reporting services configuration manager). Microsoft does not recommend installing IIS on the reporting services server.

    SSRS 2008 doesn't use IIS

    troubleshoot IIS and 401 not useful then for me
    https://support.microsoft.com/es-es/kb/907273


    Any suggestions ?

    ReplyDelete
  20. Great post!! thank you very much.....

    ReplyDelete