Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

오류 기록 - MICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER

이번 글은 단순히 다음의 글을 실습하는 과정에서 겪은 오류 기록입니다.

MICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER
; https://adrianszen.com/2019/06/16/microsoft-graph-how-to-implement-iauthenticationprovider/

결론 먼저 말하면, 해당 예제는 정상적으로 동작하지 않는데 아마도 제가 아직 Microsoft Identity Platform에 익숙하지 않아서 그런 것 같습니다. ^^ 위의 예제는 ClientSecrets를 이용하면서도 사용자의 로그인 과정을 필요로 하지 않는 코드만 포함하고 있습니다. 따라서 기존 예제들과는 달리 Access Token이 사용자 문맥을 포함하지 않는 인증을 대표하는 것 같은데 딱히 이것이 아직 어떻게 쓰일 수 있는지 잘 모르겠습니다. (혹시, 위의 예제가 어떤 식으로 활용되는지 아시는 분은 덧글 부탁드립니다.)

그래도, 막상 (성공은 못했지만) 실습하면서 겪은 사항들을 나중에라도 한 번 더 살펴봤을 때 도움이 되도록 기록을 남깁니다.




우선, App 등록을 해야 하는데 그 과정은 아래의 글에서 이미 설명했습니다.

C# - Azure AD 인증을 지원하는 ASP.NET Core/5+ 웹 애플리케이션 예제 구성
; https://www.sysnet.pe.kr/2/0/12614

여기서는 "hMICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER" 글에 따라 다음의 설정이 되었다고 가정하겠습니다.

name: test-auth-webapp
Certificates & secrets: 1개 생성
API permissions: Directry.Read.All, User.Read.All

그다음 간단하게 ASP.NET Core 프로젝트를 하나 만들고 다음의 3개 패키지를 추가합니다.

Install-Package Microsoft.Graph
Install-Package Microsoft.Graph.Core
Install-Package Microsoft.Identity.Client

이후 기본 생성된 코드에 GraphServiceClient를 이용해 Microsoft Graph 쿼리를 발생시키는 코드를 추가합니다.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        // ..[생략]...

        private readonly ILogger<WeatherForecastController> _logger;
        private readonly IAuthenticationProvider _auth;

        public WeatherForecastController(ILogger<WeatherForecastController> logger, IAuthenticationProvider auth)
        {
            _logger = logger;
            _auth = auth;
        }

        [HttpGet]
        public async Task<IEnumerable<WeatherForecast>> Get()
        {
            // ..[생략]...

            GraphServiceClient graphClient = new GraphServiceClient("https://graph.microsoft.com/v1.0", _auth);
            var me = await graphClient.Me.Request().GetAsync();

            System.Diagnostics.Trace.WriteLine(me.DisplayName);

            return result;
        }
    }
}

위에서 IAuthenticationProvider를 주입했는데요, 이를 위한 구현 클래스는 다음과 같고,

using Microsoft.Graph;
using Microsoft.Identity.Client;
using System.Net.Http;
using System.Threading.Tasks;

namespace WebApplication1
{
    public class AuthenticationProvider : IAuthenticationProvider
    {
        private readonly string clientId;
        private readonly string clientSecret;
        private readonly string[] appScopes;
        private readonly string tenantId;

        public AuthenticationProvider(string clientId, string clientSecret, string[] appScopes, string tenantId)
        {
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.appScopes = appScopes;
            this.tenantId = tenantId;
        }

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            // Client credential flows in MSAL.NET
            // ; https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-credential-flows
            var clientApplication = ConfidentialClientApplicationBuilder.Create(this.clientId)
                .WithClientSecret(this.clientSecret)
                .WithTenantId(this.tenantId)
                .Build();

            var result = await clientApplication.AcquireTokenForClient(this.appScopes).ExecuteAsync();

            if (request.Headers.Contains("Authorization") == false)
            {
                request.Headers.Add("Authorization", result.CreateAuthorizationHeader());
            }
        }
    }
}

이것을 Startup.cs 파일의 ConfigureServices에서 다음과 같이 추가를 하면 됩니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    var scopes = new string[]
    {
        "api://...[app_client_id].../.default"
    };

    services.AddSingleton(typeof(IAuthenticationProvider), new AuthenticationProvider)
        (
            "...[app_client_id]...",
            "...[client_secret_value]...",
            scopes,
            "...[tenant_id]..."
        ));
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




이렇게 코딩하고 실행해 보면, AuthenticateRequestAsync의 AcquireTokenForClient 호출에서는 정상적으로 Access Token을 받아옵니다. 하지만, 해당 token이 설정된 GraphServiceClient를 사용하는 코드에서,

GraphServiceClient graphClient = new GraphServiceClient("https://graph.microsoft.com/v1.0", _auth);

var sampleRequest = await graphClient.Users.Request().GetAsync(); // 예외 발생

다음과 같은 예외가 발생합니다.

