Pull SecurityToken from SAML Assertion - c#

I have the XML of a SAML Assertion that looks like this:
<saml:Assertion MajorVersion="1" MinorVersion="1" AssertionID="_9b6e6302-d6a8-47f0-9155-1051a05edbfb" Issuer="http://example.com/adfs/services/trust" IssueInstant="2013-04-29T19:35:51.197Z" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">
...
</saml:Assertion>
I am trying to get a SecurityToken out of this XML using code similar to the following:
// Loading the XML referenced above.
XDocument doc = XDocument.Load(new StringReader(assertion));
// Creating config to use in TokenHandlers below; required if not using a SecurityTokenHandlerCollection.
SecurityTokenHandlerConfiguration config = new SecurityTokenHandlerConfiguration();
config.AudienceRestriction.AllowedAudienceUris.Add(new Uri("https://localhost/Orchard/"));
config.CertificateValidator = X509CertificateValidator.None;
// Both of these lines throw Exceptions, as explained below.
new Saml11SecurityTokenHandler() { Configuration = config }.ReadToken(doc.CreateReader());
new Saml2SecurityTokenHandler() { Configuration = config }.ReadToken(doc.CreateReader());
If I try to read the token using the Saml11SecurityTokenHandler, I get the following Exception:
ID4075: SAML Assertion is missing the required 'MajorVersion' Attribute.
If I try to read the token using the Saml2SecurityTokenHandler, I get a different Exception:
Element 'Assertion' with namespace name 'urn:oasis:names:tc:SAML:2.0:assertion' was not found.
Obviously the one for Saml2SecurityTokenHandler makes sense, since this is a SAML 1.1 Assertion. However, why can't the SAML 1.1 TokenHandler read this Assertion?
EDIT: The reader appears to be empty; why is that? doc has content.
string notEmpty = doc.FirstNode.ToString();
string empty = doc.CreateReader().ReadOuterXml();

Drawing from the technique shown here, this works:
SecurityToken token;
using (StringReader sr = new StringReader(assertion))
{
using (XmlReader reader = XmlReader.Create(sr))
{
if (!reader.ReadToFollowing("saml:Assertion"))
{
throw new Exception("Assertion not found!");
}
SecurityTokenHandlerCollection collection = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
token = collection.ReadToken(reader.ReadSubtree());
}
}
Make sure you don't change of the whitespace in the XML document, otherwise you'll get a signature verification error.

Related

Unable to verify Forge callback payload signature

I am currently using Forge Webhooks API to handle different events that might occur on a project. Everything works fine, except the payload signature check.
The reason why I want to check the payload is because the callback will end up on my API and I want to reject all requests that do not come from Forge's webhook service.
Steps I followed:
Add (register) secret key (token) on Forge. API Reference
Trigger an event that will eventually call my API for handling it.
Validating signature header. Followed this tutorial.
PROBLEM!!! My computedSignature is different from the signature received from Forge.
My C# code looks like this:
private const string SHA_HASH = "sha1hash";
var secretKeyBytes = Encoding.UTF8.GetBytes(ForgeAuthConfiguration.AntiForgeryToken);
using var hmac = new HMACSHA1(secretKeyBytes);
var computedHash = hmac.ComputeHash(request.Body.ReadAsBytes());
var computedSignature = $"{SHA_HASH}={computedHash.Aggregate("", (s, e) => s + $"{e:x2}", s => s)}";
For one example, Forge's request has this signature header: sha1hash=303c4e7d2a94ccfa559560dc2421cee8496d2d83
My C# code computes this signature: sha1hash=3bb8d41c3c1cb6c9652745f5996b4e7f832ca8d5
The same AntiForgeryToken was sent to Forge at step 1
Ok, I thought my C# code is broken, then I tried this online HMAC generator and for the given input, result is: 3bb8d41c3c1cb6c9652745f5996b4e7f832ca8d5 (same as C#)
Ok, maybe the online generator is broken, I tried their own code in node js and this is the result:
I have 3 ways of encrypting the SAME body using the SAME key and I get the SAME result every time. BUT those results are DIFFERENT from the signature provided by Forge, resulting in failing the check and rejecting a valid request...
Does anyone know what is happening with that signature?
Why is it different from my result if I follow their tutorial?
How are you validating your requests?
The code below is working at my side. Could you give it a try if it helps?
[HttpPost]
[Route("api/forge/callback/webhookbysig")]
public async Task<IActionResult> WebhookCallbackBySig()
{
try
{
var encoding = Encoding.UTF8;
byte[] rawBody = null;
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
rawBody = encoding.GetBytes(reader.ReadToEnd());
}
var requestSignature = Request.Headers["x-adsk-signature"];
string myPrivateToken = Credentials.GetAppSetting("FORGE_WEBHOOK_PRIVATE_TOKEN");
var tokenBytes = encoding.GetBytes(myPrivateToken);
var hmacSha1 = new HMACSHA1(tokenBytes);
byte[] hashmessage = hmacSha1.ComputeHash(rawBody);
var calculatedSignature = "sha1hash=" + BitConverter.ToString(hashmessage).ToLower().Replace("-", "");
if (requestSignature.Equals(calculatedSignature))
{
System.Diagnostics.Debug.Write("Same!");
}
else
{
System.Diagnostics.Debug.Write("diff!");
}
}
catch(Exception ex)
{
}
// ALWAYS return ok (200)
return Ok();
}
If this does not help, please share with your webhook ID (better send email at forge.help#autodesk.com). We will ask engineer team to check it.

c# - validating signed XML

I have a problem with validating signed XML.
Maybe you can help me :)
I have an ASP.NET MVC service, which receives an XML and I need to validate if signature in this XML is valid.
Certificate I'm using for validation looks like this:
cert.crt file:
-----BEGIN CERTIFICATE-----
MIIDcjCCAlqgAwIBAgIFALVBJRQwDQYJKoZIhvcNAQEFBQAwaTELMAkGA1UEBhMCREUxDz ............
-----END CERTIFICATE-----
My code for signature validation:
var xmlDoc = new XmlDocument { PreserveWhitespace = true };
xmlDoc.LoadXml(samlXML);
var signedXml = new SignedXml(xmlDoc);
var certPath = HostingEnvironment.MapPath(#"~/App_Data/cert.crt");
var readAllBytes = File.ReadAllBytes(certPath);
X509Certificate2 certificate = new X509Certificate2(readAllBytes);
XmlNodeList signatureElement = xmlDoc.GetElementsByTagName("ds:Signature");
signedXml.LoadXml((XmlElement)signatureElement[0]);
var isValid = signedXml.CheckSignature(certificate, true);
XML is signed by :
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
This line
X509Certificate2 certificate = new X509Certificate2(readAllBytes);
Throws an error
Object was not found.
What am I doing wrong?
According to the docs the byte array must be either binary encoded (DER format) or Base64-encoded X.509 data. You have something else on your hands, which is why the constructor can't handle your data.
Check the docs for more information.

How to prevent model state binding from altering the InputRequest in .NET C#

I have a Web API (POST) which accepts the input JSON and does operation over it. Due to model state binding, the request by default getting bound to the request model.
We are facing a scenario where in the received JSON is not as per the expected format. Like we are having additional key-value pairs which we want to identify and notify about it. Due to model state binding I'm not able to find the additional parameters.
I have been trying the below code but I do not get the actual request. Is there a way to get the actual request rather than the overridden request.
public override void OnActionExecuting(HttpActionContext actionContext)
{
string uri = actionContext.Request.RequestUri.ToString();
uri = uri.Substring(uri.LastIndexOf('/') + 1).ToLower();
if(uri.Contains("xxx"))
{
PartnerLoginSchema reqSchema = new PartnerLoginSchema();
JsonSchema schema = JsonSchema.Parse(reqSchema.schemaJson);
var requestInput = actionContext.ActionArguments["requestx"];// receiving overriden request
string valid = JsonConvert.SerializeObject(requestInput);
JObject jsonObj= JObject.Parse(valid);
bool testcheck = person.IsValid(schema);
}
}
Eg: Expected JSON
{
req1: "asd",
req2: "wer"
}
Input JSON Received:
{
req1:"asdf",
req2:"werr",
req3:"unwanted" // this attribute is not required and has to be identified
}
I would want to find the req3 present in the JSON by some means.
Is there a way to achieve it in ASP.NET C#?
I'm able to achieve it by reading the input JSON from HttpContext.Current.Request.InputStream
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.MissingMemberHandling = MissingMemberHandling.Error;
string req_txt;
using (StreamReader reader = new StreamReader(HttpContext.Current.Request.InputStream))
{
req_txt = reader.ReadToEnd();
}
try
{
ExpectedJsonFormat s =
JsonConvert.DeserializeObject<ExpectedJsonFormat>(req_txt,
settings); // throws expection when over-posting occurs
}
catch (Exception ex)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, BadAndUnAuthorisedRequest("extra column"));
}

Log in to SimpleMembership app using external SAML identity provider

