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
- Claim - Statement about a user, with type, value and issuer. Eg "Name"="Martin".
- Claim may use standard typenames from OASIS/SAML (URIs like http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name ). See WIF ClaimTypes enum.
SAML1.1 names must be in the form "http://..." or "namespace/name" (exception ID4216)
- Claim may use standard typenames from OASIS/SAML (URIs like http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name ). See WIF ClaimTypes enum.
- Security Token - A signed list of claims.
- Typically is SAML (xml format with signature, defined by OASIS)
- You can use JWT (Json Web Token, pronounced "jot") (like OAuth). There's no public key, so you can't check the issuer thumbprint. See leastPrivilege
- Compare to a simple Kerberos ticket in AD.
- Note you may have multiple "name" claims, from one issuer and/or multiple issuers.
- Identity Provider (IdP) knows about the user. The user logs on to the IdP and gets a token.
- Active Directory Federation Service (ADFS) is one IdP. Others are Azure AD, Oracle Identity Manager, PingFederate ...
- In Windows Server 2012, you can add additional claims to the name/groupSids of a normal Windows Identity (active directory administrative center- Dynamic Acces Control - Claims Types)
- Security Token Service (STS) - the part of the IdP that generates security tokens.
- Relying Party (RP) - the application that uses claims (also: Service Provider)
- The Rp has a URI called a realm which the IdP must know.
- In .net the application uses WIF to define the claims it trusts, and process the claims.
- In OAuth this would be client.
- Passive means depending on browser-based (the browser itself just sees redirects, it has no knowledge of the underlying protocol). Active means directly using WS-* (i.e. WCF).
Tooling
- WIF (Windows Identity Framework) was a separate module in .net 4.0. It became part of the framework in .net 4.5
- WIF uses HttpModules. Since OWIN, there is Owin Middleware
- VS2012 had the Identity Access Tool which included a local STS.
- VS2013/VS2015 project create wizard has "Configure Authentication". wsFederation is "Organizational Accounts".
- There is no local STS but there is a Nuget EmbeddedSts (github docs)
- See also Identity Server (v2 or v3, not v4 as that is only OpenId) or ADFS or Azure AD
OWIN middleware
- Ensure the project is .net 4.5+
- Ensure the project uses Integrated appPool (not IIS6-compatible Classic). OWIN requires integrated mode.
- You must use SSL.
- Install-package Microsoft.Owin.Security.Cookies - cookie authentication + OWIN
- Install-package Microsoft.Owin.Security.WsFederation - Ws Federation
- Install-package Microsoft.Owin.Host.SystemWeb - allows IIS to call OWIN
- In web.config, add the following
<configuration>
<appSettings>
<add key="ida:ADFSMetadata" value="https://adfs/federationmetadata/2007-06/federationmetadata.xml" />
<add key="ida:Wtrealm" value="http://testingAdfs" />
</appSettings>
<system.web>
<authorization>
<deny users="?" />
</authorization>
</system.web>
<system.webServer>
<modules>
<remove name="FormsAuthentication" />
</modules>
</system.webServer>
</configuration> - Add New Item - "OWIN startup class" named "Startup.cs"
- Use the following code
using System.Configuration;
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.WsFederation;
using Owin;
[assembly: OwinStartup(typeof(MyWebsite.Startup))]
namespace MyWebsite
{
public class Startup
{
private static readonly string Realm = ConfigurationManager.AppSettings["ida:Wtrealm"];
private static readonly string AdfsMetadata = ConfigurationManager.AppSettings["ida:ADFSMetadata"];
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = Realm,
MetadataAddress = AdfsMetadata,
});
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
}
}
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.
{
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.
Relax that with system.identityModel.services/ federationConfiguration/ cookieHandler @requireSsl="false"
- Sessions have fixed expirations by default. Do a sliding expiration using the SessionSecurityTokenReceived event (the eventArgs has a ReissueCookie=true)
- Cookies are NOT WebFarm safe by default (use DPAPI).
To use machinekey, there is a built-in one:
system.identityModel/identityConfiguration/securityTokenHandlers/remove SessionSecurityTokenHandler and add MachineKeySessionSecurityTokenHandler - You can cache in server rather than in cookie: sessionToken.IsReferenceMode = true
By default caches in memory. To change it (AppFabric or Redis) derive from SessionSecurityTokenCache and add in identityConfiguration/caches/sessionSecurityTokenCache @type
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
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...
- User tries to contact application...
- ...which forwards to it's STS...
- ...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)
- ...user logs in to their home IdP which gives them a token...
- ...which passes to the resource STS, which knows the IdP is trusted so...
- ...it issues it's own token (transforming the external token)...
- ...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";
};