Jan 21

Written by: Michael Washington
1/21/2019 6:43 PM  RssIcon

You can log users into your server side Blazor (Razor Components) application using Google authentication.

Server-side Blazor (also known as Razor Components) provides options for deeper integration between the ‘client side’ and ‘server side’ code because the ‘client side’ code is processed server-side. With server-side Blazor, we end up using less code, and things are a lot less complex because we can trust that the end-user was not able to alter or hack the ‘client side’ code.

If the ‘client side’ code says a user is who they say they are, we can trust them. Otherwise, we have to always make a ‘round trip’ to the server, before permitting operations or returning sensitive data. The ‘round trip’ is still being made (because the ‘client site’ code is being generated ‘server side’), but, our code structure is now cleaner and more streamlined.

Note: This example works without using the Google+ API that Google started to shut down. This sample incorporates the work-around that Microsoft is in the process of rolling out officially. See this GitHub issue for more information.

Note: Most examples, showing Google authentication in an .Net Core application, use the SignInManager. The problem is, the SignInManager requires a data store. This example does not.

 

The Application

image

Initially the user clicks the Login button.

image

They are taken to the Google Login page where they enter their Google username and password.

Note: If the user is already logged into Google, they will be automatically logged in without the need to go through this step.

image

They are returned to the application.

Their name, and if they have an Avatar, will display.

The Logout button will also appear. Clicking the Logout button will log the user out of the application.

 

Create The Application

image

Open Visual Studio 2017.

image

Select File then New then Project.

image

Select ASP.NET Core Web Application.

Name the project BlazorGmail.

image

Select Server-side Blazor.

 

Configure SSL

image

To use Google Authentication for logging users in, the application should run using SSL (https),

To enable this, double-click on the Properties node in the server project (BlazorGmail.Server).

image

  1. Select the Debug tab
  2. Click Enable SSL
  3. Click the Copy button
  4. Paste the SSL URL in the App URL box (this will start the application in SSL mode when debugging)

 

image

Click the Save button, to save the changes.

Also, save the App URL, you will need it in a later step.

 

Add NuGet Package

image

Right-click on the solution node in the Solution Explorer, and select Manage NuGet Packages for Solution.

image

Install:

Microsoft.AspNetCore.Http

Ensure you install at least version 2.1.1 (or higher)

 

Set Up the Google Application

image

You need to create a Google API Console project to obtain a client ID.

Go to:

https://developers.google.com/identity/sign-in/web/devconsole-project

Sign in with your Google account and click CONFIGURE A PROJECT.

image

Give the project a name.

image

Fill in the information for the remaining screens

image

Select Other for OAuth client.

Click the CREATE button.

image

Copy and save the Client ID and Client Secret and click the DONE button.

Note: You can return to the configuration at anytime by going to:

https://console.developers.google.com

 

Configure the Authentication Pipeline

image

Add a appsettings.json file to the server project (BlazorGmail.Server), using the following code:

 