I was tasked with adding logging via external service (using SAML 2.0) to an MVC app (.Net 4.5) that uses SimpleMembership. To be honest I'm not even sure where to start. From what I found on the internet there are few points to the problem. Most of the materials I found dealt with communication with the SAML identity provider (frequently written from scratch). However before I can reach that point I need to make sure I can actually integrate it with the SimpleMembership which we are using.
I suspect for starters I would need something like SAMLWebSecurity (akin to OAuthWebSecurity which we also use). I have found no such thing* on the internet which makes me believe it does not exist (though I wouldn't mind being wrong here). This makes me believe I would have to write it myself, but can I do that without have to write my own membership provider?
*I'm not sure what would be a correct way to call this static class.
I'd recommend that you upgrade to ASP.NET Identity and the OWIN Based authentication middleware. Then you can use Kentor.AuthServices middleware that works with ASP.NET Identity (except that the XSRF-guard has to be commented out until bug #127 has been resolved).
You could also use the SAML classes from Kentor.AuthServices if you have to stick with SimpleMembership, so that you don't have to implement SAML from scratch.
Disclaimer: I'm the author of Kentor.AuthServices, but since it's open source, I'm not making money on people using it.
After discussing it with a colleague I think I figured out the course of actions. Both OAuthWebSecurity and WebSecurity appear to be a part of SimpleMembership, so what I wrote in the question would indicate I want to write a custom membership or reverse engineer SimpleMembership to copy OAuthWebSecurity (which doesn't sound like a fun activity to have).
My best bet here is hijacking the OAuthWebSecurity, by writing a custom client (one which implements the IAuthenticationClient interface). Normally one registers various OAuth clients using OAuthWebSecurity's built in methods (like RegisterFacebookClient). But it is also possible to register those clients using OAuthWebSecurity.RegisterClient which accepts IAuthenticationClient. This way I should be able to add this SAML login without writing a custom membership provider and keep using SimpleMembership.
I managed to do this. Thankfully the identity provider wasn't extremely complicated so all I had to do was redirect to a certain address (I didn't even need to request assertion). After a successful login, the IDP "redirects" the user using POST to my site with the base64 encoded SAMLResponse attached. So all I had to do was to parse and validate the response. I placed the code for this in my custom client (implementing IAuthenticationClient interface).
public class mySAMLClient : IAuthenticationClient
{
// I store the IDP certificate in App_Data
// This can by actually skipped. See VerifyAuthentication for more details
private static X509Certificate2 certificate = null;
private X509Certificate2 Certificate
{
get
{
if (certificate == null)
{
certificate = new X509Certificate2(Path.Combine(HttpContext.Current.ApplicationInstance.Server.MapPath("~/App_Data"), "idp.cer"));
}
return certificate;
}
}
private string providerName;
public string ProviderName
{
get
{
return providerName;
}
}
public mySAMLClient()
{
// This probably should be provided as a parameter for the constructor, but in my case this is enough
providerName = "mySAML";
}
public void RequestAuthentication(HttpContextBase context, Uri returnUrl)
{
// Normally you would need to request assertion here, but in my case redirecting to certain address was enough
context.Response.Redirect("IDP login address");
}
public AuthenticationResult VerifyAuthentication(HttpContextBase context)
{
// For one reason or another I had to redirect my SAML callback (POST) to my OAUTH callback (GET)
// Since I needed to retain the POST data, I temporarily copied it to session
var response = context.Session["SAMLResponse"].ToString();
context.Session.Remove("SAMLResponse");
if (response == null)
{
throw new Exception("Missing SAML response!");
}
// Decode the response
response = Encoding.UTF8.GetString(Convert.FromBase64String(response));
// Parse the response
var assertion = new XmlDocument { PreserveWhitespace = true };
assertion.LoadXml(response);
//Validating signature based on: http://stackoverflow.com/a/6139044
// adding namespaces
var ns = new XmlNamespaceManager(assertion.NameTable);
ns.AddNamespace("samlp", #"urn:oasis:names:tc:SAML:2.0:protocol");
ns.AddNamespace("saml", #"urn:oasis:names:tc:SAML:2.0:assertion");
ns.AddNamespace("ds", #"http://www.w3.org/2000/09/xmldsig#");
// extracting necessary nodes
var responseNode = assertion.SelectSingleNode("/samlp:Response", ns);
var assertionNode = responseNode.SelectSingleNode("saml:Assertion", ns);
var signNode = responseNode.SelectSingleNode("ds:Signature", ns);
// loading the signature node
var signedXml = new SignedXml(assertion.DocumentElement);
signedXml.LoadXml(signNode as XmlElement);
// You can extract the certificate from the response, but then you would have to check if the issuer is correct
// Here we only check if the signature is valid. Since I have a copy of the certificate, I know who the issuer is
// So if the signature is valid I then it was sent from the right place (probably).
//var certificateNode = signNode.SelectSingleNode(".//ds:X509Certificate", ns);
//var Certificate = new X509Certificate2(System.Text.Encoding.UTF8.GetBytes(certificateNode.InnerText));
// checking signature
bool isSigned = signedXml.CheckSignature(Certificate, true);
if (!isSigned)
{
throw new Exception("Certificate and signature mismatch!");
}
// If you extracted the signature, you would check the issuer here
// Here is the validation of the response
// Some of this might be unnecessary in your case, or might not be enough (especially if you plan to use SAML for more than just SSO)
var statusNode = responseNode.SelectSingleNode("samlp:Status/samlp:StatusCode", ns);
if (statusNode.Attributes["Value"].Value != "urn:oasis:names:tc:SAML:2.0:status:Success")
{
throw new Exception("Incorrect status code!");
}
var conditionsNode = assertionNode.SelectSingleNode("saml:Conditions", ns);
var audienceNode = conditionsNode.SelectSingleNode("//saml:Audience", ns);
if (audienceNode.InnerText != "Name of your app on the IDP")
{
throw new Exception("Incorrect audience!");
}
var startDate = XmlConvert.ToDateTime(conditionsNode.Attributes["NotBefore"].Value, XmlDateTimeSerializationMode.Utc);
var endDate = XmlConvert.ToDateTime(conditionsNode.Attributes["NotOnOrAfter"].Value, XmlDateTimeSerializationMode.Utc);
if (DateTime.UtcNow < startDate || DateTime.UtcNow > endDate)
{
throw new Exception("Conditions are not met!");
}
var fields = new Dictionary<string, string>();
var userId = assertionNode.SelectSingleNode("//saml:NameID", ns).InnerText;
var userName = assertionNode.SelectSingleNode("//saml:Attribute[#Name=\"urn:oid:1.2.840.113549.1.9.1\"]/saml:AttributeValue", ns).InnerText;
// you can also extract some of the other fields in similar fashion
var result = new AuthenticationResult(true, ProviderName, userId, userName, fields);
return result;
}
}
Then I just registered my client in App_Start\AuthConfig.cs using OAuthWebSecurity.RegisterClient and then I could reuse my existing external login code (which was originally made for OAUTH). For various reasons my SAML callback was a different action than my OAUTH callback. The code for this action was more or less this:
[AllowAnonymous]
public ActionResult Saml(string returnUrl)
{
Session["SAMLResponse"] = Request.Form["SAMLResponse"];
return Redirect(Url.Action("ExternalLoginCallback") + "?__provider__=mySAML");
}
Additionally OAuthWebSecurity.VerifyAuthentication didn't work with my client too well, so I had to conditionally run my own verification in the OAUTH callback.
AuthenticationResult result = null;
if (Request.QueryString["__provider__"] == "mySAML")
{
result = new mySAMLClient().VerifyAuthentication(HttpContext);
}
else
{
// use OAuthWebSecurity.VerifyAuthentication
}
This probably all looks very weird and might differ greatly in case of your IDP, but thanks to this I was able to reuse most of the existing code for handling external accounts.

Updating a document in Google Docs using the API?

I want to update the contents of the already uploaded Google Doc file.
I'm using the below code:
DocumentsService service = new DocumentsService("app-v1");
string auth = gLogin2();
service.SetAuthenticationToken(auth);
Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(
"CONTENTS PLEASE CHANGE"));
DocumentEntry entry = service.Update(new Uri("feedURL"), stream, "text/plain",
"nameOfDoc") as DocumentEntry;
For "feedURL" I tried using all the possible links: alternate, self, edit, edit-media even resumable-edit-media, but I keep on getting exceptions.
Also how do I read a response with such requests?
I just started using this API. Earlier, I was using it on the protocol level so was sending GET/POST requests and receiving web responses. I don't know how to get or read response in this case.
UPDATE:
Now the code I'm using is:
RequestSettings _settings;
string DocumentContentType = "text/html";
_settings = new RequestSettings("Stickies", "EMAIL", "PASSWORD");
var request = new DocumentsRequest(_settings);
//var entryToUpdate = doc.DocumentEntry;
var updatedContent = "new content..."; ;
var mediaUri = new Uri(string.Format(DocumentsListQuery.mediaUriTemplate, rid));
Trace.WriteLine(mediaUri);
var textStream = new MemoryStream(Encoding.UTF8.GetBytes(updatedContent));
var reqFactory = (GDataRequestFactory)request.Service.RequestFactory;
reqFactory.CustomHeaders.Add(string.Format("{0}: {1}", GDataRequestFactory.IfMatch, et));
var oldEtag = et;
DocumentEntry entry = request.Service.Update(mediaUri, textStream, DocumentContentType, title) as DocumentEntry;
Debug.WriteLine(string.Format("ETag changed while saving {0}: {1} -> {2}", title, oldEtag,et));
Trace.WriteLine("reached");
And the exception I'm getting is:
{"The remote server returned an error: (412) Precondition Failed."}
I'm getting this exception at DocumentEntry entry = request.Service.Update(mediaUri, textStream, DocumentContentType, title) as DocumentEntry;
Solved.. Exception precondition Failed was due to Etag mismatch
The above UPDATED code works perfectly for saving a document.

Categories

Resources