How to fix browser back button issue after SAML SSO from Shibboleth - asp.net-mvc-3

Have a SAML2 Service Provider web application build on .NET MVC (3). We are using ComponentSpace's SAML2 library to do the authentication, not the Shibboleth SP. Here is the relevant controller logic:
public class SignOnController : Controller
{
// constructor-inject _services
[HttpPost]
public ActionResult SendAuthnRequest(string userName, string returnUrl)
{
// this users ComponentSpace internally to push user to IdP
_services.SamlServiceProvider.SendAuthnRequest(args);
return new EmptyResult();
}
[HttpPost]
public ActionResult ReceiveAuthnResponse()
{
var samlResponse = _services.SamleServiceProvider
.ReceiveSamlResponse(args);
// ...
return Redirect(samlResponse.RelayResourceUrl ?? defaultUrl);
}
}
Ultimately the user lands on either the relay url or the default login page. However when they click a link, then click the back button, the browser goes back to an error page on the Shibboleth IdP server. After that, the browser forward and back buttons ultimately become useless.
Am I doing something wrong in either of the above methods? Should I be returning a View() instead of an EmptyResult() when sending? Is there some way to reset the browser history to prevent repeated postbacks back to the IdP? Is this something that I can configure in ComponentSpace's implementation?

So I understand that you hit back button during authentication in IdP i WBSSO SAML2 profile? I don't know components which you enumerated but in Shibboleth case it always ends with error page. I may be wrong but if you consider the nature of SP->IdP communication I believe there is no other option, unless you reimplement something.

The SAML protocols messages used in SAML SSO as sent via the browser. Clicking the browser forward and backward navigation buttons can be problematic when using SAML SSO, as you have seen.
There is no simple solution to this problem. You could look at incorporating Javascript to disable the navigation buttons but that most likely will cause other issues.
I think the best and simplest solution is to display a generic error message if this occurs. This is something you need to do in your application.
Our MvcExampleIdentityProvider and MvcExampleServiceProvider applications that we ship don't have issues in most cases. They use the SAML high-level API as opposed to the SAML low-level API used in the question but this shouldn't make a difference. In our examples we return an EmptyResult as well.

We faced the same problem and worked around it with JavaScript history API:
https://www.thecssninja.com/javascript/stealing-history-api
It's not nice though since essentially the browsing history is manipulated. But it works efficiently and prevents the error page from showing up. Back/forward buttons inside your web app continue to work correctly but there's no way for the user to get back to any previous page which is not your web app. That was a compromise we could live with.

We use this script. It checks if the main url is called from an SSO redirect. If so, it skips the SSO page when the user clicks the back button. If not, the script does nothing because the back button is working as expected.
(function(window, location) {
var previous = document.referrer.toLowerCase();
var issso = previous.indexOf('account/sso');
if(issso >= 0){
history.replaceState(null, document.title, location.pathname+"#!/skipsso");
history.pushState(null, document.title, location.pathname);
window.addEventListener("popstate", function() {
if(location.hash === "#!/skipsso") {
setTimeout(function(){
history.go(-2);
},0);
}
}, false);
}
}(window, location));

Related

Microsoft Teams App - Add Authentication and Authorization for Task/Fetch Card Action

