Using Google OAuth in asp.net

Storing and managing user data such as username or password can be a lot of pain. You need to deal a lot with security issues such as encryption and secure protocols and you create yet another website with username and password registration. I wanted to get rid of this and allowing certain GMail accounts to my applications, where Google does the job of authentication for me. See how this works with FormsAuthentication in a C# sample

Table of contents

WARNING - Deprecated by April 20, 2015!

Google will shut down the OAuth 2.0 Authentication by April 20th. Therefore this code won't be working anymore by then.

See my recent article about the replacement OpenID Connect.

Federated Login

A very common approch to web application development is to store user credentials such as username and password into your own business database. You have to deal with password  encryption and other security issues. You should have a ssl certificate (expensive) for the login process, so that the password is transfered in a secure way. A lot of concerns to think of.

A more advanced approach to this (old) concept is "federated login". You can choose an identity provider of your choice, as I did on my website with Google. By doing so, you still want to save some data in your business database about the user, which is not a problem at all, you only need to remove the credentials from your own database and replace it with a foreign key reference (ClaimedIdentifier) you will get from your identity provider. Note that you will receive always a different ID per application you have! For some pages I actually didn't use the ClaimedIdentifier, instead I've choosen the email-address, but be aware, this might cause troubles as soon as the users changes the email address, but he is still the same user.

Once you've linked your web application with Google for example, users have the benefit of re-using one account they already have. The authentication process is handeld by Google on HTTPS in a secure way. The user might have turned on even a two-factor authentication which makes it even more secure.

Note that you can actually offer many OpenID providers to login, still you need to consider a certain dependancy to your identity provider.

How it works

  1. The FormsAuthentication is configured in a way to detect if a user is logged in or not and let the .net Framework to do the rest to actually redirect to my "AuthenticationEndpoint.aspx" 
  2. The AuthenticationEndpoint.aspx verifies if a "callback" parameter is present, which is not the case at this time. So the AuthenticationEndpoint calls the PerformGoogleAuthentication method
  3. The PerformGoogleAuthentication method sets up a callback address, telling also to set a "callback" parameter with the value "true" and defines which attributes (you may call them also extensions or claims) it would like to have from Google
  4. The PerformGoogleAuthentication redirects the user to google, including with the information of the requested attributs
  5. The user might need to login now at Google (or skip this step if he is already)
  6. The user might need to grant access at google to allow the requested information to be sent to your page (if he did already, this step will be skipped)
  7. Google redirects back to the AuthenticationEndpoint.aspx with the attributes and the callback parameter, which was defined in a step earlier
  8. The AuthenticationEndpoint.aspx requests again the callback parameter which is now set to "true", so the page calls the HandleAuthenticationCallback method
  9. The HandleAuthenticationCallback method uses again the DotNet-Auth library to simply verify the posted data, if they were compromised, an exception is being thrown.
  10. With the parameters that were posted back from Google, such as Email, Firstname and Lastname, the FormsAuthentication AuthCookie is set and redirected back to the calling address
  11. The user is now logged in.  

You might want to see the graphical flow-diagram

Prerequisite

The example is based on the DotNet OpenAuth library. I wrote the sample in C# / .net4.

Code sample

First of all, you need to enable the FormsAuthentication for your application and define the AuthenticationEndpoint as your login page in the web.config. In fact the user will never see this page, as this is used only to redirect to Google or receiving the data that's being posted back from Google.

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="uri" 
             type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, 
PublicKeyToken=b77a5c561934e089
"/>     <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection"               requirePermission="false" allowLocation="true"/>   </configSections>   <uri>     <idn enabled="All"/>     <iriParsing enabled="true"/>   </uri>   <system.web>     <compilation debug="true" targetFramework="4.0" />     <authorization>             <deny users="?"/> <!-- Deny all anonymous -->     </authorization>     <authentication mode="Forms"> <!-- Enable FormsAuthentication-->       <!-- Set the Endpoint as your login page-->       <forms loginUrl="AuthenticationEndpoint.aspx"              path="/"              requireSSL="false"/>            </authentication>   </system.web>   <!-- Grant access to this page to everyone -->   <location path="AuthenticationEndpoint.aspx">     <system.web>       <authorization>         <allow users="*"/>       </authorization>     </system.web>   </location> </configuration>


This is the core logic of the authentication in a simple ASPX file called "AuthenticationEndpoint.aspx"