{
  "Google": {
    "Instance": "https://accounts.google.com/o/oauth2/v2/auth",
    "ClientId": "{{{ YOUR APP ID }}}",
    "ClientSecret": "{{{ YOUR CLIENT SECRET }}}",
    "CallbackPath": "/signin-google"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

 

Replace  {{{ YOUR APP ID }}} with the Client ID you saved earlier. 

Replace  {{{ YOUR CLIENT SECRET }}} with the Client Secret you saved earlier.

 

image

In the server project (BlazorGmail.Server), open the Startup.cs file.

Add the following using statements to the top of the file:

 
using System.Net.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

 

Add the following to the Startup class:

 

        // To hold the values from the appsettings.json file
        public IConfiguration Configuration { get; }
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

 

Add the following to the bottom of public void ConfigureServices(IServiceCollection services) method:

 

        // ***********************************************
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie();
        // Fix from:
        // https://github.com/aspnet/AspNetCore/issues/6069#issuecomment-451124466
        services.AddAuthentication().AddGoogle(options =>
        {
            options.ClientId = Configuration["Google:ClientId"];
            options.ClientSecret = Configuration["Google:ClientSecret"];
            options.CallbackPath = "/signin-google";
            options.UserInformationEndpoint = "https://www.googleapis.com/oauth2/v2/userinfo";
            options.ClaimActions.Clear();
            options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
            options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
            options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
            options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
            options.ClaimActions.MapJsonKey("urn:google:profile", "link");
            options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
            options.ClaimActions.MapJsonKey("urn:google:image", "picture");
        });
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential
            // cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });         
        services.AddMvc(options => { })
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        // From: https://github.com/aspnet/Blazor/issues/1554
        // Adds HttpContextAccessor
        // Used to determine if a user is logged in
        // and what their username is
        services.AddHttpContextAccessor();
        services.AddScoped<HttpContextAccessor>();
        // Required for HttpClient support in the Blazor Client project
        services.AddHttpClient();
        services.AddScoped<HttpClient>();
        // ***********************************************

 

Add the following to the bottom of the public void Configure(IApplicationBuilder app, IHostingEnvironment env) method (but, before the app.UseServerSideBlazor<App.Startup>() line):

 

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();
        app.UseAuthentication();
        app.UseMvc(routes =>
        {
            // Allows Blazor Client project code to call Blazor
            // Server project pages
            routes.MapRoute(name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });

 

image

Save the file.

Rebuild the Solution.

It should build without errors.

Note: You can get more information about Google Authentication configuration at the following link: https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-2.2

 

Create Login and Logout Pages

image

In the server project, create a Pages folder, and add the following pages using the following code:

 

Login.cshtml

 

@page
@model BlazorGmail.Server.Pages.LoginModel
@{
    ViewData["Title"] = "Log in";
}
<h2>Login</h2>


 

Login.cshtml.cs

 

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace BlazorGmail.Server.Pages
{
    [AllowAnonymous]
    public class LoginModel : PageModel
    {
        public IActionResult OnGetAsync(string returnUrl = null)
        {
            string provider = "Google";
            // Request a redirect to the external login provider.
            var authenticationProperties = new AuthenticationProperties
            {
                RedirectUri = Url.Page(
                    "./Login", 
                    pageHandler: "Callback", 
                    values: new { returnUrl }),
            };
            return new ChallengeResult(provider, authenticationProperties);
        }
        public async Task<IActionResult> OnGetCallbackAsync(
            string returnUrl = null, string remoteError = null)
        {
            // Get the information about the user 
            // from the external login provider
            var GoogleUser = this.User.Identities.FirstOrDefault();
            if (GoogleUser.IsAuthenticated)
            {
                var authProperties = new AuthenticationProperties
                {
                    IsPersistent = true,
                    RedirectUri = this.Request.Host.Value
                };
                await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(GoogleUser),
                authProperties);
            }
            return LocalRedirect("/");
        }
    }
}

Logout.cshtml

 

@page
@model BlazorGmail.Server.Pages.LogoutModel
@{
    ViewData["Title"] = "Logout";
}
<h2>Logout</h2>


 

Logout.cshtml.cs

 

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
using System.Threading.Tasks;
namespace BlazorGmail.Server.Pages
{
    public class LogoutModel : PageModel
    {
        public string ReturnUrl { get; private set; }
        public async Task<IActionResult> OnGetAsync(
            string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            // Clear the existing external cookie
            try
            {
                await HttpContext
                    .SignOutAsync(
                    CookieAuthenticationDefaults
                    .AuthenticationScheme);
            }
            catch (Exception ex)
            {
                string error = ex.Message;
            }
            return LocalRedirect("/");
        }
    }
}

 

Create Login Control

image

In the client project (BlazorGmail.App), Add LoginControl.cshtml using the following code:

 

@using System.Security.Claims
@using Microsoft.AspNetCore.Http
@using BlazorGmail.App.Shared
@page "/login"
@inject IHttpContextAccessor _httpContextAccessor
@inject HttpClient Http
@if (User.Identity.Name != null)
{
    <img src="@Avatar" />
    <b>You are logged in as: @GivenName @Surname</b>
    <a class="ml-md-auto btn btn-primary"
       href="/Logout"
       target="_top">Logout</a>
}
else
{
    <a class="ml-md-auto btn btn-primary"
       href="/Login"
       target="_top">Login</a>
}
@functions {
    private ClaimsPrincipal User;
    private string GivenName;
    private string Surname;
    private string Avatar;
    protected override void OnInit()
    {
        base.OnInit();
        try
        {
            // Set the user to determine if they are logged in
            User = _httpContextAccessor.HttpContext.User;
            // Try to get the GivenName
            var givenName =
                _httpContextAccessor.HttpContext.User
                .FindFirst(ClaimTypes.GivenName);
            if (givenName != null)
            {
                GivenName = givenName.Value;
            }
            else
            {
                GivenName = User.Identity.Name;
            }
            // Try to get the Surname
            var surname =
                _httpContextAccessor.HttpContext.User
                .FindFirst(ClaimTypes.Surname);
            if (surname != null)
            {
                Surname = surname.Value;
            }
            else
            {
                Surname = "";
            }
            // Try to get Avatar
            var avatar =
            _httpContextAccessor.HttpContext.User
            .FindFirst("urn:google:image");
            if (avatar != null)
            {
                Avatar = avatar.Value;
            }
            else
            {
                Avatar = "";
            }
        }
        catch { }
    }
}

 

image

To display the Login control, open Shared/MainLayout.cshtml and change all the code to the following:

 

@inherits BlazorLayoutComponent
<div class="sidebar">
    <NavMenu />
</div>
<div class="main">
    <div class="top-row px-4">
        <!-- ******************************** -->
        <LoginControl />
        <!-- ******************************** -->
    </div>
    <div class="content px-4">
        @Body
    </div>
</div>

 

 

Links

Blazor.net

Authentication for serverside Blazor (How to use IHttpContextAccessor)

Blazor Security/Authorization

A Demonstration of Simple Server-side Blazor Cookie Authentication

Google Developers Dashboard

Using OAuth 2.0 to Access Google APIs

External Login Providers in ASP.NET Core

Implementing Google OAuth with Blazor (0.4) and ASPNET Core 2.1.1

Google external login setup in ASP.NET Core

 

Download

The project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

You must have Visual Studio 2017 (or higher) installed to run the code.


Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation