ASP.NET Core - Implement Google One Tap Sign In

Raymond Raymond event 2021-05-18 visibility 1,644 comment 26
more_vert
ASP.NET Core - Implement Google One Tap Sign In

Google one tap is a cross-platform sign-in mechanism for Web and Android. It enables Google users to sign up to a third party services like website literally with one tap only.  This article provides detailed steps to implement this sign-in mechanism for ASP.NET Core website. 

About Google one tap sign-up

For website implemented Google one tag sign up or sign in like Kontext, a popup window will show up if the user already signed into Google accounts. Once the user click 'Continue as ...' button, Google will return credential details to the callback service; the credential can be used to retrieve more information about the Google user. 

Now let's start to implement Google one tap in an ASP.NET core application. 

infoASP.NET Identity can be used to keep one tap returned user information as local account. For more information about Identity Core, please refer to official documentation Introduction to Identity on ASP.NET Core | Microsoft Docs. This article won't cover details about identity.

Register Google API client

The first step is to register a Google API client ID and configure OAuth Consent Screen. Refer to the official documentation: Get your Google API client ID  |  Sign In With Google. This step is required for normal Google sign-in and one tap sign-in. 

The main tasks involved are:

  1. Create OAuth client ID credentials via Google APIs console.
  2. Configure OAuth Consent Screen via OAuth consent screen.

There are two data items can be used later on: ClientId and ClientSecret.  ClientSecret is not mandatory for Google One Tap sign-in but is required for Google Sign-in (Google external login setup in ASP.NET Core | Microsoft Docs). 

 "Google": {
      "ClientId": "***.apps.googleusercontent.com",
      "ClientSecret": "***"    }

Add client scripts

Now we can add Google One Tap required client scripts into client pages. 

First we need to add Google JavaScript file into HTML header section of Razor pages or MVC views. 

<script src="https://accounts.google.com/gsi/client" async defer></script>

And then add the following HTML to the pages that you want to show one tag sign-in popup window:

<div id="g_id_onload"
             data-client_id="***"
             data-login_uri='/GoogleOneTap'>
</div>

The value for data-client_id attribute need to be replaced with Google OAuth client Id created in the previous steps. For the second attribute data-login_uri that is the callback URI once the authentication is successful. In the example, URI /GoogleOneTap is used as callback handler. We will add this controller or Razor page in next step.

Add callback handlers

Let's add a Razor page named GoogleOneTap. In this Razor page, we need to implement a handler for POST HTTP request. For MVC project, please follow similar steps. 

/// <summary>
/// Google one tap sign in handler
/// </summary>
/// <returns></returns>
public async Task<IActionResult> OnPostAsync()
{
}

This function will be invoked once Google authenticate the user successfully and it will send a POST HTTP request to this Razor page. In the POST form body, there are two critical elements:

  • g_csrf_token: CSRF token.
  • credential: the returned credential that can be used in Google APIs for validation. 

Before implementing the function, we need to add two packages into the ASP.NET Core web project: Google.Apis.Auth and Microsoft.AspNetCore.Authentication.Google. The first package will be used to retrieve user details like Email, GivenName, FamilyName, JWT ID token, etc. The second package will be used to sign-in to ASP.NET Identity. 

Implement the callback function

The main steps for implementing the callback function involves:

  1. Verify CSRF token.
  2. Verify returned credential (ID token).
  3. Sign in to ASP.NET core Identity service using SignInManager with returned payload details.
  4. If sign-in is not successful, it means the local account is not created yet in Identity; thus UserManager can be used to create local account and add external login details (scope subject) with provider as Google and then sign-in again before returning to the previous visited URL.
  5. If sign-in is successful, return to the previous visited URL (ReturnUrl) before one tap sign in. 

The sample code looks like the following code snippet:

