ASP.NET Core JWT Authentication and role-based authorization

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
an essential aspect of a solid risk API is making sure that these resources are protected in such a way that only properly authorized users and clients can get access to them this has traditionally been quite challenging in the Asia Network world but thanks to the latest Innovations in the platform protecting your web API could not be easier so today I'm going to show you how to protect your asp.net core web API in just a few steps let's start there are many ways to authenticate users so that they can access your risk API however the ideal and modern approach is called token-based authentication and it works like this when a user wants to get access to the rest API resources via his client let's say a browser the client first requests authorization from what is known as an authorization server which usually lives somewhere in the cloud the authorization server presents a login form where the user enters his or her username and password once the user submits the form the authorization server verifies the credentials and then it generates an encoded access token and sends it back to the client this token includes information about how to verify it is a valid token where it came from where it can be used who is authorized by the token meaning the person that successfully logged in what can be done with the token and much more the client can then use this token to try to access your rest API if the API can verify that this token is valid and presents a required authorization it will allow it perform the requested operation and return a successful response now how do you protect an expanded core web API so that it only allows requests that include valid tokens and how do you read information included in these encoded tokens also is there any way we can generate the tokens for local development without having to involve an entire authorization server well let's jump into the code and let's learn how to do all of this to demonstrate how to protect an asp and netcore web API I'm going to create a very simple fictional API that will provide access to a series of games purchased by video game players now what I'm going to show you here uses very recent features that became available with net 7. so to follow along you're going to need at least a.net 7 SDK in your box and in fact I can show you quickly the version that I'm using here so I'll just do.net version and yeah as you can see the version I'm using today is version 70102 all right let me clean this and so let's start by creating our web API project so I'm just going to say.net new web and the name that we're going to be using is games API all right so it's going to create our project and what I'm going to do now is just uh switch my vs code instance into that new directory so I'll just do code and then I'll go into games API and then I'll do Dash R so that I can reuse this same code instance hit enter and here we are now in the context of our games API project so you're going to get this prompt to add a few missing files and say yes that's going to help us to be able to build and debug our project vs code and um if you go into parent CS you're going to see that this is a super super simple API at this point in fact the only thing that it does is just show hello world and I can show you how that works I'll do Ctrl J to open my terminal I'll just going to do donut run okay so let's see what we get okay so the app is running import 15 26 I'm going to copy that location I'm going to open my browser over here and I'm going to paste that as you can see all you get is yours are very simple hello world over there okay but I'm going to close this and that's not really what we want to do here what we're going to do is just like I said uh introduce a couple of operations into the resource API so that users can access a list of purchase games so the first thing we're going to do is to introduce and let me collapses for a moment is to introduce adding memory map of the list of games that each of our users has purchased okay so this we're going to be creating just a very simple dictionary so I'm going to type here dictionary of string and list of string let's name it Games map and so yeah it's going to be new and so let's define the contents of of this dictionary right here so each element is going to have the key is going to be the name of the player so let's say this is player one and then the value is going to be a new list of type string and let's define a few elements for this list so here we just have to figure out like a few names of games so let's say I'll just call for with some of my favorites so let's say strict fighter Street Fighter 2. Minecraft all right and so yeah let's say that's for player one and I'm going to actually close these right here and so let's say we have a second player it's going to be player two and so this guy is going to have a couple of other games force uh Horizon 5. we also have Final Fantasy 14 I'm using a comma here so let me add that comma and lastly this is going to be FIFA 23 all right so there you go so with this map it is kind of a fiction representation of the fact that player one has purchased these two games Empire 2 has purchased these three games so now I want to do is to just set up an endpoint that can retrieve all of the games purchases by all users so I'm going to take advantage of this endpoint that has a raving defined here which by the way this uses what was called the minimal API framework it's a innovation of that showed up with the net six I think and so it's a very nice simple way to declare your apis so what I'm going to say here is that whenever somebody goes into the player a games endpoint here we're going to be returning not hello world but instead we're going to be returning our Games map so it's just that simple so let's try it out I'm going to do Ctrl J and I'm going to do.net run once again okay and so yeah I mean at this point we could go back into the browser but it will be better to actually use something more more interesting that lets us keep working within vs code without having to switch apps so for that what I'm going to be using is a one extension that let me show you here in extensions if you go to the extensions view look for this one called rest client here so I have it installed already if you have if you don't have it just go ahead and install it it's a very nice extension that allows you to do a bunch of very cool stuff within BS code with the rest apis okay so let me close that and what I'm going to do in fact is to create a brand new file at the root of my directory here so I'm going to say new file this file is going to be named games.http okay and so this is the file that we're going to be declaring all of the requests that we're going to be making into the API and so to declare request all you have to do is just put the verb in this case get and then what's going to be the location that you want to invoke in our case we know that that's going to be HTTP localhost 5026 it's like that but we also defined that our API is in the player games endpoints I'm going to copy that over here play your games and just like that we can go ahead and click Send send request and then collaps this as you can see on the right side we have the results of our request it was this was a successful request and we have our Games map down here okay so that is great I think that serves very well for us to start introducing a the required authorization element to the application because of course we don't want uh really everybody to just come in here and use our API to get the list of all the games purchased by all of our players right and so we want to somehow protect this API here so how to do that let me close this and let me close that and probably I'm going to just uh also stop stop my server there so close this so how do we introduce a authorization into this API it's actually very very straightforward the first thing you want to do is actually to set up your endpoint so that it requires authorization right so people cannot just go ahead and access that endpoint freely so to do that all you have to do is just and I'm going to put this in another line here it's just say dot require authorization so by doing this uh people is going to be required to present a corresponding access token to be able to access the API however before you can use this you also have to enable the required authorization services in your application so for that you can take advantage of this Builder object so you can say Builder and then this as you may know this includes all the series of services that have been registered for the application right and so if you go into services and then you go into add authorization right that's going to go ahead and use the you know the dependency injection mechanism to register all the services that are required by authorization and that will enable this require authorization a method over here now if you try things just like this let me show you what's going to happen so I'm going to open my terminal I'm going to do donut run once again okay and if we go back into our games.http here and then click on send request what you're going to get is actually a bit of an error as you can see that uh it's complaining because it is saying that it cannot find required I authorization authentication Service for the application right and this this makes sense because before we can do the the authorization element let me stop my terminal here and close this and close that before we can do any sort of authorization we have to have a way to authenticate and and decode that access token that's going to come in into the application so for that we're going to be needing authentic authentication Services right and for that we're going to be needing just one more a new nougat package so let me open my terminal here I'm going to clean this the package I want to install here is the net and package Microsoft dot asp.net core dot authentication Dot dot better right now hit enter okay so if we close our terminal now and go into our games API CS block we're going to notice that we have that brand new dependency over here and by the way you'll hear me say either jot or JWT across this across this video so there's just two ways to mention the same thing JWT really means Json web token but it's commonly pronounced as jot so now that we have that I'm going to go ahead and close this project file and we are now able to add the required authentication services so I'm going to do that just on top of this initial authorization call I'm going to say Builder dot Services dot at Authentication and then I'll say at job better so this is going to add the services for a what is known as better authentication which works in a way that aspin core is going to extract the access token that is also known as a better token from the incoming requests and it's going to validate that it is indeed a valid token right and later on that's going to translate into taking all of them decoded information from the token that is later going to be handed over into the authorization middleware so that we can figure out what exactly is coming in in that token so let's see how this works so with that in place I'm going to do Ctrl J and I'm going to go ahead and run my application once again okay so it's running and I'm going to collapse this and then back to getting install HTTP I'm going to hit send request and in fact notice that our request is now on authorized right which is expectation and this is good this means that nobody can now just access the endpoint without presenting the relevant access token so that is required now right so that's good uh but then yeah comes the question so how do now I come up with that a required access token and the traditional way in the past would be to of course figure out a way to get access to an authorization server so that that server can generate an access token with all the required configurations so that we can use it in our race API and you still have to do that for the production setting right and it's a bit of a challenging task but for a local development environment there's a much easier way that is enabled with the recent development in the net world so let me show you this I'm going to close this and I'm going to open up my terminal here so I'll do Ctrl J and I'm going to just Ctrl C to stop my server and what I like to do is actually to do a split a terminal with this button here the terminals we can see better and on the right side what I'm going to do is use this brand new tool that's is called.net user jwts okay so this is a tool that you can use in your box to generate tokens without requiring any sort of authorization server so let me show you how it works so to create a very basic token what you can do is just say the.net user.jwts and then create so hit enter and that's going to go ahead and as you can see it produces a token right away right is right here and so I suspected this is is all encoded right and you may be wondering well what what is really behind that token now that's something that I think is good to understand and so to understand what is behind this token there's a a couple of ways a one way that you can do it is by just using the tool itself because as you can see there is a a an ID right here for generator token and so using that ID but you can just copy that that ID and you can say let me claim this dotnet user jwts print and then the ID hit enter and that is going to give you all the information about that generate token right and so but the other way but it's going to be more traditional and but it's actually very good to understand is by using a another a another page that can decode this token and give you more information about everything that's going on there and so let me just copy the token here copy that and I'm going to go into this page that is called jwtms and there's there's a bunch of pages like this by the way this is just one but what we can do here is just paste a token and as you can see on the bottom you get a decoded version of the token right away okay now uh recorded this token notice that it is made of three parts right so this red part is the what is known as the header the second part is the payload and the last part is the signature now what you see in each of these parts and I'm not going to go into a lot of details about about this here but just so that so that you know a few details about this uh the first part like I said is the header and that defines the algorithm that was used to compute or to encode a token itself right and it also includes the type which is uh this web token right and so but then in the body which is the most interesting part is the actual set of claims that are included as part of this generated token so whoever produces this token included all of this information on it and that includes things like the for instance the sub which is who was authorized by by this token like in this case it's just it's Julio because that's that's the name of me in this machine and we use it the the the the little tool to generate in my box but it will include something different if it was a production environment another important part is going to be the audience this is set here which includes all of the URLs of the services that are the intended that audience for the token so who is expected to receive this token or who do we generate this token for now where did these did this all of these URLs came from that actually came from if you see ABS code and if we go into Explorer and you go into launch settings.json let me collapse this for a moment you're going to see that here's where the internet SDK template placed a bunch of URLs Associated to the project and here is you're going to find the for instance we're using HTTP now which is the default you're going to find that here's the URL that we're using for HTTP but if you wanted to we could also use https which is in this other URL over here and there's also URLs for IES or Express if you wanted to use that there's a URL over here and there's another Port over here so all of these URLs are the ones that are considered as valid audiences by the tool so when the token comes into the application uh it must include one of the the URLs that we are ss5 for our application then you also have a bunch of dates for instance like these two here which represents the range of dates on which the token is valid so it is it will not be valid before this date or after this date and yeah we're not going into lead us into how these days got computed it's a different way to represent the date uh but yeah we got those and we also have this one here it represents what exactly was the stock generated and lastly we have the issue here which represents the entity or the or the server authorization server that actually generated the tokens who generated this token now if you want to get more details about any of these claims what you can actually do is just go into claims over here and it's going to show a bunch of details uh more just so that you can know more about each of them but an important part if you go back into the coded token is that the signature that we have over here in green was computed from the combination of the information in the header and the payload here right so this was combined and we used the algorithm defined over here the hs256 and then that was used to compute the signature that's in green here and so that means that if anything in in this long string here is modified for any reason then the signature is not going to match and that turns it into an invalid token right so that's kind of a one way to prevent a modifications of the token along the way and that's why the signatory is super important now back into our project over here one more thing that you should notice is that a the tool did not just generate the token it also configured our service or our web API so that it can use that token and so if you close this and we go now into app settings.develop.json you're going to notice that we have this brand new section over here the authentication section so this defines a couple of important things the first thing is going to be the valid audiences right which defines all of the uh of the audiences that our service declares that are valid for the token so the token has to be generated for one of these four audiences which again if you remember going back to the page uh is is the list of audiences that are included in the token already right so those are going to match just fine and then it also includes a valid issue which is like I mentioned is who generated this token in this case it's just a little Tool uh but the issue in a more production setting will be the the address of whoever is the authorization server that generates this token right so again this has to match what comes in the token and in fact it does match air right now right the same easier okay so just keep in mind that both of these things are needed for the for the authorization to work properly right and luckily the tool is doing all of this for us so we don't have to worry about all those details now let's close this and then well let's see how do we actually use that token right so let's do Ctrl J and then here we're back into our screen here and so we have a token right here so I'm going to go ahead and copy this this full string so I'll copy that and uh what you want to do here is to add a header to your request right because right now it's just a very simple request it does include it doesn't include any headers but you need to include a new header that is known as the authorization header so you have to say authorization and then you have to specify what kind of of authorization you're going to be using in this case it is known as a better authentication right so that you can we can use the job tokens and then what you want to do is just paste that long string so I'm just going to go ahead and paste it over here it's very long uh but that is how you can specify a new header with the application in this engage.hdp using this risk line and if you're using you're using any other Tool uh for calling your rest API there's always always going to be a way to specify this additional header okay so with that in place let me go ahead and restart my server so I'm going to clean this I'm going to say dot net round so it uses the new configurations in app says that development.json and then let's try to send a request and see what we get so I click here and as you can see and let me collapse this this time we do have an uh 200 okay okay they request was validated because we presented a valid token right and we can see the full list of games down here so authorization is working as expected now this this looks great uh but of course I mean we can start thinking of other possible scenarios that are related kind of to authorization so for instance I think not everybody should be able to get the full list of all of the games that were purchased by all of the players right so this is more of an administrative task right somebody would have would have some sort of administrative client uh that wants to get a list of all of the of this information but this should not be available for every single user of the system so how can we enable something like role-based authentication so that let's say only administrators can see this information here so let's see how to do that let me close this and let me open my terminal I'm going to stop my server right there yeah it stopped to close this and let's go back to program CS so if you scroll down a little bit say down here remember that we use it this require authorization method to record authorization into a race API what we can do now is to expand this to use or to define a more specific policy that will include a new requirement and so what we can do is here just say policy and so this is going to be kind of a Lambda function there it's like that and so with this policy object we can do something else we can now say policy Dot and we can do a bunch of things and among them is going to be required role right so required role is going to be used to require that there has to be a role claim in the set of claims presented by the token and that role claim has to include whatever value we specify here so that value let's say is going to be just admin right and so just by doing this we are saying that uh yeah in the token there has to be an admin a role claim otherwise it will not be allowed and so just after maintaining that change let's do a control Ctrl J and let's go back into the terminal let me click this and let's run this once again now let's see what happens right so I'm going to collapse this and go back to against HTTP let's go ahead and run the request once again and see what happens well I expected this time we are getting a 403 for beating and that that means that uh the request was actually authenticated uh but it does not present the required claims so it cannot be authorized to go in right so and this is good and in fact if we go back once again into the terminal or actually into our page over here we'll see that there's there's really no Road claim right now right I suspect what we want to do is just to generate a brand new token that includes that role claim so that we can verify that we can that we can actually uh authorize a user based on the role so I'm going to clean this here I'm going to be running the same.net a user jwts create command but now we're going to append one more thing which is going to be the roll and that role is going to be just add me so I'll hit enter and this new token as you can see it does include the admin role as it mentions right there and but yeah just to make sure I'm going to copy copy the token I'm going to go back into our page over here and I'm going to replace that with this this brand new token and here you're going to notice that the road claim is included right there so now this token has been generated for uh with a for an administrator so if we now go back into Visual Studio code and we use that token over here so I'm going to delete this and I'll paste the brand new one there I'm going to send a request and as you can see once again the request got authorized we got a 200 okay and we got our AR games right so that is kind of how you can configure role-based authentication in your web API so now that is great and then let me actually go back here and let me close uh stop my server right there back here so let's say that now we want to introduce a brand new endpoint and the idea of this endpoint is that uh we can retrieve all of the list of games purchased by the user that that makes the call of this uh to this API right so the user is going to invoke an endpoint and it's going to receive back all of the games that belong to himself or to herself so once again so let's go down here we're going to say just add that map get and then let's give it another name so this is going to be named say my games okay and then once again we are going to need to have some land function here to tell to tell the program what to do when when a request comes in there now how can we figure out who is really calling a risk API right and so of course we could try to figure out okay so how am I going to decode uh this uh the incoming token because information is right there right so if we go back in here we can tell that here is Julio right Julio is the one that authenticated and that were assigned this token uh but do we need to be to to decode this this entire garage here to be able to get that information into our application here uh fortunately that's not the case and all we have to do is to receive one object here that is of type type claims principle right and I'll just call it user now to use this you're going to have to do I'm going to do control dot you need to import system.security.claims like that okay and so what we're doing here is just doing a little bit of dependence injection so that AC net core injects I mean after it decodes the token it creates this claims principle object and we can request it via dependency injection into our method over here right and then we can go ahead and and take advantage of it okay so that that takes us away of all that work of the code in the token that's been done for us so we can just use this and so with any place where we can say the following so the username it's going to be just user.identity identity that name okay so that represents the name or the sub that's included in the token now identity is a is a property that might be a no level here right so the the compiler here is just warning us hey identity might be new so you may be getting a new value here so be aware of that and so we want to make sure that that's that's not going to happen ever and so for that what we can say is just check this very quickly so I'm going to just say here is argument reception Al and then I'll say user.identity dot name all right so by doing this I'm saying that if identity is null or if name is null either of this is null you're going to just draw an exception here right so because we cannot move forward and so by doing that we know that if we reach this line here identity name must have some sort of a value so great so now we have the username what we want to do is user return the list of games for that user and since I mean luckily we have a very simple dictionary here so all we have to do is to match one of these keys to the user that has been provided so technically all we have to do here is just say Okay so we're going to say return Games map sub um username and so that will go ahead and retrieve the the value for for that specific key and that should be all we need to do uh however of course there's still the case that somebody that is not included in the map right somebody that does not player one or player two uh is going to invoke a race API so in that case I mean we can we can take a few options here and the option I'm going to go for is that if that happens I'm just going to return an empty and drizzle because that user is does not have any games and so to do that we're going to just say is the following if not against map contains contains key username we are going to return let's say just results dot empty okay and which is just an empty and empty result but of course I mean yeah the the compiler is complaining here because we're returning one type here and a very different type over here and that doesn't make sense you have written just one one consistent thing so what we can do now is just say instead of this we're going to say that this is going to return results dot okay and then in that okay we return the actual list of games okay so that makes it consistent because we are always returning an i result in this case all right and so yeah so with that we should be able to go ahead and retrieve the games for the user and then one more thing that we may want to do here is to also require I mean require authorization right so what I'm going to do is just copy the recorderization section from the previous endpoint and I'm going to paste that down here okay so that our brand new endpoint also requires authorization now for this one we are not actually going to require an administrator right so this should be open for all of our players so let's switch from admin into player here okay so whoever invokes this this endpoint has to include the player um role in the claims okay so now let's go back into our terminal let's see what we have to do so first the first thing is going to be that we're actually going to need a brand new token right because the admin token that we created is not going to be useful so on the right side I'm going to go ahead and run once again our tool and then I'm going to search that the role is going to be player okay so let's say let's see what happens when I use that token so and in order to use it well let's first start our server so let's start the server on the left side and then let's go into GameStop http and so let's add a brand new line and to separate things here what you want to do is just add three pounds like that and then you can go ahead and copy or add your other requests now our new request is actually called the endpoint is called my games so I'm going to put that over here my gains okay and the authorization better now we're going to we're going to paste our brand new token so I'll copy that paste it over here and we should be ready to invoke our request so I'm going to hit on send request and indeed this was a success I mean it did not reject the request as you can see but also we are not getting any games does that make sense well it's not going to make sense because if we look at our token and I'm going to bring it over here we look at our token and we need to clean this and paste a new token down here uh it indeed includes the player role but this was generated for Julio right sub is Julio and it turns to be that Julio is not included in the list of players in our dictionary right in Games map there's no users you'll only play one Empire too so what we want to do now is to generate a token specifically for one of these or 1062 right or just change one of these to Julio whatever makes sense but uh let's see what we can do in the tool to generate a token for a very specific uh user so I'm going to do Ctrl J and let's clean this and so it's going to the the line is going to be similar to the one before but what you want to add here is the name of the user so for that you all you have to do is just say dash n and then the name so in this case it's going to be let's say player one player one and then I'll hit enter and now as you can see this is a token that has been generated for player one okay and then yeah we can confirm that very quickly if I copy this copy this new token we'll paste that over here because we like this page so much later and as you can see yeah the sub is now player one all right so this token should evaluate for what we want to do now so let's go back into our games HTTP and let's delete this token and paste a brand new one right in a second request for my games I'm going to click on send request and as you can see now we got not just authorize it but we actually got the list of games that correspond to player one which are Street Fighter 2 and Minecraft and let's confirm that that is that is true we go here so yeah three Fighter 2 and Minecraft okay so that is how you can uh take advantage of the current user to return a different set of results from your race API now let's try one more thing so let's say that now uh we're moving into a model where our users can not just purchase games but they can actually subscribe into our service to get the full list of games right it's kind of a a subscription like the ones that you're going to see in any of the other major consoles right and so how to enable certain scenarios in this case we don't want us to save the games to a specific user we want to associate them to a subscription and somehow we want us to say that subscription to our users so the first thing we're going to do here and let me do open here let me make sure I just stop my server so yeah let's stop now uh is to introduce that brand new map of games right so what I'm going to do is just copy this map here copy this down here and this is going to be not against map let's call this one subscription map okay so this is going to include let's say two subscriptions one of them is going to be our uh silver subscription all right which is going to include uh just a couple of games again let's say that this the silver one is going to be a Street Fighter 2 and let's let's say yeah includes Minecraft uh but then we'll have our gold subscription it's going to include those two okay those two but also every other game guys so this includes right now our full catalog of games okay the call subscription so now that we have that how do we take advantage a of these right so what we can do is let's go down into our my games endpoint and what we can do now is to look for a very specific claim in the set of claims that were provided as part of a token so how to do that so if we go let's open up a little bit of space here and let's do this VAR has claim equals user has claim and then we're going to do is just check for the claim that is going to be a claim or a claim type is going to be one that we're going to be calling just subscription okay so this is a brand new claim it's a custom claim that is that only our system knows how to create and how to interpret and so we're going to say that if has claims so if the user is coming with a token that includes that claim we're going to go ahead and say let's let's figure out what is going what is your subscription right and we can do that by saying user Dot find first value so that will give us a value of the claim that we specified so as we said the claim is name subscription so I'm going to copy that over here and that should give us the value of the of the description that's included in the claim and then with that we can just say return results dot OK and that's going to be the subscription map with that specific subscription now once again we're getting kind of a compiler warning here saying hey you know what that's object that you have here might be new because find first value might not find the description and so what we're going to do in that case is just say okay well if that's the case let's do this of this no call this operation there and then we're going to say yeah let's just draw a new exception and we're going to say claim has no value all right I mean there are many ways to handle this of course this is just one way but with that as you can see we are going getting away from that warning and we can move ahead with our logic because there should always be uh that that claim over there so with that in place we should have our logic in place so that we will first try to find a subscription if we find that claim we return all the games from our subscription map for the required subscription and otherwise we refer to the previous logic right where we go ahead and return only the games that user has purchased it but now that takes us back into well how do we produce a token that includes this new claim right so that's something that we can actually also test via our little tool so let me open up my terminal here and on the right side I'm going to clean this and what you want to do is well let's go back to the line that we had before so.net usage I'm going to perhaps do this yeah uh create role and the role has to be player right uh the name is going to be player one but now we're going to also include claim and then here we're going to include subscription equals and that's going to have the name of the subscription so let's say we want to give full access so this is going to be called right so I'll hit enter and so that is going to include uh and produce a token that is for player one that includes the a that is for the player role and that also includes this custom claim name subscription which has a value of gold and once again we can also just copy this token check it out over here let's see what we get how does that look like here notice their subscription gold right Supply new claim all right and so yeah let's test that out I'm going to start my server once again.net run collapse this back to against HTTP and I'm going to replace this token with the brand new token and so let's hit send request and as you can see now this user because he belongs to the goal subscription he gets the full list of games as opposed to just the games that he had previously purchased so yeah that's how you can use claims to do what we call claims-based authorization right and and as you can imagine you can extend this mechanism and I'm going to just stop my server now stop that uh you can extend this mechanism to do a bunch of different kinds of validations and authorizations based on the many different claims that could be incoming in your access tokens I hope that was useful and if you'd like to know how to take it to the next level and protect your web API in a production environment please check out my site for complete courses where I covered that and many other topics that are essential for professional.ly can development consider liking this video if you found it useful and don't forget to subscribe to the channel so that you are the first to know whenever I publish new videos thanks for watching and I'll see you next time
Info
Channel: Julio Casal
Views: 8,595
Rating: undefined out of 5
Keywords:
Id: wVFfPrB5kEw
Channel Id: undefined
Length: 39min 17sec (2357 seconds)
Published: Tue Jan 31 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.