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
2 years ago
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|