Tuesday, March 5, 2019

Integrate Google Oauth 2.O OpenId without Owin in sitecore asp.net mvc

Introduction:

I've sitecore 8.2.1 and content delivery web app is integrated with form based authentication. I enabled third party google login using OAuth2.0 open id without using .net OWIN or Sitecore OWIN setup. 
I just keep the whole flow simple with very minimal impact to the overall architecture. Made solution easy to decouple in future or reuse it across the different project.

The below problem statement is addressed while implementing this solution
  • Handle Session timeout for form based users and google logged in users differently
  • Handle log-off so that user is redirected to specific login page.
  • Handle CSRF Cross side anitorgery token using State property of api call.

Let's get started

I assume one understand to enable google oauth using developer console and well verse with the whole steps of how google oauth and api is enabled for us to proceed. If not I recommend to go through this using below links

It is two step implementation

  1. Create a separate login screen or existing screen for that matter. Assume there is a button say google login in it. On click of it , it will initiate google login screen and consent form etc.Google returns Authorization code.
  2. When user select his/her google account and give consent user is redirected to respective home or landing page. In this part access token is received based on Authorization code. Custom authorization can be checked against custom application database and accordingly user will be redirect to landing page. Mind it. The first redirection url is registered with google that is where we will write our login in controller action. 

Initiate Google Login process


Key take away

In order to work through different login mechanism the only way to handle session and log-off is to set a application level state flag.
Set something at very beginning of login initialization

HttpContext.Application.Add("google_logged_in_users"true);



Also very important once we get google access token and we verify google users to have access to our system. It is important to check @if (User.Identity.IsAuthenticated)


please note above code is just for reference and it is draft version. You can refine and refactor as per your needs.
namespace GoogleOAuth.ASPNetMVC
{
public partial class GoogleLoginController : Controller
{
public virtual ActionResult OpenGoogleLogin()
{
var openIdUrl = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.OpenIdUrl];
var clientId = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.ClientId];
var redirectUrl = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.RedirectUrl];
var scope = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.Scope];
var state = Guid.NewGuid();
_sessionService.Set("CSRF_TOKEN_KEY",state);
var url = $"{openIdUrl}?response_type=code
&redirect_uri={redirectUrl}
&scope={scope}
&client_id={clientId}
&state={state}
&include_granted_scopes=true
&access_type=offline";
return Redirect(url);
}
}
}
view raw GoogleLogin.cs hosted with ❤ by GitHub
using System.Web.Mvc;
using System.Text;
using System.Net;
using System.IO;
using System.Configuration;
using Newtonsoft.Json;
namespace GoogleOAuth.ASPNetMVC
{
public virtual ActionResult GoogleReturnRedirectUrlSingleSignOn()
{
if (string.IsNullOrEmpty(Request?.RawUrl))
{
return Redirect("Some Access Denied Url");
}
var openIdUrl = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.OpenIdUrl];
var clientSecret = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.ClientSecret];
var clientId = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.ClientId];
var redirectUrl = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.RedirectUrl];
var scope = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.Scope];
var baseUrl = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.TokenBaseUrl];
var requestUrl = ConfigurationManager.AppSettings[AppSettingKeys.GoogleOAuth.RequestUrl];
string queryUrl = Request.RawUrl;
//validate CSRF token
var indexofStartOfQuerystring = queryUrl.IndexOf('?');
if (indexofStartOfQuerystring >= 0)
{
var queryString = (indexofStartOfQuerystring < queryUrl.Length - 1)
? queryUrl.Substring(indexofStartOfQuerystring + 1)
: string.Empty;
if (!string.IsNullOrEmpty(queryString))
{
NameValueCollection googleOAuthQuerystringCollection = HttpUtility.ParseQueryString(queryString);
var csrfTokenReceivedFromGoogleRedirectUrl= googleOAuthQuerystringCollection["state"];
var storedCsrfTokenFirstTime= _sessionService.Get<Guid>("CSRF_TOKEN_KEY").ToString();
//anti-forgery state token: protect the security of your users by preventing request forgery attacks
//https://developers.google.com/identity/protocols/OpenIDConnect
if (!csrfTokenReceivedFromGoogleRedirectUrl.Equals(storedCsrfTokenFirstTime, StringComparison.OrdinalIgnoreCase))
{
return Redirect("Some Access Denied Url");
}
var accessCode = googleOAuthQuerystringCollection["code"];
if (string.IsNullOrEmpty(accessCode))
{
return Redirect("Some Access Denied Url");
}
var accessTokenRequest = new AccessTokenRequest
{
AuthorizationCode = accessCode,
BaseUrl = baseUrl,
ClientId = clientId,
ClientSecret = clientSecret,
RequestUrl = requestUrl,
RedirectUrl = redirectUrl
};
var webRequest = (HttpWebRequest)WebRequest.Create(accessTokenRequest.RequestUrl);
webRequest.Method = "POST";
var requestContent =
$"code={accessTokenRequest.AuthorizationCode}
&client_id={accessTokenRequest.ClientId}
&client_secret={accessTokenRequest.ClientSecret}
&redirect_uri={accessTokenRequest.RedirectUrl}
&grant_type=authorization_code";
byte[] byteArray = Encoding.UTF8.GetBytes(requestContent);
webRequest.ContentType = "application/x-www-form-urlencoded";
webRequest.ContentLength = byteArray.Length;
var postStream = webRequest.GetRequestStream();
// Add the post data to the web request
postStream.Write(byteArray, 0, byteArray.Length);
postStream.Close();
var response = webRequest.GetResponse();
postStream = response.GetResponseStream();
if (postStream != null)
{
var reader = new StreamReader(postStream);
var responseFromServer = reader.ReadToEnd();
var accessTokenResponse = JsonConvert.DeserializeObject<AccessTokenResponse>(responseFromServer);
if (string.IsNullOrEmpty(accessTokenResponse?.Token))
{
return Redirect("Some Access Denied Url");
}
//Use accessTokenResponse?.Token to get call Google Profile
//Or google service api and then redirect to home page
return Redirect("Home Page Url");
}
}
}
}
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var isAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;
bool? isGoogleOAuthLogin =(bool?) filterContext.HttpContext.Application["google_logged_in_users"];
if (!isAuthenticated)
RedirectToAccessDeniedPage(filterContext);
if (userContext == null)
{
if (isGoogleOAuthLogin.GetValueOrDefault())
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
Sitecore.Diagnostics.Log.Info("Session expired.Redirecting user to login page...", this);
response.RedirectUrl = some page;
filterContext.HttpContext.Response.StatusCode = 441;
return;
}
else
{
Sitecore.Diagnostics.Log.Info("Session expired.Redirecting user to login page...", this);
string returnUrl = some login page;
filterContext.Result = new RedirectResult(returnUrl);
return;
}
}
else if (filterContext.HttpContext.Request.IsAjaxRequest())
{
Sitecore.Diagnostics.Log.Info("Session expired.Redirecting user to login page...", this);
response.RedirectUrl = some different page;
filterContext.HttpContext.Response.StatusCode = 440;
return
}
else
{
if (!HttpContext.Current.Response.IsRequestBeingRedirected)
{
Sitecore.Diagnostics.Log.Info("Session expired.Redirecting user to login page...", this);
string returnUrl = some differnt different page;
filterContext.Result = new RedirectResult(returnUrl);
return;
}
}
return;
}
}
view raw OnAuthorization hosted with ❤ by GitHub

Reference: