using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; //these are additional libraries needed beyond the default ones //Reference: http://blog.abettergeek.com/?p=472#part2 using System.IO; using System.Net; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Web.Script.Serialization; namespace GoogleLogin { public partial class login : System.Web.UI.Page { 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(); } } public string GoogleLogin(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(); } //now that we have the responseData (which is a JSON array), we can do stuff with it. //process JSON array JavaScriptSerializer js = new JavaScriptSerializer(); gLoginInfo gli = js.Deserialize(googleAuth); //the id_token is what needs to be decoded to (a) verify it and (b) get some basic details about the user //gli.id_token is in three parts. the first two are Base64 encoded plaintext. string[] tokenArray = gli.id_token.Split(new Char[] { '.' }); //tokenArray[0] is the JSON header //tokenArray[1] is the JSON payload //tokenArray[2] is an encrypted digital signature //process header JavaScriptSerializer js1 = new JavaScriptSerializer(); gLoginHeader glh = js1.Deserialize(base64Decode(tokenArray[0])); //verify signature based on header data - the signature is tokenArray[2] //we need the keyID, which is glh.kid //use header data to validate signature //if the signature is valid, we can process the payload //once the payload is processed, we can either add a new user or log in an existing user if (verifySignature(glh.kid, tokenArray)) { //process payload JavaScriptSerializer js2 = new JavaScriptSerializer(); gLoginClaims glc = js2.Deserialize(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 + ". You can logout if you'd like.
"; } } catch (Exception ex) { //this shouldn't ever happen. responseData = "
ERROR" + ex.StackTrace + "
"; } return responseData; } public class gLoginInfo { public string access_token, token_type, id_token; public int expires_in; } public class gTokenInfo { public string issuer, issued_to, audience, user_id, email, email_verified; public int expires_in, issued_at; } public class gLoginHeader { public string alg, kid; } public class gLoginClaims { public string aud, iss, email_verified, at_hash, azp, email, sub; public int exp, iat; } 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); } } 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; } 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; //pull out the different elements from the original JWT provided by Google string toVerify = jwt[0] + "." + jwt[1]; string signature = jwt[2]; //the JWS is encoded in Base64 that C# doesn't like //we need to replace some special characters and then Base64Decode this into a byte array byte[] sig = base64urldecode(signature); //the header and payload need to be converted to a byte array byte[] data = Encoding.UTF8.GetBytes(toVerify); //before we do anything else, we need to locally cache Google's public certificate, if it isn't already cacheCertificate(kid); //now we can validate the signature against our locally-cached 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); //this lets us use the public key from the cached Google certificate using (var rsa = (RSACryptoServiceProvider)gcert2.PublicKey.Key) { //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); } //Create an RSAPKCS1SignatureDeformatter object and pass it the //RSACryptoServiceProvider to transfer the key information. RSAPKCS1SignatureDeformatter RSADeformatter = new RSAPKCS1SignatureDeformatter(rsa); RSADeformatter.SetHashAlgorithm("SHA256"); //Verify the hash and return the appropriate bool value if (RSADeformatter.VerifySignature(hash, sig)) { verified = true; } else { verified = false; } } return verified; } 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")) { //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(); } //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 cts = js.Deserialize>(certs); 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 //at some point the server needs to have a daily script running that deletes certificates that are more than 48 hours old System.IO.File.WriteAllText(@"C:\certs\" + kid + ".cer", b64); } } //This is just for testing that your configuration allows write access to the folder where cached certificates are stored. //You don't need this in your own project. public bool hasWriteAccessToFolder(string folderPath) { try { // Attempt to get a list of security permissions from the folder. // This will raise an exception if the path is read only or do not have access to view the permissions. System.Security.AccessControl.DirectorySecurity ds = Directory.GetAccessControl(folderPath); return true; } catch (UnauthorizedAccessException) { return false; } } } }