🌟

【ASP.NET MVC】Google での認証方法

2024/05/30に公開

概要

このガイドでは、ASP.NET MVCアプリケーションでGoogleのOAuth認証を実装する手順について説明します。具体的には、Visual Studio 2022を使用して新しいASP.NET MVCプロジェクトを作成し、Google API ConsoleでOAuthクライアントIDを作成し、認証情報を設定します。そして、GoogleのOAuth認証を使用してユーザーをログインおよびログアウトさせる方法を示します。

コンテキスト

このガイドでは、ASP.NET Core MVCアプリケーションでGoogleのOAuth認証を実装する手順について説明します。具体的には、Visual Studio 2022を使用して新しいASP.NET Core MVCプロジェクトを作成し、Google API ConsoleでOAuthクライアントIDを作成し、認証情報を設定します。そして、GoogleのOAuth認証を使用してユーザーをログインおよびログアウトさせる方法を示します。

対象読者

このガイドは、Windows 11 HomeおよびVisual Studio 2022を使用して行います。ASP.NET Core MVCアプリケーションの開発およびGoogleのOAuth認証の基本的な理解が前提とされています。手順を追って、GoogleのOAuth認証をASP.NET Core MVCプロジェクトに統合する方法を詳しく説明します。

環境

Windows 11 Home
Visutal studio 2022
.NET 8.0

Google API Consoleで新しいプロジェクトを作成


https://console.cloud.google.com/

OAuth同意画面を作成






認証情報を作成→OAuth クライアント IDを作成



Visual Studio2022 で新しいプロジェクトの作成


パッケージマネージャーで下記をインストール

Install-Package Microsoft.AspNetCore.Authentication.Google
Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer

プロジェクト ディレクトリで次のコマンドを実行
OAuthのクライアントIDとシークレットをセット

dotnet user-secrets init
dotnet user-secrets set "Authentication:Google:ClientId" "YOUR_CLIENT_ID"
dotnet user-secrets set "Authentication:Google:ClientSecret" "YOUR_CLIENT_SECRET"

プロジェクトを右クリック→ ユーザーシークレットの管理 からsecrets.jsonに正しく設定されているかを確認

Program.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using WebApplication1.Data;
using WebApplication1.Models;

internal class Program
{
    private static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddControllersWithViews();

        builder.Services.AddDbContext<ApplicationDbContext>(options =>
       options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));


        builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
            options.SignIn.RequireConfirmedAccount = false) // 外部ログインではアカウント確認を求めない
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        // 認証サービスを追加
        builder.Services.AddAuthentication(options =>
        {
            // 既存の認証設定...
        })
        .AddGoogle(googleOptions =>
        {
            // Google API Consoleで取得したクライアントIDとシークレットを使用
            googleOptions.ClientId = builder.Configuration["Authentication:Google:ClientId"];
            googleOptions.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
        });

        var app = builder.Build();

        // Configure the HTTP request pipeline.
        if (!app.Environment.IsDevelopment())
        {
            app.UseExceptionHandler("/Home/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.MapControllerRoute(
            name: "default",
            pattern: "{controller=Account}/{action=GoogleLogin}/{id?}");
        app.Run();
    }
}

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=(YOUR_DB_CONNECTION_STRING)"
  }
}

Views>Shared>_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebApplication1</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/WebApplication1.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">WebApplication1</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                        @if (User.Identity.IsAuthenticated)
                        {
                            <li class="nav-item">
                                <form id="logoutForm" asp-area="" asp-controller="Account" asp-action="LogoutPost" method="post" class="form-inline">
                                    <button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
                                </form>
                            </li>
                        }
                        else
                        {
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-area="" asp-controller="Account" asp-action="GoogleLogin">GoogleLogin</a>
                            </li>
                        }
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2024 - WebApplication1 - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Views>Account>logout.cshtml

<h1>logout</h1>

Models>ApplicationUser.cs

using Microsoft.AspNetCore.Identity;

namespace WebApplication1.Models
{
    public class ApplicationUser : IdentityUser
    {
    }
}

Data>ApplicationDbContext.cs

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using WebApplication1.Models;

namespace WebApplication1.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        // Users DbSet
        public DbSet<ApplicationUser> ApplicationUsers { get; set; }
    }
}

Controllers>HomeController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        [Authorize]
        public IActionResult Index()
        {
            return View();
        }

        [Authorize]
        public IActionResult Privacy()
        {
            return View();
        }

        [AllowAnonymous]
        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Controllers>AccountController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
using WebApplication1.Models;
using System.Diagnostics;
using System.Security.Claims;
using WebApplication1.Data;
using Microsoft.EntityFrameworkCore;

