A secure implementation of OAuth2 using vanilla ASP.NET C#. Requires no third-party libraries or assemblies.
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

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;
}
}
}
}