/// <summary>
/// Google one tap sign in handler
/// </summary>
/// <returns></returns>
public async Task<IActionResult> OnPostAsync()
{
    // Validate Google CSRF first since we turned off asp.net core CSRF check.
    var google_csrf_name = "g_csrf_token";
    var cookie = Request.Cookies[google_csrf_name];
    if (cookie == null)
        return StatusCode((int)HttpStatusCode.BadRequest);

    var requestBody = Request.Form[google_csrf_name];
    if (requestBody != cookie)
        return StatusCode((int)HttpStatusCode.BadRequest);

    var idToken = Request.Form["credential"];
    GoogleJsonWebSignature.Payload payload = await GoogleJsonWebSignature.ValidateAsync(idToken).ConfigureAwait(false);

    // Sign-in users
    var provider = GoogleDefaults.AuthenticationScheme;
    var providerKey = payload.Subject;
    var result = await _signInManager.ExternalLoginSignInAsync(provider, providerKey, isPersistent: false, bypassTwoFactor: true).ConfigureAwait(false);
    
	if (result.Succeeded)
		return LocalRedirect(ReturnUrl);
    if (result.IsLockedOut)
        return RedirectToPage("./Lockout");
    else
    {
        // If the user does not have an account, then create an account.
		var user = new ApplicationUser { UserName = payload.Email, Email = payload.Email, FirstName = payload.GivenName, LastName = payload.FamilyName, IsEnabled = true, EmailConfirmed = true, DateRegister = DateTime.Now };
		await _userManager.CreateAsync(user).ConfigureAwait(false);
		// Add external Google login
		await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerKey, provider)).ConfigureAwait(false);
		// Sign-in the user
        await _signInManager.SignInAsync(user, isPersistent: false).ConfigureAwait(false);

        return LocalRedirect(ReturnUrl);
    }
}

In the above code snippet, ApplicationUser class is inherited from IdentityUser. Fields _signInManager and _userManager are the instances of Identity SignInManager and UserManager respectively. 

Now, the main functions are implemented for Google one tap sign-in.

Hopefully you are now familiar with the main steps required to implement Google One Tag sign-up or sign-in in ASP.NET Core applications. A sample project will be provided later for reference. 

More from Kontext
comment Comments
RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

Hi,


Do you have a video explaining how to do that? I need it for my project

Raymond Raymond

Raymond access_time 2 years ago link more_vert

Hi Rami,

I don't have time to publish a video at the moment. Do you have any specific questions about the steps mentioned above?

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

Dear Raymond,


Thanks for the reply. I have some questions. The razer page is it a class or just a razor file? Where do i put this file in the project? the last sample code, you mentioned that:


ApplicationUser class is inherited from IdentityUser. Fields _signInManager and _userManager are the instances of Identity SignInManager and UserManager respectively.


I am new to asp.net so I am not sure how to use them because when i took your code it gave me errors. Lastly what is a callback handeler and how call it and where to save it in the project?


Thanks again for the help in advance.

Raymond Raymond

Raymond access_time 2 years ago link more_vert

Hi Rami,

About Razor Page, I recommend you follow this official page to understand the details. For each page, it usually contains one .cs (PageModel) class file and also one .cshtml file.


About ApplicationUser and other identity related classes, they are included as part of  ASP.NET Core Identity. You can follow this page to understand more: Introduction to Identity on ASP.NET Core | Microsoft Learn. For the example I provided, it utilizes Identity to create users or update users. However to integrate your web application with Google One Tap Sign-in, you don't necessarily need to use Identity and you can create your own identity systems to manage your website's users.


About Callback part: once user clicks the prompt to sign in with Google and if the authentication is successful, Google will make a POST HTTP call to the endpoint you provided. In the POST request, a CSFR token and credential will be provided. Through the credential, you can get user's identity information from Google, for example, first name, last name, email addresses, etc.

new ApplicationUser { UserName = payload.Email, Email = payload.Email, FirstName = payload.GivenName, LastName = payload.FamilyName, IsEnabled = true, EmailConfirmed = true, DateRegister = DateTime.Now };

The information is included in the response payload after validating the credential successfully:

GoogleJsonWebSignature.Payload payload = await GoogleJsonWebSignature.ValidateAsync(idToken);

The above code uses an OAuth package published by Google: Class GoogleJsonWebSignature (1.60.0)  |  .NET client library  |  Google Cloud.


To learn more about how this whole flow works, I suggest you reading more about OAuth if you have not worked on it previously.

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

Thanks for the help and I hope that you can guide me with what I have.


in my index page:

<div id="g_id_onload"
     data-client_id="???"
     data-login_uri='/google-response'>
</div>

in my controller page:

