Raymond Raymond

ASP.NET Core - Implement Google One Tap Sign In

event 2021-05-18 visibility 1,465 comment 26 insights toc
more_vert
insights Stats
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
Raymond Raymond #1873 access_time 8 months ago 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.

format_quote

person Rami Taher access_time 8 months ago

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>

RT Rami Taher Wahdan #1872 access_time 8 months ago 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>

format_quote

person Raymond access_time 8 months ago

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. 

Raymond Raymond #1871 access_time 8 months ago 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. 

format_quote

person Rami Taher access_time 8 months ago

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>
 }
RT Rami Taher Wahdan #1870 access_time 8 months ago 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>
 }
format_quote

person Raymond access_time 8 months ago

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



Raymond Raymond #1869 access_time 8 months ago 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



format_quote

person Rami Taher access_time 8 months ago

Hi Again Raymond,


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

RT Rami Taher Wahdan #1868 access_time 8 months ago 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?

format_quote

person Raymond access_time 7 months ago

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

Raymond Raymond #1865 access_time 7 months ago more_vert

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

format_quote

person Rami Taher access_time 8 months ago

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");

RT Rami Taher Wahdan #1864 access_time 8 months ago 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");

format_quote

person Raymond access_time 8 months ago

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

Raymond Raymond #1862 access_time 8 months ago 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

format_quote

person Rami Taher access_time 8 months ago

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'


RT Rami Taher Wahdan #1860 access_time 8 months ago 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'


format_quote

person Raymond access_time 8 months ago

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.


Please log in or register to comment.

account_circle Log in person_add Register

Log in with external accounts