/* 
 * Written by Dominik Amon, 2011-12-13
 * http://www.dominikamon.com/
 * 
 * Licensed under GPL:
 * http://www.gnu.org/copyleft/gpl.html
 * 
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Security;
using System.Web.Security;
 
//To run this, you need to download DotNetOpenAuth from the following url
//http://www.dotnetopenauth.net/
//Simply add a reference to the DotNetOpenAuth.dll
//I've tested this with version 3.4.7.11121
using DotNetOpenAuth.OpenId.RelyingParty; 
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
 
namespace Amon.cc.Authentication.EndpointSample
{
    public partial class AuthenticationEndpoint : System.Web.UI.Page
    {
 
        private const string CALLBACK_PARAMETER = "callback";
        private const string RETURNURL_PARAMETER = "ReturnUrl";
        private const string AUTHENTICATION_ENDPOINT = 
                                "~/AuthenticationEndpoint.aspx";
        private const string GOOGLE_OAUTH_ENDPOINT =
            "https://www.google.com/accounts/o8/id";
        /// <summary>
        /// Call Google Authentication
        /// </summary>
        protected void PerformGoogleAuthentication()
        {
            using (OpenIdRelyingParty openid = new OpenIdRelyingParty())
            {
                //Set up the callback URL
                Uri callbackUrl = new Uri(                    
                    String.Format("{0}{1}{2}{3}?{4}=true",
                    (Request.IsSecureConnection)?"https://":"http://",
                    Request.Url.Host,
                    (Request.Url.IsDefaultPort)?
                        String.Empty:String.Concat(":", Request.Url.Port),
                    Page.ResolveUrl(AUTHENTICATION_ENDPOINT),
                    CALLBACK_PARAMETER
                    ));
              
                //Set up request object for Google Authentication
                IAuthenticationRequest request =
                    openid.CreateRequest(GOOGLE_OAUTH_ENDPOINT,
                    DotNetOpenAuth.OpenId.Realm.AutoDetect, callbackUrl);
                
 
                //Let's tell Google, what we want to have from the user:
                var fetch = new FetchRequest();
                fetch.Attributes.AddRequired(WellKnownAttributes.Contact.Email);                
                fetch.Attributes.AddRequired(WellKnownAttributes.Name.First);
                fetch.Attributes.AddRequired(WellKnownAttributes.Name.Last);
                request.AddExtension(fetch);
 
                //Redirect to Google Authentication
                request.RedirectToProvider();
            }
        }
 
        /// <summary>
        /// Handle the response that Google posted back
        /// </summary>
        public void HandleAuthenticationCallback()
        {
            OpenIdRelyingParty openid = new OpenIdRelyingParty();
            var response = openid.GetResponse();
            if (response == null) { ThrowSecurityException();  return; }
 
            switch (response.Status)
            {
                case AuthenticationStatus.Authenticated:
                    var fetch = response.GetExtension<FetchResponse>();
                    string email = string.Empty;                    
                    string firstname = string.Empty;
                    string lastname = string.Empty;
                    if (fetch != null)
                    {
                        email = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);
                        firstname = fetch.GetAttributeValue(WellKnownAttributes.Name.First);
                        lastname = fetch.GetAttributeValue(WellKnownAttributes.Name.Last);
                        FormsAuthentication.SetAuthCookie(response.ClaimedIdentifier, false);
                        FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, false);
 
                    }
                    else //we didn't fetch any info. Too bad.
                    {
                        ThrowSecurityException();
                    }
                    break;  
                //You might want to differ the states a bit more
                default:
                    ThrowSecurityException();
                    break;
                }            
        }
 
        /// <summary>
        /// This exception throws a simple and ugly error message. 
        /// You may improve this message ;-)
        /// </summary>
        public void ThrowSecurityException()
        {
            throw new SecurityException("Authentication failed");
        }
 
        protected void Page_Load(object sender, EventArgs e)
        {
            //Check if User is already authenticated and has a ReturnUrl
            if (User != null && User.Identity != null && User.Identity.IsAuthenticated 
                && !String.IsNullOrWhiteSpace(Request.Params[RETURNURL_PARAMETER]))
            {
                Response.Redirect(Request.Params[RETURNURL_PARAMETER]);                
            }
            else
            {
                //Check if either to handle a call back or start an authentication
                if (Request.Params[CALLBACK_PARAMETER]=="true")
                {
                    HandleAuthenticationCallback(); //Google has performed a callback, let's analyze it
                }
                else
                {
                    PerformGoogleAuthentication();  //There is no callback parameter, 
                                                    //so it looks like we want to sign in our fellow
                }
            }
 
        }
    }
}

 

Download sample code

See also

© Copyright 2004-2017 - Dominik Amon