[Route("google-response")]
        public async Task<ActionResult> GoogleResponse()
        {

            var google_csrf_name = "g_csrf_token";
            var cookie = Request.Cookies[google_csrf_name];
            if (cookie == null)
                return StatusCode((int)HttpStatusCode.BadRequest);

            var requestbody = Request.Form[google_csrf_name];

            if (requestbody != cookie)
                return StatusCode((int)HttpStatusCode.BadRequest);

            var idtoken = Request.Form["credential"];
            GoogleJsonWebSignature.Payload payload = await GoogleJsonWebSignature.ValidateAsync(idtoken).ConfigureAwait(false);

            TempData["name"] = payload.Name;
            TempData["email"] = payload.Email;
            return Json(payload);

        }

in appsettingsjson:

 "GoogleAuthSettings": {
   "ClientId": "???",
   "ClientSecret": "???"
 }

I added the following packages:

Google.Apis.Auth and Microsoft.AspNetCore.Authentication.Google


I try to run the above but nothing is showing.

Can you help. Thanks

Raymond Raymond

Raymond access_time 2 years ago link more_vert

Did you register your local dev application in Google Cloud? data-client_id is the Client Id of the OAuth you registered.

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

Yes I can login to google using the external login but I am interested to login using the one tap.2023091131509-Capture.PNG

but one tap is not showing

Raymond Raymond

Raymond access_time 2 years ago link more_vert

Have you logged into a Google account in the same browser? I believe one tap only shows up of you have logged into the same browser or using a browser with google account signed in.

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

yes Iam using chrome and I am logged into my profile, might be the reason?

Raymond Raymond

Raymond access_time 2 years ago link more_vert

Can you check if there are any JavaScript errors when using Developer Tools (F12) in your browser?

BTW, I assume you have included the following JavaScript within <head> tag?

  <script src="https://accounts.google.com/gsi/client" async defer></script>
RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

I pressed F12 and got this error:


Failed to load resource: the server responded with a status of 403 ()

[GSI_LOGGER]: The given client ID is not found.


but it is the same client ID i have.

Raymond Raymond

Raymond access_time 2 years ago link more_vert

If the client ID is correct (i.e. sign-in with Google works), there might be something wrong with Google GSI.

Please double check these two items:

  1. I assume your origin URL configured in Google Cloud is the same as your local website's URL?

  2. The rendered HTML has the correct client id. The element should look like the following:

    <div id="g_id_onload" data-client_id='***-****.apps.googleusercontent.com'
             data-login_uri='***'>


If there is nothing wrong with the above two, I'm afraid I won't be able to help much further. If you just registered the client app, I would suggest waiting for a little bit longer to try again (though I highly doubt that will be the cause).


You can try to contact Google for help or post it in Google forums. 

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

Thanks for the help and I finally got it working.


<div id="g_id_onload"

data-client_id="943211693904-bhh925h3rlbj7ml41rno455guds2uvqk.apps.googleusercontent.com"

data-context="signin" data-login_uri="https://localhost:44307/google-response" data-auto_select="false"

data-itp_support="false">

</div>


in google-response:


[Route("google-response")]

public async Task<ActionResult> GoogleResponse()

{

var google_csrf_name = "g_csrf_token";

var cookie = Request.Cookies[google_csrf_name];

if (cookie == null)

return StatusCode((int)HttpStatusCode.BadRequest);

var requestbody = Request.Form[google_csrf_name];

if (requestbody != cookie)

return StatusCode((int)HttpStatusCode.BadRequest);

var idtoken = Request.Form["credential"];

GoogleJsonWebSignature.Payload payload = await GoogleJsonWebSignature.ValidateAsync(idtoken).ConfigureAwait(false);

TempData["name"] = payload.Name;

TempData["email"] = payload.Email;

return Json(TempData);

}


Now, thought this will make me authenticated in my app but its not. Only getting info from google server. I want to be able to add the info to my users table in DB. Any ideas?

Raymond Raymond

Raymond access_time 2 years ago link more_vert

With the information provided, you can use Identity classes to register the user if not exists or add the login to external logins table. In the article, I’ve provided some examples about how to implement this:


var user = new ApplicationUser { UserName = payload.Email, Email = payload.Email, FirstName = payload.GivenName, LastName = payload.FamilyName, IsEnabled = true, EmailConfirmed = true, DateRegister = DateTime.Now };
		await _userManager.CreateAsync(user).ConfigureAwait(false);
		// Add external Google login
		await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerKey, provider)).ConfigureAwait(false);
		// Sign-in the user
        await _signInManager.SignInAsync(user, isPersistent: false).ConfigureAwait(false);

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

