티스토리 뷰

C#

C#: Blazor Server에 Google OAuth 연동

개태형님 2021. 12. 21. 00:41

Google의 서비스를 이용하기 위해 OAuth를 연동하는 법은 많이 나와 있지만..

Blazor Server에 적용하는 법은 자료가 많이 없다.

정답을 찾기보다는 OAuth 매커니즘 자체를 이해하고, Google의 라이브러리가 지원하는지를 확인하는 게 빠르다.

 

OAuth는 표준기술이기 때문에 큰 맥락의 매커니즘은 동일하다.

하지만 이 글에서는 Google OAuth2를 기준으로 설명한다.

 

먼저, 간략한 프로세스는 아래와 같다.

  1. 특정 앱에 대한 접근 권한과 식별을 위해 ClientID와 ClientSecret 취득
  2. OAuth 서버에 ClientID와 ClientSecret으로 Code 취득
  3. Code와 ClientSecret으로 실제 인증과 권한 획득에 사용할 AccessToken과 RefreshToken 취득
  4. AccessToken의 만료기한은 3600초(1시간) 이므로, 이후 자동으로 재 취득을 위해 RefreshToken이 필요함
  5. 이후 해당 서버에서 제공하는 서비스는 계정정보가 아닌 Token을 이용하여 접근함

 

자.. 이제 위 프로세스에 맞는 Google OAuth for Blazor 구현을 해보자.

"Google GCP에서 앱 생성 >  OAuth 동의화면 구성 >  OAuth ID 생성 > Redirect Uri 지정" 에 해당하는 일련의 과정은 다른 블로그에 너무나 방대한 자료가 있기에 생략 한다.

(따로 정리하기에는 너무 길고, 5분만 구글링 해봐도 자세히 나온다.)

 

 

이 글에서는 .NET 5 Blazor Server에서 Google OAuth를 이용한 로그인과 Google Calendar에 접근을 구현한다.

먼저 NuGet에서 아래 2가지 패키지를 설치 한다.

 

1. Microsoft.AspNetCore.Authentication.Google

- .NET Core에서 지원하는 인증 패키지에 Google 관련 모듈이 들어있는 패키지

- 여기서는 .NET 5를 기준으로 하기때문에, 5.xxx 버전을 받는다.

 

2. Google.Apis.Calendar.v3

- Google Calendar를 이용하기 위한 .NET용 Wrapper

 

자, 그럼 GCP에서 발급받은 ClientID와 ClientSecret을 appsetting.json 파일에 추가한다.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Google": {
    "Id": "GCP에서 발급 받은 ClientID",
    "Secret": "GCP에서 발급 받은 ClientSecret"
  },
  "AllowedHosts": "*"
}

 

Google 인증 완료 후 정보를 저장할 모델을 추가한다.

    public class GoogleUser
    {
        public string ID { get; set; }

        public string AccessToken { get; set; }

        public string RefreshToken { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string Email { get; set; }

        public string ImageUrl { get; set; }
    }
    
    public static class GoogleSecret
    {
        public static string ClientID { get; set; }

        public static string ClientSecret { get; set; }
    }

 

Startup.cs 파일을 열고 Google 인증 관련 셋팅을 해준다.

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor()
                    .AddHubOptions(options => options.MaximumReceiveMessageSize = null);

            // Google 인증
            services.AddAuthentication(Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddCookie(opt =>
                    {
                        opt.Cookie.Name = "GoogleOAuth";
                    })
                    .AddGoogle(opt =>
                    {
                        // appsetting.json에서 불러온 값.
                        string clientID = Configuration["Google:Id"];
                        string clientSecret = Configuration["Google:Secret"];
                        GoogleSecret.ClientID = clientID;
                        GoogleSecret.ClientSecret = clientSecret;

                        opt.ClientId = clientID;
                        opt.ClientSecret = clientSecret;

                        // OAuth 인증 시 어떤 권한을 포함할지를 정하는 Scope.
                        // Scope URL은 Google에서 정의된 값을 넣어준다.
                        // 아래는 Google 계정 정보 및 Calendar에 대한 권한을 요청하고 있다.
                        opt.Scope.Add("openid");
                        opt.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
                        opt.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
                        opt.Scope.Add("https://www.googleapis.com/auth/calendar");

                        // AccessType에 대한 정의는 찾아보면 자세히 나와있다.
                        // default값은 online이며, RefreshToken을 얻기위해서는 offline으로 지정해줘야 함.
                        opt.AccessType = "offline";

                        // Code 발급 후 Token 취득까지 해당 Warpper가 처리해준다.
                        // 이 이벤트는 Token까지 취득 후 발생한다.
                        opt.Events.OnCreatingTicket = context =>
                        {
                            var user = new GoogleUser()
                            {
                                ID = context.User.GetProperty("id").GetString(),
                                AccessToken = context.AccessToken,
                                RefreshToken = context.RefreshToken,
                                FirstName = context.User.GetProperty("family_name").GetString(),
                                LastName = context.User.GetProperty("given_name").GetString(),
                                Email = context.User.GetProperty("email").GetString(),
                                ImageUrl = context.User.GetProperty("picture").GetString(),
                            };

                            // 아래 코드처럼 적절한 위치에 GoogleUser 정보를 저장해둔다.
                            var session = Storage.GetSession(user.ID);
                            session.GoogleLogin(user);
                            return Task.CompletedTask;
                        };
                    });
        }

 

역시 Startup.cs에 Blazor의 인증 관련 서비스를 등록한다.

(OAuth를 구현하는 방법은 여러가지가 있지만, 여기서는 Controller를 이용함)

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/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();

            // Google 인증
            // 순서를 바꾸면 안 된다.
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                // Google 인증
                endpoints.MapControllers();

                endpoints.MapBlazorHub();
                endpoints.MapFallbackToPage("/_Host");
            });
        }

 

