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

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  142  143  144  145  146  147  [148]  149  150  ...
NoWriterDateCnt.TitleFile(s)
1354정성태9/19/201224456.NET Framework: 338. .NET CLR GC 시간 측정하는 방법파일 다운로드1
1353정성태9/17/201225764.NET Framework: 337. Python의 생성기와 코루틴을 C#으로 표현하면. [2]파일 다운로드1
1352정성태9/13/201223820.NET Framework: 336. .NET Profiler가 COM 개체일까?
1351정성태9/13/201228249디버깅 기술: 49. windbg - .NET Framework 스레드 개체의 COM Apartment 유형 확인하는 방법
1350정성태9/12/201228905개발 환경 구성: 167. (실은) 무료가 아니었던 AWS EC2 서비스 [4]
1349정성태9/11/201260646VS.NET IDE: 74. Visual Studio의 '새 파일'을 UTF-8 인코딩으로 지정하는 방법 [4]
1348정성태9/11/201228083오류 유형: 164. Active Directory - Functional Level 승격이 안 되는 문제
1347정성태9/10/201230552Windows: 62. 윈도우 서버 2012 - Hyper-V 서버 마이그레이션 [1]
1346정성태9/10/201231411Windows: 61. 윈도우 서버 2012 - Active Directory 서버 마이그레이션
1345정성태9/10/201235456스크립트: 12. 파이썬 - Win32 DLL 연동 [2]
1344정성태9/10/201228578오류 유형: 163. .NET Framework 4.5 제거 후 Visual Studio 2010 실행 시 Unknown Error
1343정성태9/8/201242341스크립트: 11. 파이썬(Python) 윈도우 개발 환경 [7]
1342정성태9/6/201226533VS.NET IDE: 73. Visual Studio 2012 - XmlCodeGenerator 마이그레이션
1341정성태9/4/201235848Windows: 60. Hyper-V에서 RemoteFX 없이 DirectX 11 제공 [12]
1340정성태9/4/201228039개발 환경 구성: 166. DOS - ping 결과에서 평균 응답 시간값 추출하기 [3]
1339정성태9/4/201230479개발 환경 구성: 165. 새로운 Visual Studio 2012 원격 디버깅 툴 [5]
1338정성태9/4/201232299.NET Framework: 335. C# - (핸들을 이용하여) 모든 열린 파일을 열람 [6]파일 다운로드1
1337정성태8/30/201222079Phone: 7. 디버거로 실습해 보는 윈도우 폰의 Tombstone 상태파일 다운로드1
1336정성태8/30/201240140.NET Framework: 334. 스레드 비정상 종료로 발생하는 CLOSE_WAIT 소켓 상태 [2]파일 다운로드1
1335정성태8/30/201228893Windows: 59. Hyper-V Internal 네트워크 VM의 인터넷 접속
1334정성태8/29/201248170.NET Framework: 333. 코드로 재현하는 소켓 상태(FIN_WAIT1, FIN_WAIT2, TIME_WAIT, CLOSE_WAIT, LAST_WAIT) [6]
1333정성태8/27/201251617개발 환경 구성: 164. system32 폴더에 있는 파일의 권한 조정 [2]
1332정성태8/23/201223524Team Foundation Server: 48. TFS - Team Project Collection 이전하는 방법
1331정성태8/23/201226668오류 유형: 162. Database '...' already exists. Choose a different database name. (Microsoft SQL Server, Error: 1801)
1330정성태8/22/201227417Team Foundation Server: 47. 5인 이내의 팀, 또는 개인 로컬 소스 관리를 위한 무료 TFS Express
1329정성태8/21/201222897오류 유형: 161. Azure - Storage 삭제가 안되는 경우 [1]
... 136  137  138  139  140  141  142  143  144  145  146  147  [148]  149  150  ...