sorry for being a starter, how to access identity classes? I put your code and got lots of errors.

Raymond Raymond

Raymond access_time 2 years ago link more_vert

Hi Rami, 

It's not easy to discuss about Identity framework in a comment as it covers many content. I suggest you following this article Introduction to Identity on ASP.NET Core | Microsoft Learn to understand the basics of Identity in ASP.NET Core.


RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

I did the following:

var user = new IdentityUser { UserName = payload.Email, Email = payload.Email };

await _userManager.CreateAsync(user).ConfigureAwait(false);

return RedirectToAction("Index","Home");


I get error:

Severity Code Description Project File Line Suppression State

Error CS1503 Argument 1: cannot convert from 'Microsoft.AspNetCore.Identity.IdentityUser' to 'Microsoft.PowerBI.Api.Models.User'


Raymond Raymond

Raymond access_time 2 years ago link more_vert

What is the type of _userManager? I am guessing you are referencing a wrong type. The type of user manager in Identity should be: UserManager<TUser> Class (Microsoft.AspNetCore.Identity) | Microsoft Learn

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

Thanks alot for helping me out. I finally managed to do it.


private readonly SignInManager<IdentityUser> _signManager;

private readonly UserManager<IdentityUser> _userManager;

public HomeController(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signManager)

{

_userManager = userManager;

_signManager = signManager;

}


and in my function:


var user = new IdentityUser { UserName = payload.Email, Email = payload.Email };

await _userManager.CreateAsync(user).ConfigureAwait(false);

await _signManager.SignInAsync(user, isPersistent: false).ConfigureAwait(false);

return RedirectToAction("Index","Category");

Raymond Raymond

Raymond access_time 2 years ago link more_vert

You are welcome. I am glad it now works for you.

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

Hi Again Raymond,


I came up with a question. Is there a way to get the profile photo from google using the payload?

Raymond Raymond

Raymond access_time 2 years ago link more_vert

Yes, photo is included in the response JWT token payload:

https://cloud.google.com/dotnet/docs/reference/Google.Apis/latest/Google.Apis.Auth.GoogleJsonWebSignature.Payload#Google_Apis_Auth_GoogleJsonWebSignature_Payload_Picture


GoogleJsonWebSignature.Payload.Picture



RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

Hi,


I managed to save the path in the DB but now in my sidebar how to get the path from the db into the img field below:


<img class="profile-pic" src="~/profile2.jpg" />
 @* change based on login *@ 
 @if (!SignInManager.IsSignedIn(User))
 {
     <div class="profile-wrapper">
         <img class="profile-pic" src="~/profile.jpg" />
     
         <div class="titles d-flex flex-column ps-3">
             <h6 class="mb-0">Expense Tracher</h6>
             <span class="text-muted">Guest</span>
             <a href="/">Please Login</a>
         </div>
     </div>
 }
 else
 {
     
     <div class="profile-wrapper">
         <img class="profile-pic" src="~/profile2.jpg" />

         <div class="titles d-flex flex-column ps-3">
             <h6 class="mb-0">Expense Tracher</h6>
             <span class="text-muted">Logged In</span>
         </div>
     </div>
 }
Raymond Raymond

Raymond access_time 2 years ago link more_vert

Hi Rami, depends on where you save the photo attribute to, you just need to query it from the store. You can use entity framework for this. 

RT Rami Taher Wahdan

Rami Taher access_time 2 years ago link more_vert

its saved in the default user identity table. I have the below code with no errors but no photo shown.


<div class="profile-wrapper">

<img src="@User.FindFirst("PictureSource")?.Value" class="profile" />

<div class="titles d-flex flex-column ps-3">

<h6 class="mb-0">Expense Tracher</h6>

<span class="text-muted">Logged In</span>

</div>

</div>

Raymond Raymond

Raymond access_time 2 years ago link more_vert

Hi Rami,

You can use UserManager class in Identity to retrieve the user information defined in your own ApplicationUser class.

UserManager<TUser>.GetUserAsync(ClaimsPrincipal) Method (Microsoft.AspNetCore.Identity) | Microsoft Learn

For parameter principal it is available in Razor page or MVC Controller class directly after the user is authenticated.

Please log in or register to comment.

account_circle Log in person_add Register

Log in with external accounts