user2197446 user2197446 - 1 month ago 11
C# Question

How to Validate a token in a SOAP service when a secured REST endpoint already exists

I have a bit of a Frankenstien service in that it has endpoints for both SOAP and REST hosted on the same URL by the same code base. I'm using the client credentials grant flow to successfully secure the REST endpoints, but would like to use the same process to secure the SOAP calls. The startup.cs initializes the Identity server bearer token authentication like so:

JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();

app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["IdentityServerUrl"],
RequiredScopes = new[] { ConfigurationManager.AppSettings["IdentityServerScopes"] }
});


And for the REST endpoints I add an

[Authorize]


code decoration and everything works. For the SOAP side I repurposed the password field and have sent the token through that and can decode it like so:

string sPassword = request.Authentication.Password;
if (sPassword.Contains("."))
{
"\nAccess Token (decoded):".ConsoleGreen();

var parts = sPassword.Split('.');
var header = parts[0];
var claims = parts[1];

Console.WriteLine(JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(header))));
Console.WriteLine(JObject.Parse(Encoding.UTF8.GetString(Base64Url.Decode(claims))));
}


I can see the claims but this isn't validating the token. From here I've pieced together a ValidateToken method that throws exceptions about the Signature validation failed. Unable to resolve SecurityKeyIdentifier. I'm fairly certain that everything has been signed by the IdentityServer3 cert, but I'm stuck trying to create a cert. I don't have any certs in my KeyStore and would like a solution that doesn't require that I insert the cert in the KeyStore. Here is the attempt:

public static bool VerifyToken(string token)
{
const string thumbPrint = "6bf8e136eb36d4a56ea05c7ae4b9a45b63bf975d"; // correct thumbprint of certificate
var cert = X509CertificateHelper.FindByThumbprint(StoreName.My, StoreLocation.LocalMachine, thumbPrint).First();

var validationParameters = new TokenValidationParameters()
{
//IssuerSigningToken = new BinarySecretSecurityToken(_key),
IssuerSigningToken = new X509SecurityToken(cert),
ValidAudience = "https://securityeli.twcable.com/core/resources",
ValidIssuer = "https://securityeli.twcable.com/core",
ValidateLifetime = true,
ValidateAudience = true,
ValidateIssuer = true
//ValidateIssuerSigningKey = true
};

var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken validatedToken = null;
try
{
tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
//... manual validations return false if anything untoward is discovered
return validatedToken != null;
}

public class X509CertificateHelper
{
public static IEnumerable<X509Certificate2> FindByThumbprint(StoreName storeName, StoreLocation storeLocation, string thumbprint)
{
var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);

foreach (var certificate in certificates)
{
yield return certificate;
}

store.Close();
}
}


The current process doesn't work because I have no keys in my keystore. The BinarySecretSecurityToken failed because I don't know the key length?

I'm also going to come back to the REST side of the house, it validates the bearer token using the Authorize tag, so I should have access to the cert but have no idea how to get it out of the application. I can see in Startup it get passed IAPPBuilder app that I haven't been able to access.

Two questions are how to I create a cert to validate a token created in IdentityServer3 in C#? And can I retrieve that cert somehow?

Answer

After trying multiple paths I finally found something that works, I'm going to try and capture the relevant parts in case someone else is ever trying to do the same thing.

First I split the incoming token into it's parts:

    var parts = sPassword.Split('.');
    var header = parts[0];
    var claims = parts[1];

    var token = new JwtSecurityToken(sPassword);

I then setup some variables and called a custom VerifyToken method:

    CustomResponse customResponse = null;
    SecurityToken validatedToken = null;
    ClaimsPrincipal claimsPrincipal = null;

    if (VerifyToken(sPassword, ref customResponse, ref validatedToken, ref claimsPrincipal))
    {
        // Process SOAP request after authentication
    }
    else
        return customResponse; // token wasn't authenticated, and not authorized message was set in the VerifyToken method

The VerifyToken method looks like this:

    public static bool VerifyToken(string token, ref CustomResponse customResponse, ref SecurityToken validatedToken, ref ClaimsPrincipal claimsPrincipal)
    {
            // This was the biggest challenge in finding the cert that is used to validate the token
        var certString = "Found in the CallbackController.cs in the IdentityServer3.Samples repository"
        var cert = new X509Certificate2(Convert.FromBase64String(certString));

            // Setting what you'd like the authorization to validate.
        var validationParameters = new TokenValidationParameters()
        {
            IssuerSigningToken = new X509SecurityToken(cert),
            ValidAudience = ConfigurationManager.AppSettings["IdentityServerUrl"] + "/resources",
            ValidIssuer = ConfigurationManager.AppSettings["IdentityServerUrl"],
            ValidateLifetime = true,
            ValidateAudience = true,
            ValidateIssuer = true,
            ValidateIssuerSigningKey = true
        };

        var tokenHandler = new JwtSecurityTokenHandler();
        try
        {
            claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
        }
        catch (SecurityTokenValidationException e)
        {
            //HttpContext.Current.Response.StatusCode = 401;
            //statusCode = HttpStatusCode.Unauthorized;
            customResponse = new CustomResponse();

            customResponse.ServiceReturnStatus = new ServiceReturnStatus();
            customResponse.ServiceReturnStatus.ReturnCode = -401;
            customResponse.ServiceReturnStatus.ReturnMessage = "Unauthorized";
        }
        catch (Exception e)
        {
            //HttpContext.Current.Response.StatusCode = 403;
            //statusCode = HttpStatusCode.InternalServerError;
            customResponse = new CustomResponse();

            customResponse.ServiceReturnStatus = new ServiceReturnStatus();
            customResponse.ServiceReturnStatus.ReturnCode = -403;
            customResponse.ServiceReturnStatus.ReturnMessage = "Internal Server Error";
        }
        //... manual validations return false if anything untoward is discovered
        return validatedToken != null;
    }

    private string GetClaimFromPrincipal(ClaimsPrincipal principal, string claimType)
    {
        var uidClaim = principal != null && principal.Claims != null ? principal.Claims.FirstOrDefault(s => s.Type == claimType) : null;
        return uidClaim != null ? uidClaim.Value : null;
    }

I also added a GetClaimFromPrincipal that you can use to get claims out of the principal.

That's it, it doesn't look all that complicated, but it sure took me a lot of trial and error to get it to work. I'd still like an option that uses the Owin Startup information to validate/authorize the token because all I did was basically load all the information that I loaded in the Startup.cs like so:

    app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
    {
        Authority = ConfigurationManager.AppSettings["IdentityServerUrl"],
        RequiredScopes = new[] { ConfigurationManager.AppSettings["IdentityServerScopes"] }
    });
Comments