SSRS Forms Authentication - How To Pass Cookie Credentials To Report Server - c#

I am currently attempting to render the SSRS report in my web application using forms authentication.
My SSRS Report Version is 2016.
Initially I was under the impression that NetworkCredentials would work, and after encountering errors, I found that we are required to use FormsAuthentication, with passing the cookie as a means of authenticating the user.
I have done the necessary settings on the config files in the Reporting Server by following the guide from the link below:-
https://github.com/Microsoft/Reporting-Services/tree/master/CustomSecuritySample2016
The reporting services works as intended on the localhost/ReportServer and on
the SSRS Portal, localhost/Reports. I am also able to access said server
remotely.
Below is the code I used to obtain the authenticated cookie.
MyReportingService rsClient = new MyReportingService();
rsClient.Url = "http://xxx.xxx.xxx.xxx/reportserver/ReportService2010.asmx";
try
{
rsClient.LogonUser("user", "password", "");
Cookie myAuthCookie = rsClient.AuthCookie;
HttpCookie cookie = new HttpCookie(myAuthCookie.Name, myAuthCookie.Value);
Response.Cookies.Add(cookie);
}
Which supposedly would then be used to authenticate the user.
Cookie authCookie = new Cookie(cookie2.Name, cookie2.Value);
authCookie.Domain = "DomainName";
rvSiteMapping.ServerReport.ReportServerCredentials = new MyReportServerCredentials(authCookie);
rvSiteMapping.ServerReport.Cookies.Add(authCookie);
And in my forms authentication within the IReportsServerCredentials Class:-
public bool GetFormsCredentials(out Cookie authCookie,
out string user, out string password, out string authority)
{
authCookie = m_authCookie;
user = password = authority = null;
return true; // Use forms credentials to authenticate.
}
The issue I am experiencing is when the application is passing the credentials to the report server. I believe I must be doing this part incorrectly because while my application does get the cookie, when it authenticates the credentials provided by the cookie, I receive the text/html error:-
Object moved to <a href="/ReportServer/logon.aspx?ReturnUrl=%2fReportserver%2fReportExecution2005.asmx" />
This error is in response to setting a default generic Identity in the event that
the HttpContext.Current.User = null.
if (HttpContext.Current != null
&& HttpContext.Current.User != null)
{
userIdentity = HttpContext.Current.User.Identity;
}
else
{
userIdentity = new GenericIdentity("AnonymousUser");
}
I have tried googling the answer but most of the results are for
windows authentication and the few that are related to forms authentication
are very similar to the code I referred to.

The underlying cause of the issue was under my nose the whole time.
The domain name should refer to the web domain and not the active directory domain.
authCookie.Domain = "DomainName";
The cookie is now able to authenticate the user as intended.
Hopefully this helps anyone who happens to make the same mistake.

Related

Authentication/Authorization in ASP.NET Core using Enterprise SingleSignon page and Microsoft SignInManager

Presently I am working on an authentication issue in one of my ASP.NET Core(3.0) application.
To give some background, we have to use an Enterprise Single sign on page to authenticate the users. Once the user got authenticated it redirects back to our application along with user name in a HTTP header called "SM_USER". Then using that information we load corresponding Claim information from DB using Microsoft SignInManger. The application is working fine if the user is accessing the root but it couldn't able to access if they are trying to navigate a specific page directly, like http://website/Controller/Index.
I am suspecting that we may have implemented it wrongly so would like to know how should the below scenario to be implemented?
Our users first get authenticated using an enterprise Login Page(Single sign on) then redirected to our application. user information available on HTTP Headers and the corresponding claims information is available in our DB which we need to use for authorization purpose(we wanted to use Microrosoft SignInManger to load them).
I found the issue after researching it throughly so sharing here in case it useful for someone.
we observed whenever a user try to access a page if the user is not authenticated then it is redirecting Login page(Observed that decorating them with [Authorize] attribute is causing this), where We are using this Login page for local development purpose.
so when the user get redirected to login page and if the environment is not development then below code is executed on the Get method, which takes care of signing the user and creating UserPrincipal. Then after that we redirecting to the page the user requested.
if (!_signInManager.IsSignedIn(User))
{
string userName = HttpContext.Request.Headers["SM_USER"].ToString();
if (userName.Length > 0)
{
var user = await _userManager.FindByNameAsync(userName);
if (user != null)
{
var claimsPrincipal = await _signInManager.CreateUserPrincipalAsync(user);
await _signInManager.Context.SignInAsync(IdentityConstants.ApplicationScheme,
claimsPrincipal,
new AuthenticationProperties { IsPersistent = true });
if (string.IsNullOrEmpty(returnUrl)) //returnUrl is a parameter get passed by the system.
{
return RedirectToAction("<Action>", "<Controller>");
}
else
{
return Redirect(returnUrl);
}
}
}
}