namespace WebApplication1.Controllers
{
    public class AccountController : Controller
    {
        private readonly SignInManager<ApplicationUser> _signInManager; // サインイン管理
        private readonly UserManager<ApplicationUser> _userManager; // ユーザー管理
        private readonly ApplicationDbContext _db; // データベースコンテキスト

        // コンストラクター
        public AccountController(SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager, ApplicationDbContext db)
        {
            _signInManager = signInManager;
            _userManager = userManager;
            _db = db;
        }

        /// <summary>
        /// Googleログインアクション
        /// </summary>
        /// <returns>Googleの認証を開始するアクション結果</returns>
        public IActionResult GoogleLogin()
        {
            // Googleの認証プロパティを構成してチャレンジする
            var redirectUrl = Url.Action("GoogleResponse", "Account", null, Request.Scheme); // 認証後のリダイレクトURL
            var properties = _signInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl); // Google認証のプロパティを構成
            return new ChallengeResult("Google", properties); // Google認証を開始
        }

        /// <summary>
        /// Googleログイン応答アクション
        /// </summary>
        /// <returns>Googleログインの応答を処理するアクション結果</returns>
        public async Task<IActionResult> GoogleResponse()
        {
            // 外部ログイン情報を取得する
            var info = await _signInManager.GetExternalLoginInfoAsync();
            if (info == null)
            {
                // エラーページを表示する
                return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
            }

            // 外部ログイン情報を使ってサインインを試みる
            var signInResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);

            // サインインに成功した場合
            if (signInResult.Succeeded)
            {
                // ホームページにリダイレクト
                return RedirectToAction("Index", "Home");
            }
            else
            {
                // サインインに失敗した場合、ユーザー情報を取得
                var email = info.Principal.FindFirstValue(ClaimTypes.Email);

                if (email != null)
                {
                    // Emailでユーザーをデータベースから探す
                    var user = await _userManager.FindByEmailAsync(email);

                    if (user == null)
                    {
                        // 新規ユーザーの場合、ユーザーを作成
                        user = new ApplicationUser
                        {
                            UserName = email, // ユーザー名をEmailから設定
                            Email = email // Emailを設定
                        };

                        var result = await _userManager.CreateAsync(user);
                        if (!result.Succeeded)
                        {
                            // ユーザー作成に失敗した場合、エラーページを表示
                            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, Message = "User creation failed" });
                        }
                    }

                    // 外部ログイン情報をユーザーに関連付ける
                    var addLoginResult = await _userManager.AddLoginAsync(user, info);
                    if (!addLoginResult.Succeeded)
                    {
                        // 外部ログイン情報の関連付けに失敗した場合、エラーページを表示
                        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, Message = "Adding external login failed" });
                    }

                    // ユーザーをサインインさせる
                    await _signInManager.SignInAsync(user, isPersistent: false);

                    // ホームページにリダイレクト
                    return RedirectToAction("Index", "Home");
                }
            }

            // エラーページを表示する
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, Message = "External login failed" });
        }

        /// <summary>
        /// アプリケーション内のすべての認証情報をクリアし、ユーザーをサインアウトします。
        /// また、外部認証スキーム(ここではGoogleの認証)からもユーザーをサインアウトし、
        /// ログアウト後に "Logout" アクションにリダイレクトします。
        /// </summary>
        /// <returns>ログアウト後に "Logout" アクションにリダイレクトするアクション結果</returns>
        [HttpPost(Name = "LogoutPost")]
        public async Task<IActionResult> LogoutPost()
        {
            // アプリケーション内のすべての認証情報をクリアし、ユーザーをサインアウト
            await _signInManager.SignOutAsync();
            // 外部認証スキーム(ここではGoogleの認証)からユーザーをサインアウト
            await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

            // ログアウト後に"Logout"アクションを呼び出す
            return RedirectToAction("Logout");
        }

        /// <summary>
        /// ログアウト後のビューを表示するアクション
        /// </summary>
        /// <returns>ログアウト後のビュー</returns>
        [HttpGet(Name = "Logout")]
        public IActionResult Logout()
        {
            return View();
        }

    }
}

パッケージマネージャーコンソールを使用してマイグレーションを実行する

Add-Migration InitialCreate
Update-Database

実行結果

googleアカウントでログイン

ログイン後の画面

ログアウト後の画面

googleアカウント サードパーティ製のアプリとサービス

参考

https://qiita.com/te-k/items/6665aac9183fcf7bdba5
https://developers.google.com/workspace/guides/create-credentials?hl=ja#oauth-client-id
https://shuhelohelo.hatenablog.com/entry/2019/12/29/115709
https://learn.microsoft.com/ja-jp/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-8.0
https://cloud.google.com/identity-platform/docs/web/google?hl=ja
https://support.google.com/accounts/answer/14012355?hl=ja#zippy=%2Cgoogle-アカウントへのサードパーティのアクセス権を削除するにはどうすればよいですか

Discussion