Federated authentication in ASP.NET MVC with Access Control Service

How to integrate a classic (MVC 5 and before) ASP.NET MVC application and a new type ASP.NET MVC (6?) OWin with an Azure Access Control Service (ACS). Users are authenticated outside of an application by third party authentication providers such as Facebook, Google, Yahoo etc. This process is called federated authentication.

A classic ASP.NET MVC project can be downloaded here https://github.com/mchudinov/AspMvcACSClassic

A new OWin-based ASP.NET MVC project can be downloaded here https://github.com/mchudinov/AspMvcACSOwin

acs

There are tons information in the Internet about how to setup and configure federation authentication with ASP.NET.

This MSDN article Federated Identity with Microsoft Azure Access Control Service. It covers the technology idea, terminology, protocols etc.

This blog post has a nice explanation of web.config options Windows Identity Foundation (WIF) Configuration Sections in ASP.NET Web.Config

Here are some articles how to implement federation authentication for ASP.NET MVC 5 application with OWin (using VS 2013 or 2015):

In a classic ASP.NET MVC project federated authentication is defined as module and configured in <system.webServer> section of Web.config file.

In a OWin-based solution federated authentication is enabled via a Configuration method of Startup class:

public partial class Startup
{
	public void Configuration(IAppBuilder app)
	{
		ConfigureAuth(app);
	}
	
	public void ConfigureAuth(IAppBuilder app)
	{
		app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
		app.UseCookieAuthentication(new CookieAuthenticationOptions());

		app.UseWsFederationAuthentication(
			new WsFederationAuthenticationOptions
			{
				Wtrealm = "xxx",
				MetadataAddress = "yyy"
			});
	}
}

I want to add authentication events capturing (for logging or whatever).

1. Capturing of ACS authentication events in classic ASP.NET MVC
ASP.NET uses to classes for federated authentication WSFederationAuthenticationModule and SessionAuthenticationModule. Authentication events can be captured in inherited classes:

sealed class CustomSessionAuthenticationModule : SessionAuthenticationModule
{
	public CustomSessionAuthenticationModule()
	{
		base.SessionSecurityTokenReceived += CustomAuthenticationModule_SessionSecurityTokenReceived;
		base.SessionSecurityTokenCreated += CustomAuthenticationModule_SessionSecurityTokenCreated;
		base.SignedOut += CustomAuthenticationModule_SignedOut;
		base.SigningOut += CustomAuthenticationModule_SigningOut;
		base.SignOutError += CustomAuthenticationModule_SignOutError;
	}

	private void CustomAuthenticationModule_SignOutError(object sender, ErrorEventArgs e)
	{
		var auth = (CustomSessionAuthenticationModule)sender;
		Debug.WriteLine("SignOutError. Message: " + e.Exception.Message);
	}

	private void CustomAuthenticationModule_SigningOut(object sender, SigningOutEventArgs e)
	{
		Debug.WriteLine("SigningOut"); 
	}

	private void CustomAuthenticationModule_SignedOut(object sender, EventArgs e)
	{
		Debug.WriteLine("SignedOut"); 
	}

	private void CustomAuthenticationModule_SessionSecurityTokenCreated(object sender, SessionSecurityTokenCreatedEventArgs e)
	{
		Debug.WriteLine("SessionSecurityTokenCreated. SessionSecurityToken: " + e.SessionToken.Id + " KeyExpirationTime:" + e.SessionToken.KeyExpirationTime); 
	}

	private void CustomAuthenticationModule_SessionSecurityTokenReceived(object sender, SessionSecurityTokenReceivedEventArgs e)
	{
		Debug.WriteLine("SessionSecurityTokenReceived. SessionSecurityToken:" + e.SessionToken.Id + " KeyExpirationTime:" + e.SessionToken.KeyExpirationTime); 
	}
}
sealed class CustomWSFederationAuthenticationModule : WSFederationAuthenticationModule
{
	public CustomWSFederationAuthenticationModule()
	{
		base.AuthorizationFailed += CustomAuthenticationModule_AuthorizationFailed;
		base.RedirectingToIdentityProvider += CustomAuthenticationModule_RedirectingToIdentityProvider;
		base.SecurityTokenReceived += CustomAuthenticationModule_SecurityTokenReceived;
		base.SecurityTokenValidated += CustomAuthenticationModule_SecurityTokenValidated;
		base.SessionSecurityTokenCreated += CustomAuthenticationModule_SessionSecurityTokenCreated;
		base.SignedIn += CustomAuthenticationModule_SignedIn;
		base.SignedOut += CustomAuthenticationModule_SignedOut;
		base.SignInError += CustomAuthenticationModule_SignInError;
		base.SigningOut += CustomAuthenticationModule_SigningOut;
		base.SignOutError += CustomAuthenticationModule_SignOutError;
	}