Authentication against local AD in the Angular application

I've been developing an Angular app with .NET Core backend (services). The task is to enable an integrated authentication, i.e. make it work with the local user seamlessly, so I login to my (connected to a local AD) machine once and the web application lets me in without the necessity to login a second time. We've been working with Identity Server 4 and intended to implement this scenario using it.
There is a little documentation on the official website concerning the Windows Authentication (e.g. against Active directory): http://docs.identityserver.io/en/latest/topics/windows.html but it doesn't explain much. As per my info, to make this scenario work the browser utilizes either Kerberos or NTLM. Neither of them is mentioned in the IS4 docs. I'm lacking the understanding of how the local credentials are getting picked up and how IS4 'knows' the user belongs to AD? How I can make sure only the users from a specific domain have access to my app?
I found some working stuff here https://github.com/damienbod/AspNetCoreWindowsAuth but questions remain the same. Even though I was able to get to the app with my local account I don't understand the flow.
I expect the user utilizing the app in the local network to log-in to the app without entering the login/password (once he's already logged in to the Windows). Is this something achievable?
Identity Server is intended to serve as an Identity Provider, if you need to talk with your AD you should see the Federation Gateway architecture they propose using the IAuthenticationSchemeProvider. Where Identity Server acts as an endpoint and talks with your AD.
This is the link:
http://docs.identityserver.io/en/latest/topics/federation_gateway.html
You have the control to programmatically reach your AD and pass the correct credentials to get the authentication. That step should be done in your Identity Server. After you get authenticated you should get redirected to your application again.
About your last question, the answer is yes, if you have your website hosted on an intranet and you have the access to your AD, you don't need to capture your credentials as user input, you can programmatically reach the AD as I said.
Bellow is the code I use to connect with my active directory
On the ExternalController class, you get when you use IdentityServer, you have this:(I don't remember at the top of my head how much I changed from the original code, but you should get the idea)
/// <summary>
/// initiate roundtrip to external authentication provider
/// </summary>
[HttpGet]
public async Task<IActionResult> Challenge(string provider, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
// validate returnUrl - either it is a valid OIDC URL or back to a local page
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
if (AccountOptions.WindowsAuthenticationSchemeName == provider)
{
// windows authentication needs special handling
return await ProcessWindowsLoginAsync(returnUrl);
}
else
{
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", provider },
}
};
return Challenge(props, provider);
}
}
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, testing windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
If you want to use Azure AD, I would recommend you to read this article:
https://damienbod.com/2019/05/17/updating-microsoft-account-logins-in-asp-net-core-with-openid-connect-and-azure-active-directory/
Not sure if it's what you want, but I would use the Active Directory Federation Services to configure an OAuth2 endpoint and obtain the user token in the .Net Core Web App.
Isn't NTLM authentication support limited on non Microsoft browsers?
OAuth2 have the advantage of using only standard technologies.
One way to do it is to have 2 instances of the app deployed.
The first one is configured to use Windows Authentication and the other one uses IS4.
ex:
yoursite.internal.com
yoursite.com
Your local DNS should redirect traffic internally from yoursite.com to yoursite.internal.com
yoursite.internal.com will be the one configured to use AD authentication. You should have a flag in your appsettings.json to indicate if this instance is a AD auth or IS4 auth.
The downside of this solution is that you have to deploy 2 instances

