
One of the ways to add more flexibility to your SQL Server Reporting Services (SSRS) is to extend SSRS with custom extensions.
SSRS supports a wide variety of extensions that serve different purposes, i.e. the custom data extension to manipulate a connectionstring or a custom security extension to enable Forms Authentication.
On my current project we’re making extensive use of a few custom extensions, including a custom security extension. In our environment we were unable to use integrated security on the Report Server, so we decided use Forms Authentication and therefore the need for a custom security extension quickly arose.
While security extension are a great way to implement tailormade security solutions, extensions can cause quite a few headaches as well.
In this blog I will address an issue we came across while using ReportBuilder 2.0 in combination with our security extension.
When we started our latest project for SSRS 2008 we already had a similar solution running on SSRS 2005, so we thought that a simple copy-paste of the extensions would give us a headstart.
And indeed, after a little bit of tweaking we got both Report Server and Report Manager running.. until we decided to start ReportBuilder 2.0 (RB2.0).
In our SSRS 2005 solution we’re using ReportBuilder as well, but we’re still on version 1.0 here.
If you have worked with both, there is no need to tell that RB2.0 is a lot more sophisticated than RB1.0 (this might explain why the download is a lot bigger as well).
And while RB1.0 cooperates with the security extension quite nicely, RB2.0 is a whole different story.
While working with RB2.0 in Forms Authentication mode we experienced some very unstable behavior when using the wizard to create a new dataset. The first few screens were very responsive and stable, but after third, fourth screen RB2.0 would suddenly freeze and stop responding.
And with ‘stop responding’ I mean you have to actually kill the process, unless you want to look at a wizard all day long.
So what is happening when you let RB2.0 run in Forms Authentication?
As taken from MSDN (in a nutshell):
1. A client application calls the Web service method LogonUser to authenticate a user.
2. The Web service makes a call to the LogonUser method of your security extension, specifically, the class that implements IAuthenticationExtension.
3. Your implementation of LogonUser validates the user name and password in the user store or security authority.
4. Upon successful authentication, the Web service creates a cookie and manages it for the session.
5. The Web service returns the authentication ticket to the calling application on the HTTP header.
When the Web service successfully authenticates a user through the security extension, it generates a cookie that is used for subsequent requests. The cookie may not persist within the custom security authority because the report server does not own the security authority. The cookie is returned from the LogonUser Web service method and is used in subsequent Web service method calls and in URL access.
So, in short, after we have submitted our credentials to the RB2.0 we should have a cookie that is able to keep us ‘logged in’ during the process.
Since extensions can be a bit tedious to debug, we added some logtraces to find out what was happening while the wizard of Report Builder freezes.
When we checked our logfile, the following message showed up:
1: <html><head><title>Object moved</title></head><body>
2: <h2>Object moved to <a href="/ReportServer/logon.aspx?ReturnUrl=%2fReportServer %2fReportService2005.asmx">here</a>.</h2>
3: </body></html>
RB2.0 uses the ReportService2005.asmx webservice to communicate with the Report Server.
The Report Server will only redirect us to logon.aspx when the authentication cookie is missing.
This is the kind of behavior you get when the cookie has reached its timeout, which was not the case in our scenario.
After doing some research it seemed like RB2.0 does not send the cookie with every request, unlike the documentation seems to suggest.
This behavior has puzzled us quite a bit, because most of the requests include the authentication cookie, but with some requests RB2.0 doesn’t seem to set the cookie properly.
When RB2.0 fails to send the cookie with the request, Forms Authentication will attempt to redirect (HTTP 302) and if you are in the middle of a wizard at this point, RB2.0 will stop responding.
We have ‘solved’ this issue by preventing RB2.0 from getting redirected.
The report server calls the GetUserInfo method for each request to retrieve the current user identity.
Our original code looked like this:
1: public void GetUserInfo(out IIdentity userIdentity, out IntPtr userId)
2: {
3: if (HttpContext.Current != null
4: && HttpContext.Current.User != null)
5: {
6: userIdentity = HttpContext.Current.User.Identity;
7: }
8: else
9: {
10: userIdentity = new GenericIdentity("Temporary user");
11: }
12:
13: userId = IntPtr.Zero;
14: }
If there is no current HttpContext available, a generic identity will be returned (instead of the identity of the current user).
This will cause the Report Server to send a HTTP 302 redirect, which RB2.0 really dislikes.
1: public void GetUserInfo(out IIdentity userIdentity, out IntPtr userId)
2: {
3: if (HttpContext.Current != null
4: && HttpContext.Current.User != null)
5: {
6: userIdentity = HttpContext.Current.User.Identity;
7: }
8: else
9: {
10: userIdentity = null;
11: }
12:
13: userId = IntPtr.Zero;
14: }
If we change the ‘else’ branch to return a null identity, this will cause the Report Server to send a HTTP 500 response.
Somehow this response triggers RB2.0 to invoke the LogonUser method on the Report Server.
This will yield a brand new authentication cookie, without RB2.0 being redirected.
And even though we’re still not convinced that this is the perfect solution for our problem, it’s working out pretty well so far.
9 comments
Didn’t work for me.. this solution brings a bigger error.
leo
I’m sorry to hear that Leo. Could you tell a bit more about the error message you get?
Security extensions can cause a variaty of exceptions under different circumstances, it’s hard to tell what’s going wrong in your situatiion without a bit more information.
marks
Hey Marks, thanks for your replay. I’m not doing anything different than implement the template supplied in the samples of VS. The GetUserInfo method returns a generic identity instead of a null value. I noticed that working on the rs manager and it timeout as well. The thing is that we don’t use the manager, but we need to keep alive the builder at least for 6 hours.
And this is the last thing I found out today. Our system calls RS web service ans retrieves the list of objects that we present to the user. You can set permissions over this folders/reports. So I set permissions to a folder for Administrator access only and logged in with a user that should not have access to this folder and it was listing and allowing access to it. After reviewing all the code and not even changing a line, I compiled as DEBUG and replaced the dll on the server and it worked. Thus the folder wasn’t showing on the list. Again, I compiled as RELEASE and replace the dll on the server and it was broken again. The difference between DEBUG and RELEASE is the DEBUG and TRACE constant definitions, everything else is the same. I can’t explain this behavior.
Anyway, I just hope you can give me a hint for the timeout case.
Thank you.
leo
Hi leo,
I have a problem with the Report Builder, when I create a new dataset, it stays asleep, I really do not mean this error.
If you were so kind to explain that I am wrong.
I have implemented custom security and the truth, I need to run the report builder, since it is a very powerful tool for us.
What I see is that when I send the dataset to call GetUserInfo again and that’s where they stick.
This is my code:
public void GetUserInfo(out IIdentity userIdentity, out IntPtr userId)
{
// If the current user identity is not null,
// set the userIdentity parameter to that of the current user
if (HttpContext.Current != null && HttpContext.Current.User != null)
{
userIdentity = HttpContext.Current.User.Identity;
}
else
// The current user identity is null. This happens when the user attempts an anonymous logon.
// Although it is ok to return userIdentity as a null reference, it is best to throw an appropriate
// exception for debugging purposes.
// To configure for anonymous logon, return a Gener
{
userIdentity = null;
//System.Diagnostics.Debug.Assert(false, “Warning: userIdentity is null! Modify your code if you wish to support anonymous logon.”);
//throw new NullReferenceException(“Anonymous logon is not configured. userIdentity should not be null!”);
}
// initialize a pointer to the current user id to zero
userId = IntPtr.Zero;
}
Rodolfo
@leo: You should be able to keep your session alive longer by setting the timeout to a greater value, see the following snippet from the ReportServer web.config:
In this case the timeout is set to 60 minutes.
Did you manage to find a solution for your DEBUG/RELEASE issue?
marks
@Rodolfo: can you confirm that your extension is working as intended when you run reports through the Report Manager?
And when your ReportBuilder freezes, does the “Object moved to..” error show up in your logfiles?
marks
Thanks for this post, Mark! It’s working well for us now. After the timeout, we’re getting the cryptic error message:
The Authentication Extension threw an unexpected exception or returned a value that is not valid: identity==null. (rsAuthenticationExtensionError)
But we consider that a training issue.
Thanks again!
Joe
Joe Zamora
The HTTP 500 error is being caused as a result of the null value returned from the GetUserInfo method being dereferenced. While this accomplishes the desired behavior, it would be more straightforward (and likely easier to debug, should the SSRS method invocation change to guard against a null result) to throw a AuthenticationException when a principal cannot be established.
Regardless, thanks for the workaround here. It works as advertised!
Brandon
Brandon Haynes
This fixed the issue not only with the Report Builder but also with the SSMS for SQL2008 R2. I had a temporary user to fix an old issue in 2005 for the first authentication request (call to LogonUser) Thanks!
Jose