4 Home
Claire edited this page 3 months ago

Code Notebook

Introduction

Years ago, my now-husband and I had an idea for a website, and in talking about our ideas, I decided to work on authentication using ASP.NET C#. At the time, the most popular .NET solution was a library called DotNetOpenAuth, which was abandoned by its creator right about the time I started working on my solution.

I'm not a fan of third-party dependencies. When I was working on this project in 2013, it quickly became evident that there simply weren't any comprehensive, actively maintained .NET libraries for implementing OAuth2-based authentication. As as result, I worked out a complete authentication solution using vanilla .NET assemblies. The basic concepts in this solution are still applicable today, but because I haven't worked on this code since 2013, I can't make any positive statements regarding its relevance in 2022.

If you want to understand how OAuth2 works in technical terms, this project lays everything out pretty clearly. The rest of this article is a copy of the code breakdown I wrote on my old WordPress site back in 2013.

Getting Started

Step-by-Step Authentication Process

First off, let's look at what exactly we need to do with our code to allow logging in to Google and, more importantly, verifying that the login information is valid and that the login session from Google hasn't been compromised or tampered with.

  1. Send the user to Google's login service via URL
  2. Process the GET data returned by Google
  3. Request user details from Google via POST
  4. Verify the returned user data is valid

That was easy!

Okay...so maybe it's not quite that simple. On the other hand, it's also not as scary as many developers believe it is. C# has excellent support for the RSA cryptography that Google uses to sign authentication data sent back to your application, which makes it surprisingly easy to handle the third step - verifying the data.

The first step is the easiest, so we're going to cover that to get started.

Sending the user to Google's login service

In order for your application to use Google's OAuth2 service, there's a few things you need to do first.

Before you do anything, set up a Google account for your application. You can use an existing Google account, but I opted to create an account using an email under my new project's domain in order to keep this project separate from my personal online identity. Once that's done, log in to Google Cloud's credentials console. Create a new OAuth client ID. Follow the instructions to create a new application - it's pretty straightforward, but if you need more information, check out Google's support article. You're going to need to note down two important pieces of information provided by the API Console: your client ID and your client secret. Your client id is a unique identifier that tells Google what API project is being used. This doesn't change. It's also publicly visible in the login URL that your user will see in their browser's address bar during the login process.

Your client secret, on the other hand, is just that - a secret. This is going to be sent to Google over an SSL-encrypted channel using POST. It's also all done on the server side, so as long as your server is secure, this piece of information is safe. The client secret is what tells Google that you are who you say you are and that your application has permission to use the API project you created through Google. If your client secret is ever compromised, you can easily generate a new secret through the API console.

