3.2 Secure Our Webhook
Currently, our webhook will process any request, regardless of its origin. An attacker could use that to interfere with our system or spoof data. We want to prevent that, and in this lesson I'll show you how to use GitHub's request signature functionality to do so.
1.Introduction1 lesson, 01:40
2.Get Started2 lessons, 11:54
2.1What You Need05:38
2.2How GitHub Webhooks Work06:16
3.Build the App4 lessons, 56:39
3.1Start the Project11:06
3.2Secure Our Webhook15:21
3.4Build a Custom Configuration12:59
4.Display Content3 lessons, 38:55
4.1Parse the Documents10:21
4.2Display a List of Posts14:54
5.Conclusion1 lesson, 01:10
3.2 Secure Our Webhook
In the previous lesson, we created our controller, and then we set it up so that it will automatically parse the request into something that we can programatically use. Now the problem here is that it's not secure. Anybody can send a request to our web hook. And it will process it. And that's definitely not what we want. We want to be able to ensure that the request coming in is from GitHub. And if not, then, we want to completely ignore it. But thankfully, we can do that because GitHub can generate a signature. Base upon the body of the request and then we can validate the request by generating our own signature and comparing that with what get hub generated. So, if you go to your git power repository or whatever you called that repository go to settings and then Webhooks and services. Now we didn't setup a Webhook but go ahead and click on add a Webhook. And then you're going to see this field for a secret value. This is a phrase that we could use that is then used to generate this signature. That we would then use for validation purposes. So, later on, whenever we set up this web hook, we are going to use a secret phrase. Then whenever we receive the request, we will generate our own hash. Compare that with what GitHub has sent us and then validate the request. Now unfortunately, this means that we can no longer rely upon asp.net's built in model binder. Because by the time the post method executes, the model binder has already done its work, and it has read the raw request stream. And once it does that, we can not read it again. I wish we could but we can't. So, we won't be able to use that feature instead we are going to have to read the raw request ourselves so that we can then generate our own signature and then we will need to parse that request into an object that we can then use. But all of that is very simple to do so the first thing we want to do is read the request body. So let's create var body = Request.Body; and this is the stream that we want to read. Now we could just write and extension method to make it easier to read a stream. So let's go ahead and do that. In the root of the project, let's create a new class. Let's call this StreamExt. Extensions and since this is going to be a class four extension methods it needs to be static and then our method is going to return a string, because that is what we will get whenever we read the stream. And let's just call this read to end a synch. Because we want do this stuff asynchronously. So that means you will need to say public static async and we also need to return a task of string. So we have read to end async, then the parameter is going to be a stream. We do need a using statement for system IO and we will just call the parameter, stream. And all we are going to do here is return a new stream reader. We will pass in the stream and then we will call the read to end a sync method. Although, we do need to await this, so return, await. New StreamReader ReadToEndAsync. That way we can go back to our controller and then we can call that method and that's nice and clean. So after we have the raw body. We want to parse that into our push payload object. So let's say if our payload equals and JSON converts, we need a using statement for Newton soft JSON. And then we are going to call the deserialized object method. The type parameter is our push payload and then, what we pass to the method is the body. That way we have our payload and then we can then do whatever it is that we need to do. Now, we could go ahead and test this if we wanted to so Let's create a variable and then let's set a breakpoint on that line. We'll go ahead and start running this. And we run into an error, so no. Let's see what the problem is and the problem is we did not, wait, that so we need to await, we also need to change our method at the signature to a-sync task. We don't want void anymore. And that way we will get around that problem. So, if we run this again then everything should work. And it is. So while that is going up we need to fire up Fiddler and we want to make a request. Now whenever you fire up Fiddler, and you have an ASP.NET 5 application running, you are going to see a lot of requests. And I think that this is from that Browser Link feature. So if you right-click an any one of these, choose filter, and then hide scriptedsandbox64, then those requests go away. So we want to make a request for our application. Let's get that URL or at least the base URL. And we also need api and then whatever name the controller is. And I don't remember what that is. That is PushWebhook. So we'll take that copy that there, this needs to be a post request, and we also need an actual request body. So let's go to the files that have our payloads, and we're not going to use the formatted Because that GitHub is actually going to send us is the minified version, and whenever they generate the signature, it is based on the minified. So that is what we want to use for our body and then we will execute that request. We see we hit that breakpoint. We have the raw body data. And as far as the Payload is concerned, it was properly parsed, it looks like. We have two commits one of those has one modified file and we could see that the time stamp was parched as well and the other commit has two modified files and the time stamp was parched there. So our code for reading the body and then parsing it into an object is working. And that's great. But now we need to validate that request. So let's stop debugging our application, and we are going to validate this. Now we could do all of this inside of the controller. But I would prefer not to. I like lean controllers. So instead I would rather have something like this. Validates or maybe even just a static method on a class so it could be get.hub validate and then call method like validate. We would need to pass in the signature that get hub sent us and that is in the form of a header. So we can say var header equals and then, request.headers, and that header is ["X-Hub-Signature"]. So, we could pass that header, and then, we could also pass the body. And then, that would tell us if the request was valid. And then, we can do whatever it is that we need to do. So, let's go ahead and write that code. Let's create a new folder and let's just call this validation and we are going to put that new class there. We'll call it GitHub validate and we will have a static method called validate. So public static Bool validate, let's say we had the header or I guess we could call this the signature, and then we also need the request body. Now the signature is a hash of the request body using that secret value. And what I have used is called ducks fly with geese. So if you want to use all of the request Payloads that I have included with the code download, then you'll need to use this secret. Otherwise feel free to use whatever you want. Now ideally, this secret value would be inside of our config file. And later on we will probably do that. But for now on we're going to keep this here. And we want to generate the hash of our body. So, let's write a method to do that. Private, this needs to be static as well and it's going to return a string, we'll just call it ComputeBody hash. We want two things. We need the secret, and we also need the body, so we'll have that as well. Now since we are going to generate hash values, we are going to have to convert these into byte arrays. So we will start with our secrets, we'll just call it secretByteArray. And we will use the encoding class. We need a using statement for system.text. The asking property and the get bytes method. We'll pass in the secret and we have that by array. So we'll do the same thing for the body We'll just copy and paste and make the necessary changes. So we have our two ByteArrays, now we just need to compute that hash. And we're going to do so using hmac. If you read the GitHub documentation, this is how they generate that hash, they use hmac. So we're going to new up HMACSHA1. We need a using statement for system security cryptography and we need to pass in that secret byte array to the constructor. And then once we have this HMAC we can compute the hash. So we will do that. We will pass in the bodyByteArray. And we need to assign that to a variable. So we'll say var hash equals the ComputeHash. Now this is a byte array as well. So we need to convert it into a string. But it's going to be a hexadecimal string. It's also going to have dashes and all of the letters are going to be in upper case. Now, the hash that GitHub is going to send us has no dashes, and all of the letters are in lower case. So we need to make those adjustments but first we need to convert this by the rate into a string, one of the faster ways to do that is with bit converter and then to string and we will pest in the hash. And then we need to modify this, by replacing all of the dashes with an empty string and then we want to lowercase everything. So we'll call the ToLower() method. And that will give us our hash. So let's go back to the validate method. Let's first of all check to see if we have anything within our signature. So we're going to use the IsNullOrWhiteSpace. We'll pass in signature, and if we don't have anything here, then we will just return false. Because if we don't have that signature, then there's nothing to validate against, meaning that the request didn't come from GitHub. So after this check we want to create our hash. So we'll say hash equals compute body hash we pass in our secret, we also pass in the request body. And then we compare the hash with the signature. So, if hash is equal to signature, then our request is validated. Otherwise, it's not. So we can go back to our controller. And we first of all need to add a using statement for our validation name space and we're going to use an if statement here. If not validate then we have an invalid request and it's a bad request. Really. So we will say Response.StatusCode = 400; and then we will return. Otherwise, we will just process. So let's add a to do, and process. So we can test this. Let's run this first. Let's also set a break point inside of the validate method. So let's say right here online 16. Whenever we check to see if we have a signature. So let's see if it's running, it is. We will send our request and we will hit that breakpoint. Now signature is null. So this should return false, and then we should have a response of 400 back to fiddler. So if we continue on then yes, that is what we have. So that works. If we don't have a signature, then the request is properly rejected. Now, let's also test an actual request. If you go back to the payload files, I have one called headers.txt and this is the file that contains that signature. So let's just copy that, we'll go back to Fiddler, and we will paste it into the headers. Now, let's first of all test an invalid signature, so let's just take off the last character. We will execute this again, and here we are back on line 16 inside of the validate method. But we now have a signature. So we are at least make it pass this first check. So now we compute the hash and we check to see if hash is equal to signature. This should be false. So we should still have a response of 400. And we do. So let's continue on. And there is the result for that request. So, now let's make this a valid request. Let's add that final character back to the xhub signature header. The execute the request. And let's step on through. So, we will compute the hash. We will compare the two hashes together. And that validated so we are no longer having a response of a bad request and we are moving on to processing the request. So now that we have secured our web hook we need to do the actual processing. We need to retrieve the files from GitHub, and then we need to make the necessary modifications to our local storage. And we will get started with that in the next lesson.