Delegation not working

I'm trying to get delegation working to call a webservice using WebClient (or HttpClient for that matter). I have a MVC4 web application with a controller and a Web Api controller. I have the application set up on a remote server on the intranet, and I'm running the same application on my local computer, using IIS on both computers, with Basic Authentication.
The Web Api controller is simple, this is the code for the service:
[Authorize(Users="domain\\username")]
[HttpPost]
[HttpGet]
public string GetSimulationTest()
{
return "WS successfully called";
}
This WS works perfectly fine from my browser or fiddler, both locally and remote.
In my home controller, I have the following method:
public ActionResult TestOwnWS()
{
ContentResult res = null;
WindowsIdentity wi = (WindowsIdentity)User.Identity;
WindowsImpersonationContext ctx = null;
try
{
ctx = wi.Impersonate();
WebClient wc = new WebClient();
wc.UseDefaultCredentials = true;
wc.Headers.Add("Content-Type", "application/xml");
res = this.Content(wc.DownloadString("linktoWS"), "application/xml");
}
catch (Exception e)
{
Response.Write(e.Message);
Response.End();
}
finally
{
ctx.Undo();
}
return res;
}
Here's the problem: I get 401 Unauthorized on the wc.DownloadString() call. It doesn't even work if I use the local webservice rather than the remote one. If I however set up wc.Credentials manually using wc.Credentials = new NetworkCredentials(user,pass,domain); it works.
I used Windows Authentication before, but it still refused to work, so I read up on delegation and apparently, it should work fine if the accounts are identical on both computer (which they are) with basic authentication while Windows Authentication is more finicky.
Why does it refuse to use the default credentials, am I still getting delegation wrong? I've read msdn articles on it and I can't find what I'm doing incorrectly.
Authorize is a filter that allows certain users to access the action method.
WindowsIdentity wi = (WindowsIdentity)User.Identity;
The above code still gets the identity of the logged-in user. Impersonation is not the right way to do this. The code for impersonation requires you to login with the impersonated user, and get the Windows identity with a handle.
MSDN Link
The best way is to use the NetworkCredentials as you have mentioned.

BootstrapContext in claimsprincipal null

We are using WIF in out MVC 4 web application. We also have set up an authentication with an STS.
So far, so good. Now the problem: When we login and return to the website we can see our username. We are also able to logout.
Because of securityreasons we make use of the bootstrap token to secure calls to out API. Problem is that after login the bootstrap token is NULL, while the claims are present.
When the apppool is recycled or the website is rebuild in Visual Studio, the token is present and shown on the website.
This is how we get the token now:
var claimsPrincipal = (ClaimsPrincipal)System.Web.HttpContext.Current.User;
var bootstrapContext = (BootstrapContext)claimsPrincipal.Identities.First().BootstrapContext;
ViewBag.Token = bootstrapContext.Token;
Does anyone knows how to get the token without recycling the app pool or rebuilding the website?
There is a way to retrieve it from the SecurityToken property on BootstrapContext, assuming that the property value is not null.
The following code snippet extracts a SAML token from the security token and writes it to a string builder:
var builder = new StringBuilder();
using (var writer = XmlWriter.Create(builder)) {
new SamlSecurityTokenHandler(new SamlSecurityTokenRequirement()).WriteToken(writer, bootstrapContext.SecurityToken);
}
var tokenXml = builder.ToString();

EnableCrossAppRedirects - Where is the cross-domain feature documented?