Microsoft.Graph.ServiceException
  HResult=0x80131500
  Message=Code: InvalidAuthenticationToken
Message: Access token validation failure. Invalid audience.
Inner error:
    AdditionalData:
    date: ...[생략]...
    request-id: ...[생략]...
    client-request-id: ...[생략]...
ClientRequestId: ...[생략]...

  Source=Microsoft.Graph.Core
  StackTrace:
   at Microsoft.Graph.HttpProvider.<SendAsync>d__18.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs:line 56
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 173
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 150
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 551
   at Microsoft.Graph.BaseRequest.<SendRequestAsync>d__40.MoveNext()

  This exception was originally thrown at this call stack:
    [External Code]
    System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in ExceptionDispatchInfo.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter.GetResult() in TaskAwaiter.cs
    [External Code]

실제로 해당 Access Token을 postman으로 테스트를 해보면 동일한 오류 메시지를 받게 됩니다. 그리고 이때의 AcquireTokenForClient의 반환값을 보면,

        AccessToken "ey...[생략]...Ow"    string
        Account null    Microsoft.Identity.Client.IAccount
+       AuthenticationResultMetadata    {Microsoft.Identity.Client.AuthenticationResultMetadata}    Microsoft.Identity.Client.AuthenticationResultMetadata
+       ClaimsPrincipal null    System.Security.Claims.ClaimsPrincipal
+       CorrelationId   {f0...[생략]...63}    System.Guid
+       ExpiresOn   {2021-...[생략]...}   System.DateTimeOffset
+       ExtendedExpiresOn   {2021-...[생략]...}   System.DateTimeOffset
        IdToken null    string
        IsExtendedLifeTimeToken false   bool
-       Scopes  Count = 0x00000001  System.Collections.Generic.IEnumerable<string> {System.Collections.Generic.HashSet<string>}
        [0] "api://32...[생략]...62/.default" string
+       Raw View        
        TenantId    null    string
        TokenType   "Bearer"    string
        UniqueId    null    string
+       Non-Public members      

정상적인 동작을 하던 authResult와는 다른 결과를 갖습니다.




위의 코드 접근을 그대로 따르는 마이크로소프트의 다른 예제를 보면,

Azure-Samples/active-directory-dotnetcore-daemon-v2
; https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2

약간 환경 값이 다릅니다.

// scopes = new string [] { "https://graph.microsoft.com/.default" };

result = await app.AcquireTokenForClient(scopes)
                    .ExecuteAsync();

"MICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER" 글의 전체 예제 코드가 공개되지 않아 scope 값이 "api://{clientId}/.default" 형식이라고 생각했는데 마이크로소프트의 예제에서는 "https://graph.microsoft.com/.default"로 사용하고 있는 것입니다.

하지만, 저렇게 적용을 해도 (역시 Access Token까지는 받아오지만) Microsoft Graph API를 수행할 때 (두 가지 예제 모두) 이런 오류가 발생합니다.

Microsoft.Graph.ServiceException
  HResult=0x80131500
  Message=Code: Authorization_RequestDenied
Message: Insufficient privileges to complete the operation.
Inner error:
    AdditionalData:
    date: ...[생략]...
    request-id: ...[생략]...
    client-request-id: ...[생략]...
ClientRequestId: ...[생략]...

  Source=Microsoft.Graph.Core
  StackTrace:
   at Microsoft.Graph.HttpProvider.<SendAsync>d__18.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs:line 56
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 173
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 150
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 551
   at Microsoft.Graph.BaseRequest.<SendRequestAsync>d__40.MoveNext()

  This exception was originally thrown at this call stack:
    [External Code]
    System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in ExceptionDispatchInfo.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter.GetResult() in TaskAwaiter.cs
    [External Code]

마이크로소프트의 예제 설명을 보면,

This sample application shows how to use the Microsoft identity platform endpoint to access the data of Microsoft business customers

라고 나오는데, 아무래도 제가 보유하고 있는 Azure Active Directory의 tenant 유형이 아닌 Office 365 같은 식의 tenant에서 발급받은 ClientSecret 값이어야 하지 않나 싶습니다. 이에 대한 힌트를 다음의 글에서 볼 수 있는데요,

Call MS Graph APIs from ASP.NET Core 3.1
; https://cmatskas.com/call-ms-graph-apis-from-asp-net-core-3-1/

"No authentication", "Individual User Accounts", "Work or School Accounts", "Windows Authentication" 4가지인데 아마도 제가 한 환경은 "Individual"인 듯싶고, ... 암튼 이번엔 여기까지 정리하고 다음에 또 시간 나면 접근해 봐야겠습니다. ^^




다음과 같은 오류가 발생한다면?

Microsoft.Identity.Client.MsalServiceException
  HResult=0x80131500
  Message=A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details.  Original exception: AADSTS7000215: Invalid client secret is provided.
