0

I am making an OWIN app to self host a website. I am having trouble with the authentication part. The basic flow I am looking for is:

  1. Check if the request has the correct cookie. If it has the correct cookie, then get the SecuritySessionToken and build the ClaimsIdentity, and then return the AuthenticationTicket.
  2. If the cookie isn't there, then let the STS know.
  3. Get the SAML token from the STS and build the claims & authentication ticket. (Even though I'm using WsFederation authentication, ADFS is not involved here.)

Step 1 is working fine for me. Steps 2 and 3 are where I'm having trouble.

This is what I have for my configuration method in the startup class:

public void Configuration(IAppBuilder app)
{
    CookieAuthenticationOptions options = new CookieAuthenticationOptions
    {
        AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
        CookieName = "MyCookieName",
        CookiePath = "/cookiePath",
        AuthenticationMode = AuthenticationMode.Active
    };
    // Not relevant how this module is made. It helps with
    SessionAuthenticationModule module = this.CreateModule();
    app.Use(typeof(MyCookieAuthenticationMiddleware), app, options, module);

    WsFederationConfiguration fedConfig = new WsFederationConfiguration();
    fedConfig.Issuer = "https://mySTS.com/NotRealUrl/";
    SecurityTokenHandlerCollection handlerCollection = new SecurityTokenHandlerCollection(new List<SecurityTokenHandler>() { new SamlSecurityTokenHandler() });
    WsFederationAuthenticationOptions wsFederationOptions = new WsFederationAuthenticationOptions
    {
        Configuration = fedConfig,
        Wtrealm = "http://localhost/MyApp/NotRealUrl",
        Wreply = "https://mySTS.com/NotRealUrl/Login",
        SecurityTokenHandlers = handlerCollection,
        AuthenticationMode = AuthenticationMode.Active,
        AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType
    };
    app.Use(typeof(MyWsFederationAuthenticationMiddleware), app, wsFederationOptions);
    app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
    app.Use<MyMiddleware>();
}

The part up to app.Use(typeof(MyCookieAuthenticationMiddleware), app, options, module); works fine. That is, when the cookie is there, I use my custom cookie authentication middleware (MyCookieAuthenticationMiddleware) to generate the AuthenticationTicket.

The part that doesn't work is the WsFederation authentication middleware part. I created my own middleware for this (MyWsFederationAuthenticationMiddleware) so that I could take a look at what's going on since doing app.UseWsFederationAuthentication(wsFederationOptions) directly wasn't working for me. I am looking at the source code for WsFederationAuthenticationHandler.cs to guide me, but I'm still confused about some overall concepts.

Questions

  1. I think I know what the general flow should be (what I numbered at the top), but I'm not sure how this translates exactly to the middleware pipeline. If I don't have a cookie, then I find this out in AuthenticateCoreAsync() in MyCookieAuthenticationHandler. But at what point do I redirect this to the STS to get the SAML token? Do I just wait for the pipeline to hit AuthenticateCoreAsync() in MyWsFederationAuthenticationHandler, and if the user is not authenticated, then get the token?
  2. How do I actually tell the pipeline that it needs to get the token from the STS? Looking again at WsFederationAuthenticationHandler.cs, it seems like their implementation of ApplyResponseChallengeAsync() might be doing what I want? More specifically, if it's a 401 status code then make a WsFederationMessage and redirect to the STS using that message?
  3. Assuming I can actually get the STS to return the token I want, where in the pipeline does it return to?

Thanks and sorry for the sort of long post.

Update 1

I forgot to mention, when I run my code using app.UseWsFederationAuthentication(wsFederationOptions) (instead of my own MyWsFederationAuthenticationMiddleware), I get back a 400 "bad request - request too long" error. The URL is very long and looks like it contains various query parameters, including wtrealm, wctx, wa, and wreply, each of which contains url encoded strings. Looks like wctx is the really long one. I imagine it's some base64 encoded object. Unfortunately I don't really know what's going on.

Drew
  • 1,277
  • 3
  • 21
  • 39
  • If you like to debug the flow it is not necessary to write an own Middleware(wrapper). The class WsFederationAuthenticationOptions provides the possibility to hook into the wsfed authentication. You can set the notifications property to a new instance of WsFederationAuthenticationNotifications. – Dennis K Oct 27 '17 at 19:53
  • Do you have also an username/password login with the cookie? Or only WsFed? – Dennis K Oct 27 '17 at 19:54
  • @DennisK Yep, there's a username/password login. I'm not sure what you're suggesting exactly with your first comment. Could you elaborate please? – Drew Oct 27 '17 at 21:18
  • 1
    You wrote: "I created my own middleware for this (MyWsFederationAuthenticationMiddleware) so that I could take a look at what's going on". I just want to mention that there is an other way for this in the WsFederationAuthenticationOptions.Notifications (see https://msdn.microsoft.com/en-us/library/microsoft.owin.security.wsfederation.wsfederationauthenticationnotifications(v=vs.113).aspx ) – Dennis K Oct 27 '17 at 21:34
  • Ah, I didn't realize that's what those notifications are for. Thank you, I'll try making use of that. – Drew Oct 27 '17 at 21:50

1 Answers1

1

I think, the problem is, that both Middlewares have the AuthenticationMode Active

You should change the CookieAuthenticationOptions.AuthenticationType to the CookieAuthenticationDefaults.AuthenticationType then set WsFederationAuthenticationOptions.AuthenticationMode to passive.

I recommend an custom controller. If the user visits this controller you must trigger the Authentication on the OwinContext.Authentication manually for the WsFederationAuthenticationDefaults.AuthenticationType and return an 401. That should trigger the ApplyResponseChallengeAsync in the WsFederationAuthenticationHandler

In the SecurityTokenValidated Method on the WsFederationAuthenticationOptions.Notifications you can issue a new AuthTicket with an identity of type CookieAuthenticationDefaults.AuthenticationType.

Now the identity from the identity provider is converted to a an local identity with cookieauth.

Dennis K
  • 477
  • 1
  • 3
  • 12
  • Thanks for this answer. This looks like the right approach, but I'm still confused. Instead of a controller I just have a class that inherits `OwinMiddleware`, with an override on the `Invoke(IOwinContext context)` method to handle requests. By the time this `Invoke` method is called, it has already gone through the authentication pipeline. So if the cookies were there then the user is authenticated and it's all good, but if not I set the response status code to 401. (cont.) – Drew Oct 28 '17 at 00:11
  • At this point, `ApplyResponseChallengeAsync` will supposedly be called for `WsFederationAuthenticationHandler` (or do I manually invoke it?). But I'm not sure what happens at that point. Do I just trust that it comes back and authenticates correctly? It's not really clear from the source code what the flow is. Also, wouldn't it then be too late to do what the request wanted? So I'd have to do all this before the `Invoke` method? – Drew Oct 28 '17 at 00:19
  • I tried doing what [the answer for this question](https://stackoverflow.com/questions/25663773/the-owin-authentication-pipeline-and-how-to-use-katana-middleware-correctly?rq=1) says, but the code in `AuthenticateAllRequests` didn't work for me. It seems like it's an implementation of what you're suggesting. – Drew Oct 28 '17 at 00:22
  • Have you tried the following? Login manually with `HttpContext.Current.GetOwinContext().Authentication.Challenge(WsFederationAuthenticationDefaults.AuthenticationType);` and then set the responsestatus to 401. `return new HttpUnauthorizedResult(); ` – Dennis K Oct 30 '17 at 20:06
  • I've worked on it a bit more and I'm getting closer, but still stuck. At this point I am able to redirect to the STS and get back the token. I wrote some code for `SecurityTokenReceived` and can see the claims are there. The problem I'm having is I can only set the `AuthenticationTicket` in `SecurityTokenValidated`, and so far I'm having trouble figuring out how to trigger it. – Drew Oct 30 '17 at 22:50
  • Are you sure that there is no exception? This behaviour is mostly when you receive the token but the validation of the token failed. – Dennis K Oct 31 '17 at 09:16
  • Oh you're right -- I realized AuthenticatedFailed is being called with the following exception: `{"ID4037: The key needed to verify the signature could not be resolved from the following security key identifier 'SecurityKeyIdentifier\r\n (\r\n IsReadOnly = False,\r\n Count = 1,\r\n Clause[0] = System.IdentityModel.Tokens.Saml2SecurityKeyIdentifierClause\r\n )\r\n'. Ensure that the SecurityTokenResolver is populated with the required key."}` I'll try looking into it more. – Drew Oct 31 '17 at 21:19