You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
9.6 KiB
304 lines
9.6 KiB
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<gLoginInfo>(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<gLoginHeader>(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<gLoginClaims>(base64Decode(tokenArray[1])); |
|
|
|
|
|
//we can tell the session that we're logged in |
|
Session["loggedin"] = "yes"; |
|
responseData = "<div class='good'>You have successfully logged in using " + glc.email + |
|
". You can <a href='?logout=yes'>logout</a> if you'd like.</div>"; |
|
} |
|
|
|
} |
|
catch (Exception ex) |
|
{ |
|
//this shouldn't ever happen. |
|
responseData = "<div class='bad'>ERROR</h1" + ex.Message + "<br />" + ex.StackTrace + "</div>"; |
|
} |
|
|
|
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<dynamic, dynamic> cts = js.Deserialize<Dictionary<dynamic, dynamic>>(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; |
|
} |
|
} |
|
} |
|
} |