다음은 로그인과 로그아웃 처리를 위한 Controller를 추가한다.

    // Google 인증
    [Route("/api/auth")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        [HttpGet("google/login")]
        public ActionResult GoogleLogin()
        {
            var properties = new AuthenticationProperties()
            {
                RedirectUri = "login",

                // prompt=consent : 매번 권한 요청 및 승인 시 RefreshToken 획득
                // prompt=login : 최초 권한 승인만 RefreshToken 획득
                Parameters = { { "prompt", "consent" } },
            };
            return Challenge(properties, GoogleDefaults.AuthenticationScheme);
        }

        [HttpGet("google/logout")]
        public async Task<ActionResult> GoogleLogout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return LocalRedirect("~/login");
        }
    }

 

이제 거의다 왔다.

GCP에서 redirect URL을 "https://localhost:포트/signin-google" 이라고 정의했을 것이다.

그러면 우리 Blazor에서 해당 페이지에 대한 접근을 캐치하기 위해 Routing처리를 해주고, 위에 추가한 Controller로 보내줘야 한다.

LoginPage.razor 파일을 추가하고 아래와 같이 작성한다.

@page "/login"
@page "/signin-google"

@inject NavigationManager NavigationManager
@attribute [AllowAnonymous]

<AuthorizeView>
    <Authorized>
        @{ NavigationManager.NavigateTo("index"); }
    </Authorized>
    <NotAuthorized>
        @{ NavigationManager.NavigateTo("api/auth/google/login"); }
    </NotAuthorized>
</AuthorizeView>

 

로그아웃 처리를 위해 LogoutPage.razor 파일을 추가하고 아래와 같이 작성한다.

@page "/logout"

@inject NavigationManager NavigationManager

@{ NavigationManager.NavigateTo("api/auth/google/logout", true); }

 

자 그럼 이제 Blazor Server를 올리고 로그인 페이지로 들어가보자.

아래와 같은 화면이 보일 것이다.

 

계정을 선택하면, 아까 Startup.cs에 추가했던 Calendar관련 Scope에 의해 권한요청 화면으로 넘어간다.

아래화면에서 허용버튼을 누르면, Startup.cs에 정의했던 OnCreatingTicket이벤트가 발생한다.

그리고 우리가 만들었던 GoogleUser모델에 Token과 정보가 저장된다.

 

여기까지 진행 시 Blazor Server에서 제공하는 인증 시스템에 Google 인증이 적용된다.

razor파일 내에서 <AuthorizeView> 태그를 이용하여 어디서든 인증 여부를 확인할 수 있다.

 

그럼 이제 Google Calendar의 정보를 취득 후 저장하기 위한 모델을 추가한다.

    public class GoogleCalendarEvent
    {
        public string CalendarName { get; set; }

        public string EventName { get; set; }

        public DateTime StartDate { get; set; }

        public DateTime EndDate { get; set; }
    }

 

그리고 실제 Google에서 제공하는 서비스를 사용하기 위해 GoogleService.cs 파일을 추가한다.

Google 공식 문서에서는 이미 획득한 Token을 활용한 구현방법에 대해서는 나와있지 않다.

아래 코드를 사용 시 OAuth로 로그인과 함께 취득한 Calendar 접근 권한까지 있는 AccessToken과 RefreshToken으로 CalendarService를 이용할 수 있다.