	private void CustomAuthenticationModule_SignOutError(object sender, ErrorEventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		Debug.WriteLine("SignOutError. Message: " + e.Exception.Message);
	}

	private void CustomAuthenticationModule_SigningOut(object sender, SigningOutEventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		Debug.WriteLine("SigningOut");
	}

	private void CustomAuthenticationModule_SignInError(object sender, ErrorEventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		Debug.WriteLine("SignInError. Message: " + e.Exception.Message);
	}

	private void CustomAuthenticationModule_SignedOut(object sender, EventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		Debug.WriteLine("SignedOut");
	}

	private void CustomAuthenticationModule_SignedIn(object sender, EventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		Debug.WriteLine("SignedIn");
	}

	private void CustomAuthenticationModule_SessionSecurityTokenCreated(object sender, SessionSecurityTokenCreatedEventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		var token = (System.IdentityModel.Tokens.SessionSecurityToken) e.SessionToken;
		Debug.WriteLine("SessionSecurityTokenCreated. TokenId:" + token.Id + " KeyExpirationTime:" + token.KeyExpirationTime);
	}

	private void CustomAuthenticationModule_SecurityTokenValidated(object sender, SecurityTokenValidatedEventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		Debug.WriteLine("SecurityTokenValidated");
	}

	private void CustomAuthenticationModule_RedirectingToIdentityProvider(object sender, RedirectingToIdentityProviderEventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		Debug.WriteLine("RedirectingToIdentityProvider. SignInRequestMessage:" + e.SignInRequestMessage);
	}

	private void CustomAuthenticationModule_AuthorizationFailed(object sender, AuthorizationFailedEventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule) sender;
		Debug.WriteLine("AuthorizationFailed. RedirectToIdentityProvider:" + e.RedirectToIdentityProvider);
	}

	public void CustomAuthenticationModule_SecurityTokenReceived(object sender, SecurityTokenReceivedEventArgs e)
	{
		var auth = (CustomWSFederationAuthenticationModule)sender;
		Debug.WriteLine("SecurityTokenReceived. SecurityToken:" + e.SecurityToken + " SignInContext:" + e.SignInContext);
	}
}

Now these new classes must be referred in Web.config as federation modules:

<system.webServer>
<validation validateIntegratedModeConfiguration="false" />    
<modules>
  <remove name="FormsAuthentication" />
  <add name="CustomWSFederationAuthenticationModule" type="AspMvcACSClassic.CustomWSFederationAuthenticationModule" preCondition="managedHandler" />
  <add name="CustomSessionAuthenticationModule" type="AspMvcACSClassic.CustomSessionAuthenticationModule" preCondition="managedHandler" />  
</modules>
</system.webServer>

2. Capturing of ACS authentication events in OWin ASP.NET MVC
To capture authentication events in OWin application WsFederationAuthenticationNotifications class should be used. What I need is just create a WsFederationAuthenticationNotifications object and assign delegates to it’s notification properties, then use this object in WsFederationAuthenticationOptions class instance. All modifications can be done in OWin Startup class in Startup.Auth.cs file that configures authentication.

public void ConfigureAuth(IAppBuilder app)
{
	app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
	app.UseCookieAuthentication(new CookieAuthenticationOptions());

	var notifications = new WsFederationAuthenticationNotifications
	{
		SecurityTokenReceived = (context) =>
		{
			Debug.WriteLine("SecurityTokenReceived");
			return Task.CompletedTask;
		},

		AuthenticationFailed = (context) =>
		{
			Debug.WriteLine("AuthenticationFailed");
			return Task.CompletedTask;
		},                
		
		MessageReceived = (context) =>
		{
			Debug.WriteLine("MessageReceived");
			return Task.CompletedTask;
		},

		RedirectToIdentityProvider = (context) =>
		{
			Debug.WriteLine("RedirectToIdentityProvider");
			return Task.CompletedTask;
		},

		SecurityTokenValidated = (context) =>
		{
			Debug.WriteLine("SecurityTokenValidated");
			return Task.CompletedTask;
		}
	};


	app.UseWsFederationAuthentication(
		new WsFederationAuthenticationOptions
		{
			Notifications = notifications,
			Wtrealm = _realm,
			MetadataAddress = _adfsMetadata
		});
}