We are using Ping Federate to protect two web servers (both IIS and both protected using the IIS integration kit, or opentoken module from Ping). One server hosts a WEB API application and the other hosts a webpage. The Web API application has CORS enabled.
The webpage makes Ajax post requests with json data to the API server. This causes the browser to initiate a preflight options request. On the API server, the Ping module intercepts this request that does not contain credentials (the specs say that preflight options requests are not supposed to contain credentials) and returns a 302 redirect before the Web API code can handle it when it should return a 200.
My only current guess is to make a custom module that handles options requests and install it ahead of the opentoken module. Are there any other possible/better solutions?
Instead of waiting for PING I've put an IAuthorizationFilter on top of their .NET Integration Kit/Agent. The nice thing about a custom filter like this is you gain a lot more control over the security requirements of your API endpoints.
When writing the filter I used the following references:
https://msdn.microsoft.com/en-us/magazine/dn781361.aspx
using opentoken; using PF.SAML.Result; using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Filters;
namespace PF.SAML.Filters { public class PingAuthenticationAttribute : IAuthenticationFilter { public bool AllowMultiple { get { return false; } }
// http://www.asp.net/web-api/overview/security/authentication-filters
// https://msdn.microsoft.com/en-us/magazine/dn781361.aspx
public async Task AuthenticateAsync( HttpAuthenticationContext context, CancellationToken cancellationToken )
{
await Task.Run( () => {
/*
* Look for credentials in the request.
* If there are no credentials, do nothing and return (no-op).
* If there are credentials but the filter does not recognize the authentication scheme, do nothing and return (no-op). Another filter in the pipeline might understand the scheme.
* If there are credentials that the filter understands, try to authenticate them.
* If the credentials are bad, return 401 by setting context.ErrorResult.
* If the credentials are valid, create an IPrincipal and set context.Principal.
*/
var opentoken = context.Request.Headers.GetCookies()
.SelectMany( c => c.Cookies )
.Where( c => c.Name == "opentoken" )
.FirstOrDefault();
if( opentoken == null ) return;
var userInfo = getOpenToken( opentoken.Value );
if( userInfo == null ) {
context.ErrorResult = new AuthenticationFailureResult( "Invalid Token", context.Request );
return;
}
var claims = new List<Claim>();
foreach( var item in userInfo ) {
foreach( var value in userInfo[item.Key] ) {
claims.Add( new Claim( item.Key, value ) );
}
}
var id = new ClaimsIdentity( claims, "opentoken" );
var principle = new ClaimsPrincipal( new[] { id } );
context.Principal = principle;
} );
}
public async Task ChallengeAsync( HttpAuthenticationChallengeContext context, CancellationToken cancellationToken )
{
await Task.Run( () => {
var challenge = new AuthenticationHeaderValue( "SAML" );
context.Result = new AddChallengeOnUnauthorizedResult( challenge, context.Result );
} );
}
private MultiStringDictionary getOpenToken( string token )
{
MultiStringDictionary attributes = null;
Configuration.Agent agentConfig = (Configuration.Agent) ConfigurationManager.GetSection( "pfConfigurationGroup/agentConfiguration" );
AgentConfiguration config = new AgentConfiguration
{
CookieDomain = agentConfig.CookieDomain,
CookiePath = agentConfig.CookiePath,
NotBeforeTolerance = agentConfig.NotBeforeTolerance,
ObfuscatePassword = agentConfig.ObfuscatePassword,
RenewUntilLifetime = agentConfig.RenewUntilLifetime,
SecureCookie = agentConfig.SecureCookie,
SessionCookie = agentConfig.SessionCookie,
TokenLifetime = agentConfig.TokenLifetime,
TokenName = agentConfig.TokenName,
UseCookie = agentConfig.UseCookie,
UseSunJCE = agentConfig.UseSunJCE,
UseVerboseErrorMessages = agentConfig.UseVerboseErrorMessages
};
var str = ( config.ObfuscatePassword
? Encoding.UTF8.GetString( Obfuscator.Deobfuscate( agentConfig.Password ) )
: Encoding.ASCII.GetString( Convert.FromBase64String( agentConfig.Password ) ) );
config.SetPassword( str, Token.CipherSuite.AES_128_CBC );
// TODO: Check for token expiration
Agent agent = new Agent( config );
attributes = agent.ReadTokenMultiStringDictionary( token );
return attributes;
}
}
}