Trace ID: ...[생략]...
Correlation ID: ...[생략]...
Timestamp: ...[생략]...
  Source=Microsoft.Identity.Client
  StackTrace:
   at Microsoft.Identity.Client.OAuth2.OAuth2Client.ThrowServerException(HttpResponse response, RequestContext requestContext)
   at Microsoft.Identity.Client.OAuth2.OAuth2Client.CreateResponse[T](HttpResponse response, RequestContext requestContext)
   at Microsoft.Identity.Client.OAuth2.OAuth2Client.<ExecuteRequestAsync>d__11`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs:line 56
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 173
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 150
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 551
   at Microsoft.Identity.Client.OAuth2.OAuth2Client.<GetTokenAsync>d__10.MoveNext()

  This exception was originally thrown at this call stack:
    [External Code]
    System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in ExceptionDispatchInfo.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter.GetResult() in TaskAwaiter.cs
    [External Code]

메시지("Invalid client secret is provided.")에 나오듯이, ClientSecret 값을 잘 못 전달한 것입니다. (아마도 테스트를 이거저거 하면서 저처럼 혼동하면 저 오류를 보게 될 것입니다. ^^)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 8/1/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12891정성태12/23/2021633스크립트: 38. 파이썬 - uwsgi의 --master 옵션
12890정성태12/23/2021672VC++: 152. Golang - (문자가 아닌) 바이트 위치를 반환하는 strings.IndexRune 함수
12889정성태12/22/20211060.NET Framework: 1123. C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법파일 다운로드1
12888정성태12/21/2021858.NET Framework: 1122. C# - ImageCodecInfo 사용 시 System.Drawing.Image와 System.Drawing.Bitmap에 따른 Save 성능 차이파일 다운로드1
12887정성태12/21/2021892오류 유형: 777. OpenCVSharp4를 사용한 프로그램 실행 시 "The type initializer for 'OpenCvSharp.Internal.NativeMethods' threw an exception." 예외 발생
12886정성태12/20/2021727스크립트: 37. 파이썬 - uwsgi의 --enable-threads 옵션
12885정성태12/20/2021482오류 유형: 776. uwsgi-plugin-python3 환경에서 MySQLdb 사용 환경
12884정성태12/20/2021475개발 환경 구성: 620. Windows 10+에서 WMI root/Microsoft/Windows/WindowsUpdate 네임스페이스 제거
12883정성태12/19/2021644오류 유형: 775. uwsgi-plugin-python3 환경에서 "ModuleNotFoundError: No module named 'django'" 오류 발생
12882정성태12/18/2021531개발 환경 구성: 619. Windows Server에서 WSL을 위한 리눅스 배포본을 설치하는 방법
12881정성태12/17/2021606개발 환경 구성: 618. WSL Ubuntu 20.04에서 파이썬을 위한 uwsgi 설치 방법 (2)
12880정성태12/16/2021602VS.NET IDE: 170. Visual Studio에서 .NET Core/5+ 역어셈블 소스코드 확인하는 방법
12879정성태12/16/20212376오류 유형: 774. Windows Server 2022 + docker desktop 설치 시 WSL 2로 선택한 경우 "Failed to deploy distro docker-desktop to ..." 오류 발생
12878정성태12/15/2021677개발 환경 구성: 617. 윈도우 WSL 환경에서 같은 종류의 리눅스를 다중으로 설치하는 방법
12877정성태12/15/2021548스크립트: 36. 파이썬 - pymysql 기본 예제 코드
12876정성태12/14/2021583개발 환경 구성: 616. Custom Sources를 이용한 Azure Monitor Metric 만들기
12875정성태12/13/2021562스크립트: 35. python - time.sleep(...) 호출 시 hang이 걸리는 듯한 문제
12874정성태12/13/2021478오류 유형: 773. shell script 실행 시 "$'\r': command not found" 오류
12873정성태12/12/2021701오류 유형: 772. 리눅스 - PATH에 등록했는데도 "command not found"가 나온다면?
12872정성태12/12/2021606개발 환경 구성: 615. GoLang과 Python 빌드가 모두 가능한 docker 이미지 만들기
12871정성태12/12/20211343오류 유형: 771. docker: Error response from daemon: OCI runtime create failed
12870정성태12/9/2021548개발 환경 구성: 614. 파이썬 - PyPI 패키지 만들기 (4) package_data 옵션
12869정성태12/8/2021644개발 환경 구성: 613. git clone 실행 시 fingerprint 묻는 단계를 생략하는 방법
12868정성태12/7/2021514오류 유형: 770. twine 업로드 시 "HTTPError: 400 Bad Request ..." 오류
12867정성태12/7/2021568개발 환경 구성: 612. 파이썬 - PyPI 패키지 만들기 (3) entry_points 옵션
12866정성태12/7/20212282오류 유형: 769. "docker build ..." 시 "failed to solve with frontend dockerfile.v0: failed to read dockerfile ..." 오류
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...