static void

WS-Federation

Support for WS-Federation is based on .net 4.5 Claims Security. Claims-based identity means an application (Relying Party, RP) uses a separate service (Security Token Service, STS / Identity Provider, IdP) for security.

See Claims Security for basics on ClaimsPrincipal and WS-Federation Config for application configuration.

Definitions

Although ws-Federation is based on SAML tokens, it is not the same as the SAML2.0 passive protocol (SAML2P), also built on SAML tokens. SAML protocols are an older, incompatible standard (and frankly more common outside the Microsoft world). There is an old CTP, but not official support for SAML2P in .Net, although ADFS can federate to SAML2P identity providers. There is a Danish open source project.

Tooling

OWIN middleware

Testing this locally on your machine won't work, because of the redirect to ADFS. The simplest possible fix is a switch for local development (#if DEBUG or if (ConfigurationManager.AppSettings["isLocalTesting"] == "1")) with this app.UseFakeLogon() extension.

public static class FakeExtensions
{
    private static readonly string AuthenticationType = "ApplicationCookie";

    public static void UseFakeLogon(this IAppBuilder app)
    {
        const string fakeLoginPath = "/fakeLogin";
        app.UseCookieAuthentication(new CookieAuthenticationOptions()
        {
            AuthenticationType = AuthenticationType,
            LoginPath = new PathString(fakeLoginPath)
        });
        app.Use(async (context, next) =>
        {
            if (context.Request.Path.Value == fakeLoginPath)
            {
                context.Authentication.SignIn(CreateIdentity());
                context.Response.Redirect("/");
            }
            else
            {
                await next();
            }
        });
    }

    private static ClaimsIdentity CreateIdentity()
    {
        var identity = new ClaimsIdentity(new List<Claim>
                                          {
                                              new Claim(ClaimTypes.Name"tester"),
                                              new Claim(ClaimTypes.Role"Admin"),
                                          }, AuthenticationType);
        return identity;
    }
}

WIF HttpModules Pipeline

There are two ASP HttpModules required for WIF. See Owin Middleware which replaces this.

The WSFederationAuthenticationModule (FAM) reacts to unauthorized requests and forwards to the IdP STS. When the STS redirects back with a token, it creates the principal and claims.

The SessionAuthenticationModule (SAM) persists the principal and claims to a cookie, and rehydrates it on each application AuthorizeRequest.

By default SessionAuthentication only works over SSL.
Relax that with system.identityModel.services/ federationConfiguration/ cookieHandler @requireSsl="false"
The AntiForgeryTokens may fail when using wsfed, so put this in Application_Start
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
WIF events are broken. You can't use Application_Start, you have to use Init
IIS creates several application instances to serve requests, each with an Init. See here.

You can use global.asax reflection-based "events" like this one to dynamically change wreply so they go back to the page they requested:

void WSFederationAuthenticationModule_RedirectingToIdentityProvider(
    object sender, RedirectingToIdentityProviderEventArgs e)
{
    var ctx = HttpContext.Current;
    var raw = ctx.Request.RawUrl;
    if (raw != "/")
    {
        //return to current address
        e.SignInRequestMessage.Reply = ctx.Request.Url.ToString();
    }
}

Claims Transformation

The IdP claims may be useful for authentication, but may not be directly relevant to the application's authorization. You can transform the claims by deriving from ClaimsAuthenticationManager and overriding Authenticate.

public class ClaimsTransformer : ClaimsAuthenticationManager
{
    public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
    {
        //not authenticated- skip it
        if (!incomingPrincipal.Identity.IsAuthenticated)
            return incomingPrincipal;
 
        //validation
        if (!incomingPrincipal.HasClaim(ClaimTypes.Country, "USA"))
        {
            throw new SecurityException("Only Americans allowed");
        }
 
        if (incomingPrincipal.HasClaim("Transformed", "1"))
            return incomingPrincipal;
 
        //transformation of claims
        var claimsList = new List<Claim>
                    {
                        new Claim(ClaimTypes.Name, incomingPrincipal.Identity.Name),
                        //database look up?
                        new Claim(ClaimTypes.Role, "American"),
                        new Claim(ClaimTypes.Country, "USA"),
                        //transformed marker
                        new Claim("Transformed", "1"),
                    };
 
        var cId = new ClaimsIdentity(claimsList, "Application");
        var claimsPrincipal = new ClaimsPrincipal(cId);
 
        //save it in a token (chunked into cookies)
        var sessionToken = new SessionSecurityToken(claimsPrincipal);
        //to store in claims in server-side memory and just issue a cookie key...
        //sessionToken.IsReferenceMode = true; //liable to apdomain recycles, not webfarm friendly!!
        FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionToken);
 
        return claimsPrincipal;
    }
}

You can hook this up is Application_PostAuthenticateRequest or just in login action

protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
    var principal = ClaimsPrincipal.Current;
    //if it's registered in web.config, you can grab it here
    var claimsAuthenticationManager = FederatedAuthentication.FederationConfiguration.
        IdentityConfiguration.ClaimsAuthenticationManager;
    //otherwise just new it up
    claimsAuthenticationManager = new ClaimsTransformer();
    //transform it
    var transformed = claimsAuthenticationManager.Authenticate(string.Empty, principal);
    //save it
    Thread.CurrentPrincipal = HttpContext.Current.User = transformed;
}

Sign-Out

For application-only sign-out, it's just like FormsAuthentication.SignOut():
FederatedAuthentication.WSFederationAuthenticationModule.SignOut();
To sign out of the STS and all RPs (i.e. "single sign-out") you need to redirect to the STS. The STS shows a signed out page (which calls the RPs with an html image with src= ?wa=wsignoutcleanup1.0 )

public ActionResult SignOut()
{
    var fam = FederatedAuthentication.WSFederationAuthenticationModule;
    if (fam != null)
    {
        //application sign out
        fam.SignOut();
 
        //sign out from the Identity Provider (and all the other Relying Parties)
        var signout = new SignOutRequestMessage(new Uri(fam.Issuer), fam.Realm);
        return new RedirectResult(signout.WriteQueryString());
    }
    FormsAuthentication.SignOut();
    return Redirect("~/");
}

Federated Sign-In (Resource STS)

An external user (who uses another IdP) may need to access your application. An application trusts a single STS. But the STS can trust other IdPs. So...

  1. User tries to contact application...
  2. ...which forwards to it's STS...
  3. ...which is a "resource STS" which shows a list of IdPs it trusts... (a.k.a "home realm discovery", the "log in with google, facebook" type page)
  4. ...user logs in to their home IdP which gives them a token...
  5. ...which passes to the resource STS, which knows the IdP is trusted so...
  6. ...it issues it's own token (transforming the external token)...
  7. ...which the user passes on to the application, and they are logged in.

The external to local token transformation is automatic, but the picking the "home realm" is a little awkward.

The application/RP can tell the STS which external IdP the user is by passing the whr="extIdp" querystring (you may know by giving a start page eg /entry/external). You can change it dynamically in the RedirectingToIdentityProvider event:

FederatedAuthentication.WSFederationAuthenticationModule
    .RedirectingToIdentityProvider += (sender, e) =>
{
    e.SignInRequestMessage.HomeRealm = "external";
};