Search code examples
asp.net-mvcauthenticationcookiesowinws-federation

Rejecting an otherwise successful ws-federation login in a CookieAuthenticationProvider()


I am working with an MVC5 application which is secured by the WsFederationAuthentication and CookieAuthentication OWIN components. As part of the login process, I inject some additional claims into the ClaimsIdentity returned from the SAML provider (ADFS in this case) during the OnResponseSignIn delegate of a CookieAuthenticationProvider supplied when I set CookieAuthenticationOptions during Startup. That allows me to manipulate the ClaimsIdentity in the OwinContext before it's used to generate the ticket. In practice, this works great for what I've needed to do in the past (mostly add custom claims depending upon values found in a database matched with data supplied from the incoming claim).

However, dependent upon circumstances that arise when OnResponseSignIn is executed, I now need to be able to reject the login in such a way that I can display an error to the user indicating why it's not possible for them to log in. This is proving to be much trickier than I had imagined.

These are the two main approaches I've tried:

  1. Throw an exception in CookieAuthenticationProvider.OnResponseSignIn:
    This works in that the authentication cookie is never set and my global error handler catches the exception and displays it to the user. However, I can't determine a way to effectively reject the identity before throwing the exception. So, my global error handler believes that the HttpContext is authenticated and renders the error view with elements that should not be rendered if the user is not authenticated. Nulling the OwinContext.Identity inside OnResponseSignIn does not have an effect as all I'm doing is clearing a value inside the OnResponseSignInContext which has no effect on the overall pipeline. So far as I'm aware, I can't mark the actual ClaimsIdentity itself as not authenticated as IsAuthenticated is set to true when you set the authentication type in the ClaimsIdentity ctor and can't be modified after that.

  2. Add an "error" claim in CookieAuthenticationProvider.OnResponseSignIn as a bread crumb and then look for it in CookieAuthenticationProvider.OnValidateIdentity and throw the exception there.
    This approach has the benefit of being able to clear the login (a benefit afforded by OnValidateIdentityContext.RejectIdentity()). So, now my error view renders correctly for an unauthenticated user. However, the problem here is that I can't figure out a way to prevent the authentication cookie created after OnResponseSignIn from being emitted to the client and set in the browser. The tools I have available in either of these delegates allow me to append and delete cookies from the OwinContext, but somehow this cookie is not one that I can do that with. It appears to be actually added to the response stream later in the pipeline where I can't change it. So, if I let OnResponseSignIn complete and then look for the cookie during the execution of OnResponseSignedIn, I don't see it there. Really not sure why that is -- looking at the source of CookieAuthenticationHandler, it looks as thought I should see it as it's added in between the execution of those two CookieAuthenticationProvider delegates. I also can't prevent the cookie from being generated in the first place as the CookieAuthenticationHandler will throw an exception if the ticket can't be serialized.

So, I either can get no cookie set and an as-if-authenticated error view or get an unauthenticated error view, but have a cookie set that effectively prevents the user from trying to sign in again.

Ultimately, I'm rolling around in the mud of the framework enough at this point that I feel like I must be approaching the problem incorrectly. What I'm looking for is an answer to this question:

How can I reject an otherwise valid WsFederation authentication postback and display an error to a user that explains what happened without emitting an AuthenticationTicket to the client?

I'm guessing there must be another point in the process (perhaps not in CookieAuthenticationProvider) where this can be done before the pipeline decides that authentication has succeeded.


Solution

  • Probably you use something like this in your Startup code:

    app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions())
    

    The class WsFederationAuthenticationOptions provides the possibility to hook into the wsfed authentication (like the CookieAuthenticationProvider for the cookie authentication). You can set the Notifications property to a new instance of WsFederationAuthenticationNotifications. I recommend to use the SecurityTokenValidated property and set it to your implementation i.e. a new method matching the required Func<> definition. This is the point after the token passed validation and a ClaimsIdentity has been generated. Then you can do your additional checks and set the AuthenticationTicket of the methods parameter SecurityTokenValidatedNotification to null and modify the HttpResponse. Than the CookieAuthenticationHandler should not be triggered.