Here an interesting feature of ASP.NET FormsAuthentication explained in this SO answer: How do you pass an authenticated session between app domains
Quick summary; you can create two ASP.NET websites with the same encryption keys. WebsiteA can create a formsauth token, and redirect to WebsiteB with the token in the querystring (or POST body). Switch on EnableCrossAppRedirects in WebsiteB and ASP.NET detects the token and creates the formsauth cookie. In code:
FormsAuthentication.RedirectFromLoginPage("alice", true);
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket("Alice", true, 30);
string encrypted = FormsAuthentication.Encrypt(ticket);
Response.Redirect("http://siteb.dev/Secure/WebForm1.aspx?" + FormsAuthentication.FormsCookieName + "=" + encrypted);
Sounds like a great feature, but where is it documented? I'd feel a bit uneasy using a undocumented feature.
Where I've looked - no mention of this feature in any of the MSDN reference. I thought maybe RedirectFromLoginPage would build a redirect like my code above, it doesn't.
EnableCrossAppRedirects - "is checked within the RedirectFromLoginPage method when the redirect URL does not point to a page in the current application. If EnableCrossAppRedirects is true, then the redirect is performed"
Forms Authentication Across Applications - some advice on setting the machine keys so that a cookie created on a sub-domain, nothing about EnableCrossAppRedirects
forms Element for authentication
Having looked at reflector there is a (somewhat undocumented) feature of forms Authentication. When EnableCrossAppRedirects is enabled .NET will, in addition to looking for the auth cookie, attempt to extract the forms authentication "cookie" from either the form post or the query string. This code is embedded in the FormsAuthentication class in the ExtractTicketFromCookie method, where it can clearly been seen trying to find the authentication cookie in the request data.
if (FormsAuthentication.EnableCrossAppRedirects)
{
text = context.Request.QueryString[name];
if (text != null && text.Length > 1)
{
if (!cookielessTicket && FormsAuthentication.CookieMode == HttpCookieMode.AutoDetect)
{
cookielessTicket = CookielessHelperClass.UseCookieless(context, true, FormsAuthentication.CookieMode);
}
try
{
formsAuthenticationTicket = FormsAuthentication.Decrypt(text);
}
catch
{
flag2 = true;
}
if (formsAuthenticationTicket == null)
{
flag2 = true;
}
}
if (formsAuthenticationTicket == null || formsAuthenticationTicket.Expired)
{
text = context.Request.Form[name];
if (text != null && text.Length > 1)
{
if (!cookielessTicket && FormsAuthentication.CookieMode == HttpCookieMode.AutoDetect)
{
cookielessTicket = CookielessHelperClass.UseCookieless(context, true, FormsAuthentication.CookieMode);
}
try
{
formsAuthenticationTicket = FormsAuthentication.Decrypt(text);
}
catch
{
flag2 = true;
}
if (formsAuthenticationTicket == null)
{
flag2 = true;
}
}
}
}
Therefore if you enable EnableCrossAppRedirects on both applications, then the first application is authorised to redirect to the external site, and the second application will automatically read in the authentication cookie from the request. You just need to engineer it so that the return login URL either posts the cookie data or sends it in the querystring. You also need to be sure that either the machine keys are synchronised, or that the cookie is encrypted using the external apps machine key (by the first app). It seems by default .NET will send the encrypted authentication cookie in the querystring for you and asume your machine keys are in sync (see MSDN quote below).
Here's some more info on MSDN .
If the CookiesSupported property is true, and either the ReturnUrl
variable is within the current application or the
EnableCrossAppRedirects property is true, then the
RedirectFromLoginPage method issues an authentication ticket and
places it in the default cookie using the SetAuthCookie method.
If CookiesSupported is false and the redirect path is to a URL in the
current application, the ticket is issued as part of the redirect URL.
If CookiesSupported is false, EnableCrossAppRedirects is true, and the
redirect URL does not refer to a page within the current application,
the RedirectFromLoginPage method issues an authentication ticket and
places it in the QueryString property.
There is a big warning about the impact on security. EnableCrossAppRedirects is a security setting which prevents ASP.NET login controls from redirecting to an external return URL (another web application). With this setting enabled it can be exploited in some forms of attack - a user is sent to the official login page, but on login is redirected to a different application which they may believe is the same. This is why it's disabled by default.
One way to help mitigate this when enabling the feature is as follows:
To improve security when using cross-application redirects, you should
override the RedirectFromLoginPage method to allow redirects only to
approved Web sites.
You also need to ensure the redirect request is served over SSL to protect the "cookie" in transit, as anyone intercepting would be able to gain control of the account.

Categories