Located in the BotBuilder-Samples GitHub repo: https://github.com/microsoft/BotBuilder-Samples
There is a sample app: 54.teams-task-module. This app demonstrates a task/fetch action with a Url to a Custom Form which is rendered by a Razor Page.
https://github.com/microsoft/BotBuilder-Samples/tree/main/samples/csharp_dotnetcore/54.teams-task-module
In the Bot, the OnTeamsTaskModuleFetchAsync method is overridden to return a TaskModuleResponse which tells the system to fetch the URL passed back to Teams in the response.
https://github.com/microsoft/BotBuilder-Samples/blob/main/samples/csharp_dotnetcore/54.teams-task-module/Bots/TeamsTaskModuleBot.cs
protected override Task<TaskModuleResponse> OnTeamsTaskModuleFetchAsync(ITurnContext<IInvokeActivity> turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken)
{
var asJobject = JObject.FromObject(taskModuleRequest.Data);
var value = asJobject.ToObject<CardTaskFetchValue<string>>()?.Data;
var taskInfo = new TaskModuleTaskInfo();
switch (value)
{
case TaskModuleIds.YouTube:
taskInfo.Url = taskInfo.FallbackUrl = _baseUrl + "/" + TaskModuleIds.YouTube;
SetTaskInfo(taskInfo, TaskModuleUIConstants.YouTube);
break;
case TaskModuleIds.CustomForm:
taskInfo.Url = taskInfo.FallbackUrl = _baseUrl + "/" + TaskModuleIds.CustomForm;
SetTaskInfo(taskInfo, TaskModuleUIConstants.CustomForm);
break;
case TaskModuleIds.AdaptiveCard:
taskInfo.Card = CreateAdaptiveCardAttachment();
SetTaskInfo(taskInfo, TaskModuleUIConstants.AdaptiveCard);
break;
default:
break;
}
return Task.FromResult(taskInfo.ToTaskModuleResponse());
}
I have enabled developer tools in Teams and watched the network requests, as well as overridden every method I can find to try find an extensibility point to inject some sort of token into the request so that the URL can be secured from public anonymous access.
Question: The only way to provide authorization on the Razor Page I see right now is passing the token on the query string and using a custom authorization handler to process the token.
Is there a better way to inject a token or any other info into the task/fetch request so that the request can be authenticated and authorized?
To be clear on this, authentication -is- possible, but only for web pages (Adaptive Cards don't need it). This auth would rely on the standard SSO Teams offers for Task Modules as well as Tabs. See here for intro guidance: https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/authentication/auth-aad-sso?tabs=dotnet, especially:
The SSO API also works in task modules that embed web content.
So really your question kind of becomes "how do I do SSO for web content in Teams". Here's a great video, which includes a link to text (blog) version of the content: https://devblogs.microsoft.com/microsoft365dev/lets-decode-single-sign-on-sso-in-microsoft-teams-tabs/. Here's a working sample, with both Node and DotNet backend options: https://adoption.microsoft.com/sample-solution-gallery/pnp-sp-dev-teams-sample-tab-sso. Note that the samples and docs generally focus on doing an OnBehalfOf (OBO) operation to call Graph, but the principle remains the same - you get a JWT token that you can pass back to your backend, which you can then validate. You can also, from the token, get user info for the logged in user.
From my comments: Looking at it as "Web inside Adaptive" and revisiting the sample project and your information it does seem the "CustomForm" razor page is initializing the Teams JavaScript SDK.
This DOES mean I can authenticate this content using the SSO as you mentioned.
I had only thought it would work in a TAB, not inside a bot card.Solved, follow the tabs javascript SDK guidance.

auth0.js checkSession() returns a login_required error after logging in with login() from an embedded page

I am trying to add ‘keep me logged in’ functionality to my auth0 v9.3.0 authentication flow in my SPA. I have an embedded page at app.domain.io/login where the user enters their credentials. When they hit the sign-in button, the app calls the login() method from auth.js, passing in the username and password. If they checked the box, a permission cookie is set to true.
When the user launches the page later, after the token expires, a router guard calls auth0.js’s checkSession() method to get a new token. At this point, checkSession returns a login_required error even after the user logged in with auth0.js’s login() method. If I instead call the authorize() method and have the user log in on the hosted page, checkSession succeeds and does not return a login_required error.

Why does calling the login() method from the embedded page not fulfill the login_required requirement that authorize() fulfills? I want to get this working without ever redirecting the user to the hosted auth0 page.
Update: 03/28/18
I am currently using auth0 v9.3.0.
Instead of calling the login() method, I am now using axios to make a request to the co/authenticate endpoint. This succeeds and returns a login_ticket, co_id, and co_verifier.
When I call authorize() from auth0.js and pass in the login_ticket as mentioned in the documentation (https://raw.githubusercontent.com/jaredhanson/draft-openid-connect-cross-origin-authentication/master/Draft-1.0.txt), I get a ‘No verifier returned from client’ error. I have the co_verifier, but I’m not sure what to do with it.
Here is a fully working sample to follow (SPA using plain JavaScript). The sample illustrates both embedded login and universal login (auth0 hosted login page) approaches.
If you have a codebase on Github and don't mind sharing, then I can take a look for you. Please also update your question to indicate version of auth0.js you are using (or put in comment section below). Do you know (you can check using web browser developer tools) whether you are using co/authenticate endpoint when authenticating using auth0.js embedded login? If so, then you would have a Single Sign On session. However, if you are using oauth/token endpoint then this would not create a single sign on session. the checkSession() function calls authorize?prompt=none under the covers, which detects whether a SSO session is present as part of the authentication process.
Finally, and just for the record, the strong recommendation is to use Auth0 Hosted Login Page (Universal Login). It is a superior approach from security standpoint, and offers other benefits like easy opt-in to services like MFA out of the box. Finally, you could also enable Custom Domains so that your website and the Auth0 Hosted Login Page share the same URL base origin (domain) - end users of your site would not recognise they have been redirected to Auth0 for the authentication. So it is pretty seamless from a UX perspective too.
This issue was solved by calling auth.crossOriginAuthentication.login() instead of auth.client.login(). auth.crossOriginAuthentication.login() goes through co/authenticate, auth.client.login() goes through oauth/token.

Unable to complete Azure ACS Authentication in MVC Applications

I have an MVC 3 Application that I am trying to integrate with Azure-hosted ACS Identity Providers. I have been following the Tutorials but they do not appear to be working for me when using ASP.NET MVC.
Essentially, when I hit the View which I've flagged with [Authorize] the user is redirected to the Azure-hosted Login page with the list of Identity Providers. I choose a provider (in this case Live) and log in. At this point this all works as I expect. After I successfully authenticate it appears (visually) that I'm not redirected back to my application, instead I'm returned to the Identity Providers page. When watching this in Fiddler, it appears it actually returns but then starts the cycle all over again (HTTP Status Code 302).
Can someone explain what may be causing this?
Within the Azure portal, I have the following Urls configured for my relying party application
Realm: http: //localhost:7777/
Return Url: http: //localhost:7777/ (I also tried http: //localhost:7777/Home/About)
In all other cases I have the default settings
The urls match what is in the Web.config (including the trailing slash)
There is only one controller with the following:
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[Authorize]
public ActionResult About()
{
Response.Write("Claims Received from ACS:");
ClaimsIdentity ci = Thread.CurrentPrincipal.Identity as ClaimsIdentity; foreach (Claim c in ci.Claims)
{
Response.Write("Type: " + c.ClaimType + "- Value: " + c.Value + "");
}
return View();
}
}
Note: This is a brand new project created to work through this integration. All the packages and related SDKs are all up-to-date.
I would like to know whether you mean is when you log in and return to your web application, it thinks you’re not logged in, and redirects you to the identity provider’s sign in page again.
Please check if you’ve configured the authorization logic correctly. For example, if you use role based authorization, it is needed to configure ACS to create a rule that returns a role. You can also use custom authorization instead of the Authorization attribute. In your custom authorization code, you can check if the required claims are present. The claims can be role or anything else (user name, age, etc.). Custom authorization is usually more agile than the Authorization attribute.
Best Regards,
Ming Xu.
Please make sure you haven’t modified the sample code. Since it is an official ACS SDK sample, a lot of people have tried it and it will work.
Also in your original post, you mentioned you’ve configured ASP.NET authorization:
<authorization>
<deny users="?" />
</authorization>
Please remove this (as indicated in the document), if you don’t want to use ASP.NET authorization(you want to use WIF).
Best Regards,
Ming Xu.

How to see the redirect status from an STS / IdP

I have searched (probed ,even) for an answer to this but haven't come up with anything useful so far. I'm pretty new to ADFS, STS's in general and WIF so please excuse any obvious ignorance or inappropriate use of terminology. ;)
I'm currently integrating a custom MVC3 app with an external IdP, via ADFS. The ADFS to IdP setup is all done and working.
Some parts of the site are accessible to anon users - in the web.config authentication mode has been set to none. The other parts are protected by having their controllers/action methods decorated by a custom System.Web.Mvc.AuthorizeAttribute.
All the usual modifications to the web.config for using the WsFederationAuthenticationModule have been made and it works 95%; the user can browse to the anon accessible parts of the site. When they try and hit the protected parts, the authorize attribute checks if they have some custom information from our IdP in the IClaimsPrincipals associated with the HttpContext.Current.User and then sets the ActionResult to 401 if not; The WsFederationAuthenticationModule kicks in and redirects them to the IdP's login page. When they enter their details, they're then successfully redirected with some FedAuth cookies and the authorization then passes.
The problem starts when they get to the IdP's login page. This particular IdP has a link to return you directly to our site (to the same page the original request was made to), with this SAML response embedded somewhere (this is according to their documentation)
urn:oasis:names: tc:SAML:2.0:status: AuthnFailed
At this point, they are now "Unauthorized" and all the user will see (at least in dev) is a 401 page. You have to kill the session or otherwise get rid of that cookie to start again.
What I need to do is intercept that redirect request from the IdP, and essentially check for that particular SAML status, because the user should then be redirected to one of the unauthorized areas as if nothing has happened. I've tried something like this in the global.asax:
protected void Application_Start()
{
// mvc stuff here....
// add handler to intercept handling creation of security tokens by WsFederationAuthnticationModule
FederatedAuthentication.ServiceConfigurationCreated += OnServiceConfigurationCreated;
}
void OnServiceConfigurationCreated(object sender, ServiceConfigurationCreatedEventArgs e)
{
FederatedAuthentication
.WSFederationAuthenticationModule
.SessionSecurityTokenCreated += WSFederationAuthenticationModule_SecuityTokenCreated;
}
public void WSFederationAuthenticationModule_SecuityTokenCreated (Object sender, SessionSecurityTokenCreatedEventArgs args)
{
var token = args.SessionToken;
// do something with the session token here e.g. check for SAML status
}
.. but I cant see anything useful on that token; nothing to indicate a specific response status. The fact that there is a FedAuth cookie at all but no custom info from the Idp is a dead give away that the user has been there but somehow failed to authenticate, but in principle I want to be able to see that status. I might have to deal with timeouts at the IdP as well....
Maybe I'm doing this all wrong, or just plain old don't understand, but can somehow fill me in on how to determine those response statuses?
Phew. Thank you! :D
Ok, so I'm going to answer my own question.
The answer to whether I can get that custom status from my IdP is a no, at the moment. :(
But this is only because ADFS is not setup to capture it and pass it on. Apparently you need to do some custom coding for capturing information from the back channel that is opened between ADFS and the IdP.... well beyond the current scope of work.
As a work around for the moment:
If a request is made to the site and there is NO SAML token, its a new request by a user who has made no auth attempt at the Idp
If there is a SAML token but no ID from the IdP in the token (which is only present when they auth properly), then the user failed Auth for some reason
If there is a SAML token with the ID present, the user auth'd properly
Not great but acceptable. BTW, all credit goes to YMC in this SO post for the following code which lets you check for SAML tokens:
void WSFederationAuthenticationModule_SecurityTokenReceived(object sender, SecurityTokenReceivedEventArgs e)
{
var message = SignInResponseMessage.CreateFromFormPost(Request) as SignInResponseMessage;
var rstr = new WSFederationSerializer()
.CreateResponse(message,
new WSTrustSerializationContext(
SecurityTokenHandlerCollectionManager.CreateDefaultSecurityTokenHandlerCollectionManager()));
}
Pce!

ADFS v2.0 Error : MSIS7042: The same client browser session has made '6' requests in the last '1' seconds

Folks,
I've got an ASP.NET MVC application that I am attempting to secure using the Release Candidate version of ADFS v2.0 (Geneva). I have configured the application as a relying party trust, and I've used Fedutil.exe to modify the application's Web.config so that it has the information about the Geneva server and uses the Geneva server as its claims source.
However, when I try and hit the MVC app, it redirects to Geneva, which then (after warning me about self-signed certs) re-directs me to the MVC app again. After accepting both self-signed cert warnings, the two servers play ping-pong with each other in an infinite redirect loop until finally Geneva spews the following message:
The same client browser session has made '6' requests in the last '1' seconds. There could be a possible bad configuration. Contact your administrator for details.
There are no errors in the event logs on the MVC side or on Geneva except for an event containing the above message. If someone could give me some information on how to try and debug, diagnose, and hopefully fix this problem, I would be eternally grateful.
Again, the Geneva box is the ADFS v2.0 Release Candidate and the ASP.NET MVC site was built using the latest (late '09) version of the Windows Identity Foundation SDK with Web.config modified using FedUtil.exe from the WIF SDK.
So you will all get a kick out of this... I tried this same application from Firefox and ... IT WORKS. I get prompted for my domain credentials, the ADFS v2 server re-directs me ONCE and then I end up on the home page of my application, complete with my account name and personalized greeting.
So now the real issue is this: Why the hell is IE8 getting caught in an infinite redirect loop and Firefox ISN'T ??
After even further testing, I've been able to get this scenario working, out of the box, without modification of any of the default pipeline stuff from ADFS v2 (RC) or from WIF (RTW) on BOTH Safari AND Firefox. IE8 is the ONLY browser to exhibit any problem dealing with this authentication scenario. I've tried everything, including installing and trusting the self-signed certs, adding the sites to my local intranet zone and dropping security to low and even setting first AND third party cookies to always allow.
I had the same issue with ADFS 1.0
And to resolve it, I made sure that the URL had a trailing forward slash "/" that would always work in FireFox as well as IE
eg : https://somedomain.com/Application_2/
Turns out that the host name of the relying party had an underscore in it (khoffman_2). Apparently, the underscore is an illegal DNS character and ONLY IE will reject the information with the underscore in it.
I renamed my machine from khoffman_2 to khoffman2 and the ADFS v2/MVC relying party combination works flawlessly on Firefox, Safari, AND IE.
While this isn't your problem, we have had identical problems to what you described. Our solution was to:
Enabled Basic Authentication in IIS (this solved nothing but was required for the next 2 steps)
Disable Windows Authentication in IIS (this solved the problem for some IE browsers but not all)
Disable Anonymous Access in IIS (this solved the problem for the rest of the IE browsers)
Jaxidian's answer is close.
In my case I only had to:
Windows Authentication -> Disabled
Anonymous Auth -> Enabled
ASP.NET Impersonation -> Disabled
Forms Auth -> Disabled
Windows Auth -> Disabled
This loop can occur when a user is not authorized to access a page.
We had a custom authorization attribute on our MVC controller that checks to see if the user was in a role based on the claims provided if the setting for UseADFS was true in the config files. I thought this setting was set to true and was confounded that I kept getting the adfs loop when accessing the page because I was in the groups that were authorized to access the page.
The key to troubleshooting was to make a web page that displayed my adfs claims without necessarily requiring authentication.
#if (User.Identity.IsAuthenticated)
{
<div>UserName: #User.Identity.Name;</div>
var claimsIdentity = User.Identity as System.Security.Claims.ClaimsIdentity;
<table>
#foreach (var claim in claimsIdentity.Claims)
{
<tr><td>#claim.Type</td><td>#claim.Value</td></tr>
}
</table>
}
I noticed that I was getting logged into ADFS, and my claims were getting set, so ADFS was working. The actual issue was my config file had UserADFS="true" instead of UseADFS="true" which basically caused my custom authorization code to return false on authorization. Therefore, the page kept forwarding me back to adfs to authenticate again.
Anyways, if a user does not have the correct claims to access the page, then this adfs login loop can occur, as well.
Also, if you wrote a custom authorize attribute be sure to check out the following link which describes how to prevent the loop.
Redirect loop with .Net MVC Authorize attribute with ADFS Claims
Custom HandleUnauthorizedRequest handler code for AuthorizeAttribute from that link:
protected override void HandleUnauthorizedRequest System.Web.Mvc.AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
//One Strategy:
//filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
//Another Strategy:
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(
new
{
controller = "u",
action = "LoginStatus",
errorMessage = "Error occurred during authorization or you do not have sufficient priviliges to view this page."
})
);
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}

Resources