The last thing you need to remember is what specific URIs you authorized as redirect URIs. Google's API will not allow authentication to any URI that isn't in this list. Fortunately, Google supports using localhost as a valid URI, so you can test your application locally without having to worry about having a domain or public web server set up. The URIs in this list need to be explicit - include any port number (IIS Express as part of Visual Studio Express for Web doesn't use port 80) and other information in your URI. For instance, a valid URI might look something like http://localhost:6250/login.aspx. The URIs in this list should be the pages that are actually processing the data Google returns once the user has authorized your application.

One thing to note: unless you need additional information or access to other APIs (like Gmail or Calendar), you should only ask for the user's email address. Users are much less likely to authorize applications that want to do anything with their accounts that isn't necessary.

Now that we've got the Google side of things created, we can actually create our login URL. This is really straightforward, and Google has fairly decent documentation on how the URL should be formatted. I'm not going to copypasta Google's documentation. What I want to cover here is the state querystring variable. Unlike the other arguments sent to the Google login service, the state value can literally be anything you want it to be. As our first step in verifying the login data hasn't been tampered with, we're going to use state to hold a randomly-generated session ID. Later on, when we validate the data Google returns, we can compare the returned session ID with the real session ID.

First, off, when the user loads the login page (or the main page, if the link to Google's login service is there), we need to see if a session ID for Google login is present. If not, we're going to create one. I opted to use C#'s built-in GUID class to accomplish this. My session variable is simply called state, for the sake of simplicity.

protected void Page_Load(object sender, EventArgs e)
{
	//when the page loads, a random session token needs to be created
	//this will be sent to Google to verify that the data hasn't been tampered with

	//create a random GUID as a token for preventing CSS attacks
	//this only happens if a session doesn't already exist
	if (Session["state"] == null)
	{
		Session["state"] = Guid.NewGuid();
	}
}

Whether you want to use an image, a form input button, or a straight text link, your URL needs to include this session ID. On the webpage, your URL will look something like this:

https://accounts.google.com/o/oauth2/auth?client_id=...&state=<%=Session["state"]%>&redirect_uri=...

When the user clicks this URL, it will direct them to either login to Google (if they're not already logged in) or authorize your application to use their credentials and information. As soon as the user tells Google it's okay for your application to use their information, they'll be taken back to whatever URI is in the redirect_uri section of the above URL. From there, we're going to do steps two and three - process the data and verify its integrity.

Logging In

Verifying the initial Google response

When the user clicks your login button or link and authorizes your application, Google will redirect the user back to your site. The redirect URL contains querystring data that can be used to verify the login session and access Google's APIs. The two querystring elements we are concerned with are state and code. When Google redirects back to your site, state will contain the value you passed through your original login link. If you followed my last piece in this series, this value will be the GUID generated when your login page originally loaded. This should still be present in your user's session in your application. The other value, code, is a one-time authorization code provided by Google. This is what you'll use to actually get your user's information from Google's authentication service. First, in the code for the login page (login.aspx in this example), I'm going to set all the variables for the request to be sent to Google's API.

string code = Request.QueryString["code"];
string client_id = "[...].apps.googleusercontent.com";
string client_secret = "[...]";
string redirect_uri = "http://localhost:61385/login.aspx";
string grant_type = "authorization_code";

Obviously, you'll need to set client_id and client_secret to the data in the Google Developer Console for your own application. Next, we need to verify that a code value actually exists in the URL and that the user isn't already logged in. I'm using a session variable called loggedin for this purpose.

if (code != null && Session["loggedin"] != "yes")
{
	//the state token in the return URL needs to be verified first.
	string gState = Request["state"];

	if (gState == Convert.ToString(Session["state"]))
	{
		string gurl = "code=" + code + "&client_id=" + client_id +
			"&client_secret=" + client_secret + "&redirect_uri=" + redirect_uri + "&grant_type=" + grant_type;
		Response.Write(POSTResult(gurl));

	}
	else
	{
		Response.Write("Either your session expired or someone is trying to do something bad. Either way, try again.");
	}
}
else if (Session["loggedin"] == "yes")
{
	Response.Write("Already logged in.");
}
else
{
	Response.Write("Not logged in.");
}

The gurl variable contains the data that will be passed back to Google via a POST request. I'm taking the code from what I created for testing purposes, so I'm using Response.Write to output the information Google passes back throughout the process. In the code above, this also checks to see if the user is logged in, or if the page being loaded doesn't contain a code (Google's authorization code). In those cases, we can just skip the whole login process entirely.

That state session ID is also validated, as the first step in ensuring the integrity of the data sent back from Google. The value of state in the querystring data Google returned needs to match the session ID generated when the page loaded. If it doesn't match, that can indicate that either the session in your application expired or the data passed back from Google was modified. This alone is not adequate for ensuring data integrity, however! It's just one of several steps.

The function that will handle all of the requests and validation is called gLogin().

A short review

To recap so far:

  1. Our application generates a unique session ID.
  2. The user clicks a login link or button.
  3. Our application directs the user to a Google page, including the session ID in the page URL.
  4. The user logs into Google with their credentials.
  5. The user authorizes our application to use their Google account information - namely, their email address.
  6. Google redirects the user back to our application, including the unique session ID and a one-time authorization code in our application's URL.
  7. Our application checks two things:
    • Does an access token exist in the URl querystring?
    • Is the user already logged in?
  8. If the user is not logged in and an access token exists in the URL querystring, we can proceed with requesting information from Google about the user.

We're not even close to being done yet - time to keep going!

Sending the POST request to Google

The authorization code Google provides allows your application to access basic information about the user. Your application uses an HTTPS POST request to access Google's APIs. This ensures the connection is encrypted. Additionally, this request is done server-side, so the user - or malicious third-parties - can't intercept the request and change it (or steal your user's information).

In the code above, we created a string of parameters to be sent to Google. This is passed to a function, gLogin, to be used in the request to Google. Let's look at what that function does.

Before we do anything, there are several libraries that need to be included in the application.

// reading and writing locally-cached certificates and reading data streams via HTTP
using System.IO;

// sending and receiving data from Google
using System.Net;

// cryptography libraries for digital signature verification
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

// encoding and decoding Base64 text
using System.Text;

// C#-native JSON support
using System.Web.Script.Serialization;

Now that we've included everything we need (along with the default libraries included in every new code behind page in an ASP.NET project), we can go ahead and get the user's information from Google.

The first part is pretty straightforward - pass arguments to Google's OAuth2 service using the one-time authorization code we've already obtained from Google. This is done using the HttpWebRequest class. For your reference, the MSDN documentation on this class is here. Google provides documentation on the format of the POST request, as well as details on the data that's returned, here.

public string POSTResult(string e)
{
	string responseData;

	try
	{
		// variables to store parameter values
		string url = "https://accounts.google.com/o/oauth2/token";

		// creates the post data for the POST request
		string postData = (e);

		// create the POST request
		HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url);
		webRequest.Method = "POST";
		webRequest.ContentType = "application/x-www-form-urlencoded";
		webRequest.ContentLength = postData.Length;

		// POST the data
		using (StreamWriter requestWriter2 = new StreamWriter(webRequest.GetRequestStream()))
		{
			requestWriter2.Write(postData);
		}

		//This actually does the request and gets the response back
		HttpWebResponse resp = (HttpWebResponse)webRequest.GetResponse();

		string googleAuth;

		using (StreamReader responseReader = new StreamReader(webRequest.GetResponse().GetResponseStream()))
		{
			//dumps the HTML from the response into a string variable
			googleAuth = responseReader.ReadToEnd();
		}

This request asks Google for the user's information, and Google returns a JSON array including two important bits - an access token and an ID token. If your application needs to access any of Google's APIs, you'll need the access token. However, if you're only using the user's Google account for login purposes, all you need is the ID token.

The ID token is a JWT, or JSON Web Token. There is extensive documentation on what a JWT is and how it's used. If you're the document-reading type, you can peruse the OpenID consortium's documentation on the subject. On the other hand, if you're just trying to make Google authentication work, you're in luck - I did all the research for you! Go me!

First, we're going to make a class to hold Google's JSON array. This will make it a bit easier to pull out the bits of data we need - namely, the ID token.

public class gLoginInfo
{
	public string access_token, token_type, id_token;
	public int expires_in;
}

We can use this class with the JavaScriptSerializer class that is part of .NET. MSDN, as always, has great documentation on how to use this class. We're going to use this class to create an instance of gLoginInfo containing Google's JSON response.

//process JSON array
JavaScriptSerializer js = new JavaScriptSerializer();
gLoginInfo gli = js.Deserialize<gLoginInfo>(googleAuth);

Now, all we actually care about is the id_token member of our gLoginInfo instance, called gli, since that's the JWT containing the logged-in user's information. Per the documentation, this is a period-delimited string containing three sections. The first two are Base64-encoded JSON arrays. The third is a SHA-256 hash of the first two segments, signed with Google's public certificate. That part is going to be the third article in this series.

The Split() string function will put this into an array so that we can decode each segment individually.

string[] tokenArray = gli.id_token.Split(new Char[] { '.' });

Decoding a Base64 string in C# actually results in a byte array rather than a plain string, even if the decoded data is a string. Because of this, the easiest thing to do is write a separate function that will take an inputting Base64 string, decode it, and convert the byte array into a string to use with the JavaScriptSerializer class.

This is a modified version of code I found in this forum thread. C# is really picky about Base64-encoded data, and the string passed to Convert.FromBase64String() needs to be a character length that is a whole number multiple of 4. The Base64 spec accommodates this by allowing an equals sign (=) to be used as padding at the end of an encoded string. With this requirement in mind, I added a bit of code before the actual decoding portion to add padding.

public string base64Decode(string data)
{
	//add padding with '=' to string to accommodate C# Base64 requirements
	int strlen = data.Length + (4 - (data.Length % 4));
	char pad = '=';
	string datapad;

	if (strlen == (data.Length + 4))
	{
		datapad = data;
	}
	else
	{
		datapad = data.PadRight(strlen, pad);
	}

	try
	{
		System.Text.UTF8Encoding encoder = new System.Text.UTF8Encoding();
		System.Text.Decoder utf8Decode = encoder.GetDecoder();

		// create byte array to store Base64 string
		byte[] todecode_byte = Convert.FromBase64String(datapad);
		int charCount = utf8Decode.GetCharCount(todecode_byte, 0, todecode_byte.Length);
		char[] decoded_char = new char[charCount];
		utf8Decode.GetChars(todecode_byte, 0, todecode_byte.Length, decoded_char, 0);
		string result = new String(decoded_char);
		return result;
	}
	catch (Exception e)
	{
		throw new Exception("Error in base64Decode: " + e.Message);
	}
}

What we're going to focus on right now is the second segment - the JWT payload, which is a JSON array. Google provides documentation on the elements in the payload and what they're used for. First thing we need is a class containing the elements in the decoded payload.

public class gLoginClaims
{
	public string aud, iss, email_verified, at_hash, azp, email, sub;
	public int exp, iat;
}

Now we can use our Base64 decoding function and our gLoginClaims class to decode the JWT payload.

//process payload
JavaScriptSerializer js2 = new JavaScriptSerializer();
gLoginClaims glc = js2.Deserialize<gLoginClaims>(base64Decode(tokenArray[1]));

At this point, you can use the decoded JSON array to add your user's information to your application's database, or to log in existing users. The values you want to focus on are sub and email. Because a user can change the email address associated with their Google account, it's not a reliable unique identifier for that user account. The sub value, per Google's documentation, is a unique identifier for that Google account.

This isn't a simple process, as you can see. However, once the process is clearly detailed, it's a lot easier to understand.

Validating Integrity

Another brief interlude

Here's the thing. Google makes it clear in their "Using OAuth2" guide that there are serious security implications with accessing a user's Google account information, and if you don't know what you're doing and don't take the appropriate security measures, you could accidentally make it really easy for anyone - including someone from 4chan - to pose as one of your users.

This isn't an exaggeration. When I was originally trying to figure out how to handle this whole OAuth2 beast for my project, every article I found about it either used DotNetOpenAuth or did nothing to verify the integrity of the user's data. The first step we took to mitigate this was to use a unique session ID that was passed to Google, passed back to our application, and checked against the ID in the user's web session.

There's something even more special we can do, though, and it uses fancy cryptography and certificates and stuff! Wow! If you want to go on this adventure with me, just click the jump. Otherwise, it might be easier if you leave now and judge the cuteness of others' kittens instead.

(If you went to Kittenwar anyhow, I understand. I won't judge you.)

The official IETF documentation on the JWT spec provides some insight into what the third segment of a token is actually for. This particular section is what we care about - verifying the JWT against a SHA-256 digital signature.

The steps are simple. The implementation? Not so much - but that doesn't mean we can't do it!

If you remember from part two, Google sends the user's information to our application in the form of a JWT, or JSON web token. One line is all you need to convert that line of plain text into an array that separates out each of the three segments of the JWT, where gli.id_token is the actual JWT string:

string[] tokenArray = gli.id_token.Split(new Char[] { '.' });

We've already dealt with the payloa. What we care about are the header and the signature, which are stored in tokenArray[0] and tokenArray[2].

Parsing the JWT Header

The header is a plaintext, Base64-encoded JSON array. I can't seem to find the page in Google's documentation that explains what the elements of this array are, but it's pretty easy, since there's only two: alg indicates what hashing algorithm was used for the signature in the third segment of the JWT, and kid gives us the ID of the Google public certificate that is paired with the private key Google used to sign the hash.

Just like how we handled the payload, we're going to make a class containing these elements...

public class gLoginHeader
{
	public string alg, kid;
}

...and then use the JavaScriptSerializer class to parse it.

//process header
JavaScriptSerializer js1 = new JavaScriptSerializer();
gLoginHeader glh = js1.Deserialize<gLoginHeader>(base64Decode(tokenArray[0]));

What we need is the second element, called kid. Google has already documented that they use SHA-256 to hash the data we're verifying, so we don't really need the first element.

Google provides an HTTP-accessible copy of the public certificates that are paired with the private keys used for signing. For security purposes, these certificates change about every 24 hours. For bandwidth purposes (and in order for our code to actually work), we want to cache a copy of each certificate on our own server and use that for the actual validation process.

Caching Google's public certificates

You can take a look at Google's public certificates here. As you can see, they too are stored in a JSON object. Unlike the header and payload that we've already dealt with, however, this array uses dynamic values for each element of the array, so we can't just make a class to reference when parsing this array. Instead, we need to use that kid value, which is the key in this JSON array. The value is the certificate itself.

I broke this out into a separate function that is given the kid for the purposes of caching the certificate.

First thing, we need to see if the certificate is already cached. To keep things really easy, I'm caching them in C:\certs and using the certificate ID itself as the file name.

public void cacheCertificate(string kid)
{
	//if the certificate ID doesn't already exist as a local certificate file, download it from Google
	if (!File.Exists(@"C:certs" + kid + ".cer"))
	{
		...
	}

Even though the certificate is really just plain text, it needs to use the .cer file extension, as you can see above. If the certificate already exists, we don't need to anything else, and the function is complete.

Of course, you probably want to see the rest of the code, so we'll assume the certificate hasn't been cached locally yet.

First, we'll pull the plaintext from Google's certificate URL using WebRequest and store it in a string variable called certs.

//pull JSON certificate data from Google
string url = "https://www.googleapis.com/oauth2/v1/certs";
WebRequest request = WebRequest.Create(url);
WebResponse response = request.GetResponse();
Stream certdata = response.GetResponseStream();

string certs;

using (StreamReader sr = new StreamReader(certdata))
{
	certs = sr.ReadToEnd();
}

Next, we'll use JavaScriptSerializer to serialize the text and pull out the certificate that matches the kid provided by Google when the user logged in. This is accomplished by using a Dictionary object, which can use dynamic key names. You can check out the MSDN documentation on this class here. We'll store the dictionary in a variable named cts, for "certificates". I know, I use super creative variable names...

//certs are returned as a JSON object
JavaScriptSerializer js = new JavaScriptSerializer();

//convert the JSON object into a dictionary
//pick the certificate that matches the returned key ID from Google
Dictionary<dynamic, dynamic> cts = js.Deserialize<Dictionary<dynamic, dynamic>>(certs);

Now that we have our dictionary, we can pull the certificate text we actually need, and write that text to a new file on the server.

string b64 = cts[kid];

//write the certificate to a file with the .cer extension, which identifies it as a digital certificate
//these are stored outside the web server filespace
System.IO.File.WriteAllText(@"C:certs" + kid + ".cer", b64);

One important thing to note here - Google uses two certificates each day, and rotates them out daily. At some point, you're going to want to clear out old cached certificates from your server. I'd recommend writing a script that runs every morning and deletes certificates that are more than 48 hours old, but that's another show.

For anyone concerned about bandwidth - this function only goes out to the Internet to download a certificate once. It should run, at the absolute most, twice every 24 hours.

Now that we have our certificate, we can take a look at the third segment of the JWT - the signature - and use the certificate and the signature to validate our data.

Validating the JWT digital signature

First, we're going to create a bool that returns true if the signature is valid or false if the validation fails. This function is going to take both the key ID and the original JWT provided by Google, and will use the verifySignature function we just created.

public bool verifySignature(string kid, string[] jwt)
{
	//this will return TRUE if the signature is valid or FALSE if it is invalid
	//if the signature is invalid, we must not accept the user's login information!

	//by default, the signature isn't valid, just as a precaution
	bool verified = false;

	//before we do anything else, we need to locally cache Google's public certificate, if it isn't already
	cacheCertificate(kid);

	//pull out the different elements from the original JWT provided by Google
	string toVerify = jwt[0] + "." + jwt[1];
	string signature = jwt[2];

Following the JWT spec, the toVerify string contains the first two segments of the JWT, concatenated with a period. The signature string is just the third segment as it is sent in the JWT.

Here's what a JWT signature looks like:

dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

This documentation explains what this gibberish actually is. I hate clicking external links as much as you do, so here's a summary:

  • The first two Base64-encoded segments of the JWT are concatenated with a period.
  • That string is then hashed (Google uses SHA-256).
  • The hash is digitally signed with the provider's (Google in this case) private key.
  • The hash is Base64-encoded an appended to the end of the first two segments, again concatenated with a period.

Seems straightforward enough, right? In order to actually use this signature to ensure that the first two segments of the JWT are valid and not corrupted, we just have to follow three steps. Three steps doesn't sound very scary.

  • Take the Encoded JWS Signature and Base64url decode it into a byte array. If decoding fails, the signed content MUST be rejected.
  • Submit the UTF-8 representation of the JWS Signing Input and the public key corresponding to the private key used by the signer to the RSASSA-PKCS1-V1_5-VERIFY algorithm using SHA-256 as the hash function.
  • If the validation fails, the signed content MUST be rejected.

I realize that step two makes absolutely no sense to anyone who doesn't work with cryptographic algorithms for a living. Fortunately, my hsuband does exactly that, and it made understanding this process way, way easier. Let's take a look at the first step, though.

Decoding the signature for validation

If you try to decode a string of Base64-encoded text using the base64Decode function in this solution, you're going to get a big fat error. Silly me for even trying! Base64URL encoding is different from Base64 - it replaces the special characters + and / with - and _, in order to make the string URL-friendly. Not only that, but the C# cryptographic classes we'll be using actually need the data in a byte array, so there's no point in going to the trouble to convert it to a string.

This is all you need to do to decode the signature:

  • Swap the URL-friendly characters for Base64-compliant ones.
  • Pad the string with = if necessary.
  • Convert the string into a byte array.
public byte[] base64urldecode(string arg)
{
	//this swaps out the characters in Base64Url encoding for valid Base64 syntax
	//C# can't decode Base64 without doing this first
	arg = arg.Replace('-', '+');
	arg = arg.Replace('_', '/');

	int strlen = arg.Length + (4 - (arg.Length % 4));
	char pad = '=';

	if (strlen != (arg.Length + 4))
	{
		arg = arg.PadRight(strlen, pad);
	}

	//return the Base64 decoded data as a byte array, since that's what we need for RSA
	byte[] arg2 = Convert.FromBase64String(arg);

	return arg2;
}

That wasn't so hard! Now we can just use this function to convert our signature. At the same time, we need to convert the first two concatenated JWT segments into a byte array, because that's what the .NET X509 certificate library expects.

byte[] sig = base64urldecode(signature);

//the header and payload need to be converted to a byte array
byte[] data = Encoding.UTF8.GetBytes(toVerify);

And just like that, we're done with step one!

Verifying the signature using RSA PKCS#1

I have a confession to make. I spent a long time - several days, in fact - trying to use various bits of .NET's RSA libraries to do the actual signature validation. I got more than a little frustrated and probably cursed at least twice at my laptop.

Then, a messenger from above came to me in a dream and gave me everything I needed to know. Seriously.

Nah, what actually happened was that my hours of Googling finally proved fruitful, and I found sample code on MSDN that did exactly what I needed to do. This example is almost verbatim what I used.

To start, we need to take our locally-cached certificate, which is stored as plain text, and convert it into an X509Certificate object. This will allow us to read the various properties of the certificate, much like how you can view certificate details in Windows' local certificate store. Because the original X509Certificate class was very limited, Microsoft later expanded it with a subclass titled X509Certificate2. We need some specific details of our certificate that are only available in this class, so we're going to convert it twice.

Again, we'll use the key ID, or kid, to pull the appropriate certificate.

//create an X509 cert from the google certificate in local cache
X509Certificate gcert = X509Certificate.CreateFromCertFile(@"C:certs" + kid + ".cer");

//we need to use the new X509Certificate2 subclass in order to pull the public key from the certificate
X509Certificate2 gcert2 = new X509Certificate2(gcert);

The actual certificate we're going to use is gcert2. The RSACryptoServiceProvider class provides encryption and decryption functionality using the RSA algorithm. Since the JWT signature is encrypted with Google's private key, we can use Google's public key, which is stored in our certificate, to decrypt it. This takes the PublicKey parameter of our X509 certificate and converts it for use with the other RSA functions we need.

using (var rsa = (RSACryptoServiceProvider)gcert2.PublicKey.Key)
{

Since the decrypted signature is a SHA-256 hash, we need to generate the hash we'll be comparing against the decrypted signature. To do this we'll just hash the byte array we created earlier named data.

//create a new byte array that contains a SHA256 hash of the JSON header and payload
byte[] hash;
using (SHA256 sha256 = SHA256.Create())
{
	hash = sha256.ComputeHash(data);
}

Next, we'll use the RSAPKCS1SignatureDeformatter class with our public key. This class does the actual signature verification, comparing the signed hash with the hashed JWT header and payload.

//Create an RSAPKCS1SignatureDeformatter object and pass it the   
//RSACryptoServiceProvider to transfer the key information.
RSAPKCS1SignatureDeformatter RSADeformatter = new RSAPKCS1SignatureDeformatter(rsa);
RSADeformatter.SetHashAlgorithm("SHA256");

Finally, the actual verification will return TRUE or FALSE to our login function.

		//Verify the hash and return the appropriate bool value  
		if (RSADeformatter.VerifySignature(hash, sig))
		{
			verified = true;
		}
		else
		{
			verified = false;
		}
	}

	return verified;
}

Just to put together everything, here's how you should use this signature verification process with the payload in Google's original JWT:

if (verifySignature(glh.kid, tokenArray))
{
	//process payload
	JavaScriptSerializer js2 = new JavaScriptSerializer();
	gLoginClaims glc = js2.Deserialize<gLoginClaims>(base64Decode(tokenArray[1]));

	//we can tell the session that we're logged in
	Session["loggedin"] = "yes";
	responseData = "You have successfully logged in using " + glc.email + ".";
}

Obviously, this doesn't do anything to add the user to our application's local database. I haven't gotten that far. At this point, however, we've taken all the measures necessary to verify the integrity of a user's Google account information and have assurance that the user is who they say they are.

The big conclusion

Google's caution against using custom code is not without reason. Most web developers aren't going to understand enough about cryptography to know what needs to be done to ensure end-to-end integrity when using a third-party service for logging in. I had the distinct advantage of knowing a cryptography and security professional who was able to go through all the documentation with me and explain what each security term meant and what I needed to figure out how to do it in C#.

If you're still not comfortable with using your own code like this, that's okay - there's nothing wrong with using third party libraries. You just want to make sure you're using a library that is well-supported and well-maintained, because information security and cryptography are always evolving and changing as new threats and innovations are brought to the table.

It just depends on your needs and what you want to accomplish. I found this whole exercise to be very enlightening, as I now have a much deeper understanding of how OpenID and OAuth work. Hopefully, reusing this code for other providers should be relatively trivial. If not, at least I'll have something else to write about!

References

C# classes

Members of System.IO

  • File: Facilitates working with files (creating, deleting, etc.). This is used to write Google's public certificates to your server's local disk.
  • StreamReader: An implementation of TextReader for reading text from a byte stream. This is used to read Google's JSON-formatted response.
  • StreamWriter: An implementation of TextWriter for writing text to a byte stream. This is used to send the contents of the POST request to Google.

Members of System.Net

  • HTTPWebRequest: An implementation of WebRequest that uses HTTP. This is used to send a POST request to Google to obtain authorization to use the user's credentials.
  • HTTPWebResponse: An implementation of WebResponse that uses HTTP. This is uses to download Google's response once the user authorizes the application.
  • WebRequest: Used to send data streams through the Internet. This is used to request Google's public certificates for caching locally.
  • WebResponse: Used to receive data streams through the Internet. This is used to download Google's public certificates.

Members of System.Security.Cryptography

  • RSACryptoServiceProvider: Utilizes the RSA algorithm for asymmetric encryption and decryption. This is used to decrypt the JWT signature using Google's public key.
  • RSAPKCS1SignatureDeformatter: Verifies an RSA PKCS #1 signature. This is used to verify that the JWT signature matches the signed data.
  • SHA256: Generates the SHA256 hash for a given input. This is used to generate the hash of the JWT segments signed by Google.

Members of System.Security.Cryptography.X509Certificates

  • X509Certificate: Provides a simple set of methods for working with X509 Certificates. This is used to convert the locally-cached Google public certificate into an X509 Certificate object for programmatic use.
  • X509Certificate2: An extension of X509Certificate that provides more methods for working with certificates. This is used to extract the public key from the locally-cached Google certificate.

Members of System.Text

  • Decoder: Decodes a byte array into a string. This is used with UTF8Encoding (described below).
  • UTF8Encoding: Represents UTF-8 encoding of text. This is used with Decoder to convert a byte array into a string in our Base64Decode function.

Members of System.Web.Script.Serialization

  • JavaScriptSerializer: Provides serialization and deserialization functionality for AJAX-enabled applications. This is used to serialize Google's JSON-formatted responses into key-value pairs.