SSRS Report Viewer + ASP.NET Credentials 401 Exception - asp.net

I have a report saved on a SQL2005 reporting server, and I want to return a rendered PDF of this report. I've figured this out when working with a local *.rdlc file (and I've blogged about it), but not when the *.rdl resides on a reporting server. I am getting a 401 Not Authorized error at the line...
reportViewer.ServerReport.SetParameters(reportDefinition.ReportParameters);
Here's the method used to render the report.
public byte[] Render(IReportDefinition reportDefinition)
{
var reportViewer = new ReportViewer();
byte[] renderedReport;
try
{
var credentials = new WindowsImpersonationCredentials();
reportViewer.ServerReport.ReportServerUrl = new Uri("http://myssrsbox", UrlKind.Absolute);
reportViewer.ServerReport.ReportServerCredentials = credentials;
reportViewer.ServerReport.ReportPath = reportDefinition.Path;
// Exception is thrown on the following line...
reportViewer.ServerReport.SetParameters(reportDefinition.ReportParameters);
string mimeType;
string encoding;
string filenameExtension;
string[] streams;
Warning[] warnings;
renderedReport = reportViewer.ServerReport.Render(reportDefinition.OutputType, reportDefinition.DeviceInfo, out mimeType, out encoding, out filenameExtension, out streams, out warnings);
}
catch (Exception ex)
{
// log the error...
throw;
}
finally
{
reportViewer.Dispose();
}
return renderedReport;
}
The other thing that you're missing is the WindowsImpersonationCredentials class.
public class WindowsImpersonationCredentials : IReportServerCredentials
{
public bool GetFormsCredentials(out Cookie authCookie, out string userName, out string password, out string authority)
{
authCookie = null;
userName = password = authority = null;
return false;
}
public WindowsIdentity ImpersonationUser
{
get { return WindowsIdentity.GetCurrent(); }
}
public ICredentials NetworkCredentials
{
get { return null; }
}
public override string ToString()
{
return String.Format("WindowsIdentity: {0} ({1})", this.ImpersonationUser.Name, this.ImpersonationUser.User.Value);
}
}
Other things you may need to know...
This is running on an intranet, and impersonation is turned on.
Logging indicates that the impersonation user is being set correctly.
This does work when running in Visual Studio (http://localhost:devport), and it does work when running on my development box (http://localhost/myApplication). It does not work when running on our test or production servers.
I have tried solutions both with and without system.net.defaultProxy settings in web.config. Neither worked.
What am I doing wrong? Is it a server setting? Is it code? Is it web.config?

We did finally figure out the problem. Our network administrators have disabled double-hopping, so while the impersonation was correctly connecting as domain\jmeyer, the application was still attempting to connect to the SRS box with domain\web01$. Why is it set up like this? Because double-hopping is a massive security hole. (Or so I was told. Does this sound like something you would read on The Daily WTF?)
Our solution was to create a generic domain\ssrs_report_services user, and connect with that user with the following network credentials
public class CustomCredentials : IReportServerCredentials
{
public bool GetFormsCredentials(out Cookie authCookie, out string userName, out string password, out string authority)
{
authCookie = null;
userName = password = authority = null;
return false;
}
public WindowsIdentity ImpersonationUser
{
get { return null; }
}
public ICredentials NetworkCredentials
{
get { return new NetworkCredential("ssrs_report_services", "password", "domain") ; }
}
}
The above is the classic example solution that you can find all over the internets.

"Double hopping" is allowed - swith on Kerberos authentication ... (so long as it is working properly!)

Related

WebException using reportviewer

I'm trying to connect to ReportingServices through the ASP.NET web ReportViewer control:
rvContract.ProcessingMode = Microsoft.Reporting.WebForms.ProcessingMode.Remote;
rvContract.ServerReport.ReportServerCredentials = new ReportServerCredentials("myUsername", "myNetworkPassword", "DOMAIN");
rvContract.ServerReport.ReportServerUrl = new Uri(ReportConfiguration.ReportServerUrl);
string rptPath = ReportConfiguration.RootPath;
if (!rptPath.EndsWith("/"))
{
rptPath += "/";
}
rvContract.ServerReport.ReportPath = rptPath + "AdminReports/Contract";
List<ReportParameter> reportParams = new List<ReportParameter>();
if (MkoSession.AccountId.HasValue)
{
ReportParameter accountId = new ReportParameter("AccountId", MkoSession.AccountId.Value.ToString());
}
rvContract.ServerReport.SetParameters(reportParams);
rvContract.ShowParameterPrompts = false;
rvContract.ShowZoomControl = false;
rvContract.ServerReport.Refresh();
rvContract.DataBind();
The implementation of the credentials looks like this:
public class ReportServerCredentials : IReportServerCredentials
{
private string _userName;
private string _password;
private string _domain;
public ReportServerCredentials(string userName, string password, string domain)
{
_userName = userName;
_password = password;
_domain = domain;
}
public WindowsIdentity ImpersonationUser
{
get
{
// Use default identity.
return null;
}
}
public ICredentials NetworkCredentials
{
get
{
// Use default identity.
return new NetworkCredential(_userName, _password, _domain);
}
}
public bool GetFormsCredentials(out Cookie authCookie, out string user, out string password, out string authority)
{
// Do not use forms credentials to authenticate.
authCookie = null;
user = null;
password = null;
authority = null;
return false;
}
}
Just hard-coding my credentials while testing. Before check-in, we'll have to create a domain account for this.
I can hit both ReportService2005.asmx and ReportExecution2005.asmx (which is what my ReportServerUrl becomes) no problem through a browser.
When I get to the SetParameters call, I get a WebException. Looking at the headers in the Response within the Exception:
{RSNotAuthenticated: True
RSAuthenticationHeader: .ASPXFORMSAUTH
Content-Length: 206
Cache-Control: private
Content-Type: text/html; charset=utf-8
Date: Tue, 24 Sep 2013 16:15:08 GMT
Location: /ReportServer/logon.aspx?ReturnUrl=%2freportserver%2fReportExecution2005.asmx
Server: Microsoft-HTTPAPI/2.0
X-AspNet-Version: 2.0.50727
}
which seems as if it's telling me that I haven't logged in. If that's the case, how do the exact same credentials allow me to see the web services through a browser?
BTW, I did set breakpoints in each of the methods in my ReportServerCredentials implementation and saw each breakpoint hit. Not sure what that's telling us, but the NetworkCredentials interface returned my network credentials just fine.
I think I might've stumbled into it. In case anyone else is having similar troubles.
First, I poked around and found the reporting services web.config on the server. Saw that it was using authenticaion mode = forms.
So, then I implemented the GetFormsCredentials to return true and pass a generic username. I got this username out of the Users table in the ReportServer database.
No longer getting the WebException, but my ReportViewer isn't displaying either. So, not out of the woods yet, but it seems closer.
FWIW, I think I remember somebody saying we had weird custom forms authentication on our db server. So, for anybody who happens upon this, YMMV. All of that predates me, so I'm not really sure how to check it out.

Report Server Credentials and Missing End Point Exception

Actually what I needed was a step by step guide but anyway..
I have to show some rdl reports in a web-site using the ASP.NET report vievew and do all the necessary configurations for the Reporting Services. The users of the page should not deal with ANY authorization.
Here is my code for the report viewer:
rprtView.ServerReport.ReportServerCredentials = new ReportServerCredentials();
rprtView.ProcessingMode = Microsoft.Reporting.WebForms.ProcessingMode.Remote;
rprtView.ServerReport.ReportServerUrl = new Uri(#"http://mydomain/reports");
rprtView.ServerReport.ReportPath = #"/MyReports/PurchaseOrder";
rprtView.ShowParameterPrompts = false;
ReportParameter[] parameters = new ReportParameter[1];
parameters[0] = new ReportParameter();
parameters[0].Name = "OrderNumber";
parameters[0].Values.Add(orderNumber);
rprtView.ServerReport.SetParameters(parameters);
rprtView.ServerReport.Refresh();
Here is my overload for IReportServerCredentials
public class ReportServerCredentials : IReportServerCredentials
{
public bool GetFormsCredentials(out Cookie authCookie, out string userName, out string password, out string authority)
{
authCookie = null;
userName = password = authority = null;
return false;
}
public WindowsIdentity ImpersonationUser
{
get { return null; }
}
public ICredentials NetworkCredentials
{
get { return new NetworkCredential("myUserName", "myPassword"); }
}
}
I am able to login to "http://mydomain/reports", the default web site of the SSRS, using "myUserName" and "myPassword" (I am not sure if this is related). Still I am getting MissingEndPoint exception at SetParameters() method above. It says:
"The attempt to connect to the report server failed. Check your connection information and that the report server is a compatible version."
I am also responsible for configuring the Reporting Services for the necessary configuration for this scenario and I have heard that this issue is related to the config files in SSRS but I have no idea what to write in them. Any help is much appreciated!
The string provided for rprtView.ServerReport.ReportServerUrl should be for the Report Server service, not the Report Manager application.
Change this:
rprtView.ServerReport.ReportServerUrl = new Uri(#"http://mydomain/reports");
to this:
rprtView.ServerReport.ReportServerUrl = new Uri(#"http://mydomain/reportserver");
This page has some high-level info on the Report Manager interface, Report Server web service, and how they relate.

Forcing .net Login control to make the user logout if a cookie is null

I have a code base web application that is connected to 2 databases. Depending on which login control a user uses to login, a different database is connected to the code. I am doing all of this by a cookie. This cookie is in a public class called AuthenticatedUser. The class looks like this:
public class AuthenticatedUser : System.Web.UI.Page
{
public static string ConnectionString
{
get
{
HttpCookie myCookie = HttpContext.Current.Request.Cookies["connectionString"];
return GetConnectionStringFromName(myCookie);
}
set
{
if (HttpContext.Current.Request.Cookies["connectionString"] != null)
{
ExpireCookies(HttpContext.Current);
}
var allCookies = HttpContext.Current.Request.Cookies.AllKeys;
HttpCookie cookie = new HttpCookie("connectionString");
cookie.Value = value;
cookie.Expires = DateTime.Now.AddYears(100);
HttpContext.Current.Response.Cookies.Add(cookie);
}
}
private static string GetConnectionStringFromName(HttpCookie myCookie)
{
try
{
string connectionStringName = myCookie.Value;
return ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
}
catch
{
FormsAuthentication.SignOut();
}
finally
{
HttpContext.Current.Response.Redirect("/default.aspx");
}
return "";
} private static void ExpireCookies(HttpContext current)
{
var allCookies = current.Request.Cookies.AllKeys;
foreach (var cook in allCookies.Select(c => current.Response.Cookies[c]).Where(cook => cook != null))
{
cook.Value = "";
cook.Expires = DateTime.Now.AddDays(-1);
current.Request.Cookies.Remove(cook.Name);
cook.Name = "";
}
}
}
This seems to be working on my development machine, but when I tried to deploy it, any user that was using the "remember me" option on the site was getting a null reference error because they did not use the login control to obtain the cookie.
What is the best method to get around this? I was thinking if a user was logged in but the AuthenticatedUser class could not get a Connectionstring to log out the user to force them to use the login control again. What should I do?
Try use:
try
{
FormsAuthentication.SignOut();
}
finally
{
Response.Redirect("~/Home.aspx");
}
This way is preferable, for example if in some time you will decide not- cookie auth, but URL based - the FormsAuthentication will manage it gracefully.

Active Directory Account locking out on first try

I have a website which requires users to enter their corporate network username and password. It then looks for that account in Active Directory and gets a list of any email addresses associated with that account.
The problem I am having is that ONE incorrect password is locking out an account. Our domain policy is that an account will lock out after three incorrect entries, so I am assuming that I am doing something wrong in my code. I am not very knowledgeable about Active Directory or .NET DirectoryServices in general, which may be apparent from my code. Here it is:
public ArrayList AuthenticateActiveDirectory(string Domain, string UserName, string Password)
{
// An error occurs if the username/password combo does not exist.
// That is how we know it is not a valid entry.
try
{
DirectoryEntry entry = new DirectoryEntry("LDAP://" + Domain, UserName, Password);
object nativeObject = entry.NativeObject;
ArrayList emails = new ArrayList();
DirectorySearcher ds = new DirectorySearcher(entry);
ds.Filter = "samaccountname=" + UserName;
ds.PropertiesToLoad.Add("mail");
SearchResult sr = ds.FindOne();
if (sr.Properties["mail"] != null)
{
for (int email = 0; email < sr.Properties["mail"].Count; email++)
{
emails.Add(sr.Properties["mail"][email]);
}
}
return emails;
}
catch (DirectoryServicesCOMException) { throw; }
catch (Exception) { throw; }
}
I did some searching and found some code (thanks to Ayende Rahien for the solution) to use that just authenticates and doesn't search for emails or anything else. I am using this prior to the other function, and it seems to be working fine. I am guessing that my other code is hitting AD more than once - at least 3 times - which is resulting in the lockout. Here is the code I am using now to just authenticate:
private bool Authenticate(string domain, string user, string password)
{
try
{
using (DirectoryEntry de = new DirectoryEntry("LDAP://" + domain,
user, password))
{
return de.NativeObject != null;
}
}
catch
{
return false;
}
}

Is Roles.IsUserInRole behaving as expected in the following simple scenario?

In a custom role provider (inheriting from RoleProvider) in .NET 2.0, the IsUserInRole method has been hard-coded to always return true:
public override bool IsUserInRole(string username, string roleName) { return true; }
In an ASP.NET application configured to use this role provider, the following code returns true (as expected):
Roles.IsUserInRole("any username", "any rolename"); // results in true
However, the following code returns false:
Roles.IsUserInRole("any rolename"); // results in false
Note that User.IsInRole("any rolename") is also returning false.
Is this the expected behavior?
Is it incorrect to assume that the overload that only takes a role name would still be invoking the overridden IsUserInRole?
Update: Note that there doesn't seem to be an override available for the version that takes a single string, which has led to my assumption in #2.
I looked at Roles.IsUserInRole(string rolename) in .net reflector, and it resolves to the following:
public static bool IsUserInRole(string roleName)
{
return IsUserInRole(GetCurrentUserName(), roleName);
}
I would take a look at your current user. Here's why:
private static string GetCurrentUserName()
{
IPrincipal currentUser = GetCurrentUser();
if ((currentUser != null) && (currentUser.Identity != null))
{
return currentUser.Identity.Name;
}
return string.Empty;
}
I would be willing to bet this is returning an empty string because you either don't have a Current User, or its name is an empty string or null.
In the IsUserInRole(string username, string roleName) method, there is the following block of code right near the beginning:
if (username.Length < 1)
{
return false;
}
If your GetCurrentUserName() doesn't return anything meaningful, then it will return false before it calls your overridden method.
Moral to take away from this: Reflector is a great tool :)
Also beware if you have selected cacheRolesInCookie="true" in the RoleManager config. If you have added a new role to the database, it might be looking at the cached version in the cookie.
I had this problem and the solution was to delete the cookie and re-login.
This may help someone - be aware:
If you are using the login control to authenticate - the username entered into the control becomes the HttpContext.Current.User.Identity.Name which is used in the Roles.IsUserInRole(string rolename) and more specifically - the membership's GetUser() method. So if this is the case make sure you override the Authenticate event, validate the user in this method and set the username to a value that your custom membership provider can use.
protected void crtlLoginUserLogin_Authenticate(object sender, AuthenticateEventArgs e)
{
bool blnAuthenticate = false;
string strUserName = crtlLoginUserLogin.UserName;
if (IsValidEmail(strUserName))
{
//if more than one user has email address - must authenticate by username.
MembershipUserCollection users = Membership.FindUsersByEmail(strUserName);
if (users.Count > 1)
{
crtlLoginUserLogin.FailureText = "We are unable to determine which account is registered to that email address. Please enter your Username to login.";
}
else
{
strUserName = Membership.GetUserNameByEmail(strUserName);
blnAuthenticate = Membership.ValidateUser(strUserName, crtlLoginUserLogin.Password);
//setting the userLogin to the correct user name (only on successful authentication)
if (blnAuthenticate)
{
crtlLoginUserLogin.UserName = strUserName;
}
}
}
else
{
blnAuthenticate = Membership.ValidateUser(strUserName, crtlLoginUserLogin.Password);
}
e.Authenticated = blnAuthenticate;
}

Resources