using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Calendar.v3;
using Google.Apis.Http;
using Google.Apis.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorServer.Service
{
    public partial class GoogleService
    {
        private const string ApplicationName = "TEST.APPLICATION";

        private string _id;
        private string _accessToken;
        private string _refreshToken;

        private CalendarService _calendarService;

        public GoogleService(string id, string accessToken, string refreshToken)
        {
            _id = id;
            _accessToken = accessToken;
            _refreshToken = refreshToken;

            // Credential 생성
            // AccessType을 online으로 지정 시 첫 if문 분기점에서 처리
            // offline으로 지정 시 RefreshToken을 활요하는 else 분기점에서 처리
            IConfigurableHttpClientInitializer credential;
            if (string.IsNullOrWhiteSpace(_refreshToken))
            {
                credential = GoogleCredential.FromAccessToken(_accessToken);
            }
            else
            {
                // Startup.cs에서 취득했던 ClientID와 ClientSecret을 이용함
                // RefreshToken으로 만료된 AccessToken을 재발급 받기 위해서 필요하다.
                var secret = new ClientSecrets()
                {
                    ClientId = GoogleSecret.ClientID,
                    ClientSecret = GoogleSecret.ClientSecret,
                };
                var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer { ClientSecrets = secret });
                var token = new TokenResponse
                {
                    AccessToken = _accessToken,
                    RefreshToken = _refreshToken,
                };
                credential = new UserCredential(flow, _id, token);
            }

            // Service 생성
            _calendarService = new CalendarService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            });
        }

        public async Task<Dictionary<string, List<GoogleCalendarEvent>>> GetCalendarEvents(DateTime minDate, DateTime maxDate)
        {
            var events = new List<GoogleCalendarEvent>();

            // Calendar 목록을 취득
            var calListReq = _calendarService.CalendarList.List();
            calListReq.MaxResults = 100;
            var calList = await calListReq.ExecuteAsync();
            foreach (var cal in calList.Items)
            {
                // 해당 Calendar에 등록된 Event 목록을 취득
                var eventListReq = _calendarService.Events.List(cal.Id);
                eventListReq.SingleEvents = true;
                eventListReq.TimeMin = minDate;
                eventListReq.TimeMax = maxDate;
                eventListReq.MaxResults = 100;
                var eventList = await eventListReq.ExecuteAsync();
                foreach (var evt in eventList.Items)
                {
                    if (evt.Start == null || evt.Status == "cancelled") continue;

                    DateTime start;
                    DateTime end;
                    if (string.IsNullOrEmpty(evt.Start.Date))
                    {
                        start = (DateTime) evt.Start.DateTime;
                        end = (DateTime) evt.End.DateTime;
                    }
                    else
                    {
                        start = Convert.ToDateTime(evt.Start.Date);
                        end = Convert.ToDateTime(evt.End.Date);
                    }

                    // 우리가 만든 모델에 데이터 입력
                    var eventItem = new GoogleCalendarEvent()
                    {
                        CalendarName = evt.Organizer.DisplayName,
                        EventName = evt.Summary,
                        StartDate = start,
                        EndDate = end,
                    };
                    events.Add(eventItem);
                }
            }

            // Event를 Calendar별로 구분하기 위해 Dictionary로 변환
            // Key:Calendar.Name, Value:List<GoogleCalendarEvent>
            var eventsByCalendar = events.GroupBy(o => o.CalendarName).ToDictionary(o => o.Key, o => o.ToList());
            foreach (var calName in eventsByCalendar.Keys)
            {
                eventsByCalendar[calName] = eventsByCalendar[calName].OrderBy(o => o.StartDate).ToList();
            }
            return eventsByCalendar;
        }
    }
}

 

드디어 끝났다.

실제 사용은 아래와 같이 한다.

        // 2021.12.1 ~ 2021.12.31에 해당하는 모든 Calendar의 Event 취득
        private async Task ReloadCalendarEvents()
        {
            // Startup.cs에서 생성한 GoogleUser 취득
            var googleUser = _session.GoogleUser;

            // GoogleUser모델에 저장했던 Token으로 Service 생성
            var google = new GoogleService(googleUser.ID, googleUser.AccessToken, googleUser.RefreshToken);

            var startDate = new DateTime(2021, 12, 1);
            var endDate = new DateTime(2021, 12, 31);
            var eventsByCalendar = await google.GetCalendarEvents(startDate, endDate);
        }

 

이 글에서는 Calendar를 이용하기 위한 예제를 작성하였다.

GCP에는 많은 서비스를 지원하며, 모든 서비스는 OAuth를 기반으로 되어있다.

다른 서비스를 OAuth에 같이 태워서 권한을 획득하고 서비스를 이용하기 위한 추가 구현은 간단하다.

  1. Startup.cs에서 원하는 Google Service의 Scope 추가
  2. NuGet에서 Google에서 제공하는 패키지 설치
  3. GoogleService.cs에서 설치한 패키지를 사용하는 함수 구현
  4. 함수 호출

 

끝.

'C#' 카테고리의 다른 글

C#: CLR, JIT, Assembly 등 기본 개념  (0) 2023.08.27
C#: 나의 Blazor 웹앱을 Electron위에 올려보자  (0) 2022.07.02
C#: Blazor IIS 배포 후 Route 이슈  (2) 2021.12.13
C#: Blazor 전역 로그인 체크  (0) 2021.12.01
C#: Property Copy  (0) 2021.11.28
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday