성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>Azure Active Directory - Microsoft Graph API 호출 방법</h1> <p> 지난 예제에서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - Azure AD 인증을 지원하는 ASP.NET Core/5+ 웹 애플리케이션 예제 구성 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12614'>https://www.sysnet.pe.kr/2/0/12614</a> </pre> <br /> Azure AD 인증을 해봤는데요, 그럼 해당 인증 정보를 기반으로 Microsoft Graph도 호출해 보면 좋겠죠? ^^<br /> <br /> 그리고 이에 대해서는 다음의 문서에 이미 잘 나와 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > A web app that calls web APIs: Call a web API ; <a target='tab' href='https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-call-api-call-api'>https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-call-api-call-api</a> </pre> <br /> 이번 글에서는 "<a target='tab' href='https://www.sysnet.pe.kr/2/0/12614'>C# - Azure AD 인증을 지원하는 ASP.NET Core/5+ 웹 애플리케이션 예제 구성</a>" 글에 실은 예제를 바탕으로 위의 글을 적용해 보겠습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 우선, appsettings.json에 API 호출을 위한 ClientSecret 또는 ClientCertificates를 지정해야 하는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > { "AzureAd": { "Instance": "https://login.microsoftonline.com/", "ClientId": "[...client_id...]", "TenantId": "[...tenant_id...]" // To call an API "ClientSecret": "[client_secret_value]", "ClientCertificates": [ /* 또는 인증서 지정 */ ] }, "Graph": { "BaseUrl": "https://graph.microsoft.com/v1.0", "Scopes": "user.read" } } </pre> <br /> 이를 위해서는 "<a target='tab' href='https://www.sysnet.pe.kr/2/0/12614'>C# - Azure AD 인증을 지원하는 ASP.NET Core/5+ 웹 애플리케이션 예제 구성</a>"에서 만들었던 (예제에서는 test-auth-webapp) App에 "Certificates & secrets" 설정을 통해 새로운 ClientSecret을 생성합니다.<br /> <br /> <img alt='user_profile_info_from_aad_2.png' src='/SysWebRes/bbs/user_profile_info_from_aad_2.png' /><br /> <br /> 생성 시 단 한번 ClientSecret 값이 보이기 때문에 복사해서 위의 appsettings.json에 붙여주면 됩니다.<br /> <br /> 이제 코드를 변경해야 하는데요, 이를 위해 패키지를 하나 더 추가하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect // Install-Package Microsoft.Identity.Web Install-Package Microsoft.Identity.Web.MicrosoftGraph </pre> <br /> Startup.cs의 ConfigureServices에 아래와 같은 코드를 추가합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd")) <span style='color: blue; font-weight: bold'>.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" }) .AddMicrosoftGraph(Configuration.GetSection("Graph")) .AddInMemoryTokenCaches();</span> services.AddControllersWithViews(options => { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); options.Filters.Add(new AuthorizeFilter(policy)); }); services.AddRazorPages(); } </pre> <br /> 이렇게 해주면, 이후 AccessToken이 설정된 <a target='tab' href='https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-call-api-call-api'>GraphServiceClient 인스턴스를 생성자를 통해 주입</a>받을 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; using Microsoft.Graph; using Microsoft.Identity.Web; using System.Threading.Tasks; namespace WebApplication1.Pages { public class IndexModel : PageModel { <span style='color: blue; font-weight: bold'>private readonly GraphServiceClient _graphServiceClient;</span> private readonly ILogger<IndexModel> _logger; public IndexModel(ILogger<IndexModel> logger, <span style='color: blue; font-weight: bold'>GraphServiceClient graphServiceClient</span>) { _logger = logger; <span style='color: blue; font-weight: bold'>_graphServiceClient = graphServiceClient;</span> } public async Task OnGet() { <span style='color: blue; font-weight: bold'>var user = await _graphServiceClient.Me.Request().GetAsync();</span> ViewData["tel"] = user.MobilePhone; } } } </pre> <br /> 위의 코드에서는 MobilePhone 정보를 구하는데, 테스트를 위해 Azure Portal에서 로그인 사용자의 프로파일 정보 중 "Mobile phone"을 수정하고,<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='microsoft_graph_user_profile_1.png' src='/SysWebRes/bbs/microsoft_graph_user_profile_1.png' /><br /> <br /> 실행해 보면 정상적으로 동작하는 것을 확인할 수 있습니다. (참고로 <a target='tab' href='https://cmatskas.com/call-ms-graph-apis-from-asp-net-core-3-1/'>GraphServiceClient를 직접 주입받지 않고 ITokenAcquisition을 통해 Access Token을 구한 후 구성하는 것</a>도 가능합니다.)<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1828&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 저 코드는 그냥 테스트 용도 이외에 실 서버에서는 사용할 수 없습니다. (사실, 테스트로도 무척 불편한 코드입니다.)<br /> <br /> 왜냐하면, 토큰을 메모리에 보관하기 때문입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" }) .AddMicrosoftGraph(Configuration.GetSection("Graph")) <span style='color: blue; font-weight: bold'>.AddInMemoryTokenCaches();</span> </pre> <br /> 그래서, 서버 프로세스 (개발 중에는 iisexpress.exe)를 다시 실행하게 되면 (로그아웃을 명시적으로 하지 않았기 때문에) 여전히 인증 중이지만 _graphServiceClient 호출 코드에서 다음과 같은 오류가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Microsoft.Identity.Client.MsalUiRequiredException HResult=0x80131500 Message=<span style='color: blue; font-weight: bold'>No account or login hint was passed to the AcquireTokenSilent call.</span> Source=Microsoft.Identity.Client StackTrace: at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.<ExecuteAsync>d__5.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs:line 56 at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.<ExecuteAsync>d__5.MoveNext() This exception was originally thrown at this call stack: [External Code] System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in ExceptionDispatchInfo.cs [External Code] </pre> <br /> 왜냐하면, access token을 "Add<span style='color: blue; font-weight: bold'>InMemoryTokenCaches</span>"에 보관했기 때문에 해당 정보가 더 이상 유효하지 않은 것입니다. (그래서 테스트를 하려면 다시 명시적인 로그아웃을 한 다음, 로그인을 반복하는 식으로 진행해야 합니다.) <br /> <br /> 이에 대해서는 다음의 문서에서 잘 설명하고 있는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Token cache serialization in MSAL.NET ; <a target='tab' href='https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-token-cache-serialization?tabs=aspnetcore'>https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-token-cache-serialization?tabs=aspnetcore</a> </pre> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> AddInMemoryTokenCaches<br /> <br /> In memory token cache serialization. This implementation is great in samples. It's also good in production applications <span style='color: blue; font-weight: bold'>provided you don't mind if the token cache is lost when the web app is restarted.</span> AddInMemoryTokenCaches takes an optional parameter of type MsalMemoryTokenCacheOptions that enables you to specify the duration after which the cache entry will expire unless it's used. </div><br /> <br /> token cache 관련해서 그 외에 AddSessionTokenCaches, AddDistributedTokenCaches를 제공하는데 대개의 경우 현업에서 실 서비스를 하려면 AddDistributedTokenCaches를 사용해야 할 것입니다. (혹은 AddSessionTokenCaches를 사용한다면 외부 세션 저장소를 사용해야 할 듯하고.)<br /> <br /> 재미있는 것은, 아래의 문서에 소개된,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Acquiring tokens with authorization codes on web apps ; <a target='tab' href='https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Acquiring-tokens-with-authorization-codes-on-web-apps'>https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Acquiring-tokens-with-authorization-codes-on-web-apps</a> </pre> <br /> 예제 프로젝트를 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2 ; <a target='tab' href='https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/aspnetcore2-2-signInAndCallGraph'>https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/aspnetcore2-2-signInAndCallGraph</a> </pre> <br /> 쿠키를 token cache로 사용하는 CookieBasedTokenCacheExtension 예제 코드를 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > active-directory-aspnetcore-webapp-openidconnect-v2/Extensions/AuthPropertiesTokenCacheHelper.cs / ; <a target='tab' href='https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/aspnetcore2-2-signInAndCallGraph/Extensions/AuthPropertiesTokenCacheHelper.cs'>https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/aspnetcore2-2-signInAndCallGraph/Extensions/AuthPropertiesTokenCacheHelper.cs</a> </pre> <br /> 이를 사용하는 측에도 주석에 방법을 설명하는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // <a target='tab' href='https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/aspnetcore2-2-signInAndCallGraph/Startup.cs'>https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/aspnetcore2-2-signInAndCallGraph/Startup.cs</a> // Token acquisition service and its cache implementation services.AddTokenAcquisition() .AddDistributedMemoryCache() .AddInMemoryTokenCache() /* you could use a cookie based token cache by reaplacing the last * trew lines by : <span style='color: blue; font-weight: bold'>.AddCookie().AddCookieBasedTokenCache()</span> */ ; </pre> <br /> 실제로 해보면, .AddCookie().AddCookieBasedTokenCache()와 같이 자연스럽게 연결이 안 됩니다. (아마도 <a target='tab' href='https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-differences-adal-net'>MSAL.NET의 전신이었던 Azure AD v2.0/ADAL.NET</a>으로 불리던 시절에 잘 동작했을 것입니다.) 이 부분은 나중에 살펴봐야겠지만, 아마... 시간이 지나면 라이브러리 차원에서 추가되지 않을까...라는 기대를 해봅니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 만약 다음과 같이 "Invalid version" 오류가 발생한다면?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Microsoft.Graph.ServiceException HResult=0x80131500 Message=Code: BadRequest Message: Invalid version. 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__38.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] </pre> <br /> appsettings.json 파일의 "Graph" 경로를 잘 못 설정해서 그런 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > "Graph": { <span style='color: blue; font-weight: bold'>"BaseUrl": "https://graph.microsoft.com/",</span> "Scopes": "user.read" } </pre> <br /> 위와 같이 하면 안 되고, 하위에 버전까지 명시를 해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > "Graph": { <span style='color: blue; font-weight: bold'>"BaseUrl": "https://graph.microsoft.com/v1.0",</span> "Scopes": "user.read" } </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1076
(왼쪽의 숫자를 입력해야 합니다.)