내보내기(0) 인쇄
모두 확장

Azure AD(미리 보기)를 사용하여 Windows 스토어 응용 프로그램 및 REST 웹 서비스 보안

게시: 2013년 4월

업데이트 날짜: 2014년 5월

note참고
이 예제는 오래된 버전입니다. 기술, 방법 및/또는 사용자 인터페이스가 새로운 기능으로 대체되었습니다. 유사한 응용 프로그램을 빌드하는 업데이트된 예제를 보려면 NativeClient-WindowsStore를 참조하십시오.

이 연습에서는 Azure AD 사용자 대신 Web API를 호출할 권한이 있는 Windows 스토어 응용 프로그램을 개발하는 방법을 보여 줍니다. 네이티브 클라이언트 응용 프로그램과 Web API는 모두 테넌트 관리자에 의해 Azure AD에 등록됩니다. 이 관리자는 클라이언트와 서비스 간의 관계를 제어할 수 있습니다(클라이언트와 서비스에 각각 개별 사용 권한을 할당하는 작업 포함). OAuth 2.0 전달자 권한 유형을 사용하여 권한 부여를 처리하며, Azure AD는 사용자가 자격 증명을 다시 입력해야 하는 빈도를 최소화할 수 있도록 새로 고침 토큰을 발급합니다.

이 연습을 통해 완성되는 제품은 MSDN 코드 갤러리 코드 샘플: REST 서비스에 대한 Windows 스토어 응용 프로그램 - 브라우저 대화 상자를 통해 AAD로 인증

이 연습에서는 다음 내용을 다룹니다.

  • 리소스 Web API 작성. 이 서비스는 Azure AD 인증이 필요한 MVC 4 Web API가 됩니다.

  • 클라이언트 응용 프로그램 작성. 네이티브 클라이언트는 Windows 스토어 응용 프로그램이 됩니다.

  • Azure AD에 네이티브 클라이언트 및 Web API 등록. 네이티브 클라이언트에도 Web API 호출 권한이 부여됩니다.

note참고
이 연습에서는 현재 Developer Preview 상태인 Windows 스토어용 AAL을 사용합니다. ADAL은 시험판 제품이므로 향후 릴리스에서 해당 기능이 변경될 수 있습니다.

연습을 시작하려면 다음 필수 구성 요소를 충족해야 합니다.

  • 인터넷 연결

  • 활성 Azure 구독: 90일 무료 평가판은 Azure 무료 평가판

  • Visual Studio 2012 Professional 또는 Visual Studio 2012 Ultimate: 다음 링크를 통해 무료 평가판을 다운로드할 수 있습니다. Visual Studio 무료 평가판

  • Windows 8: 이 응용 프로그램을 개발하려면 Windows 8을 사용해야 하며 개발자 라이선스를 등록한 상태여야 합니다. 추가 정보

이 문서에서는 Azure AD와 함께 네이티브 클라이언트를 사용하는 방법을 설명하기 위해 AAL(Azure 인증 라이브러리)을 사용하여 Windows 스토어 응용 프로그램을 작성합니다. 사용자는 AAL을 통해 Azure AD 계정을 사용하여 REST 서비스에 대한 액세스 권한을 부여할 수 있습니다.

또한 연습의 일부분으로 클라이언트가 호출할 수 있는 간단한 서비스도 작성합니다. 이 서비스는 사용자가 할 일 목록을 저장할 수 있는 중앙 위치를 제공하며 온-프레미스 또는 Azure에서 호스팅할 수 있습니다.

아래 그림에는 이 과정에 포함되는 당사자들의 역할이 나와 있습니다.

  1. Contoso 할 일 목록 서비스는 사용자의 할 일 목록 항목을 저장하며, 사용자가 항목을 저장하고 가져오는 데 사용할 수 있는 Web API입니다. 사용자는 다른 사용자의 항목을 관리할 수는 없습니다.

  2. Contoso 할 일 목록 클라이언트 앱은 사용자가 Azure AD를 사용하여 서비스에 액세스해 할 일 목록 항목을 보고 만들 수 있도록 합니다.

  3. 조직(여기서는 Contoso)에서 만드는 Azure AD 테넌트는 사용자, 앱 및 서비스에 대한 보안 주체를 포함합니다.

솔루션 아키텍처

이 연습에서 가장 먼저 수행해야 하는 작업은 Azure 관리 포털에서 사용할 Azure AD 테넌트가 있는지 확인하는 것입니다.

Azure Active Directory는 Office 365 및 Intune과 같은 Microsoft 제품의 ID 백본을 제공하는 서비스입니다. 이러한 서비스를 구독하는 경우에는 사용자가 로그인하는 데 사용하는 Azure AD 테넌트가 이미 있는 것입니다. 현재는 해당 테넌트를 다시 사용하려는 경우 새 Azure 구독을 만든 다음 등록 시 해당 테넌트의 관리자 사용자를 사용하면 됩니다. 이 연습에서는 해당 프로세스를 수행하는 방법에 대한 자세한 지침을 제공하지는 않습니다.

이 연습에서는 기존 Azure AD 테넌트가 없는 경우에 대해 중점적으로 설명합니다. 먼저 Azure 관리 포털에서 사용할 Azure AD 테넌트가 있는지 확인해야 합니다. Azure 관리 포털에서는 관리 포털 페이지에서 직접 새 테넌트를 만들 수 있는 메커니즘을 제공합니다.

  1. http://manage.windowsazure.com으로 이동하여 Azure 구독에 연결된 Microsoft 계정을 사용해 로그인합니다. 로그인한 후 화면 왼쪽의 탭 목록을 확인하여 Active Directory 탭을 찾아서 클릭합니다.



    엔터프라이즈 디렉터리 추가

    위의 화면은 Azure 관리 포털의 Active Directory 홈 페이지입니다. 위쪽 영역에는 두 개의 헤더가 있습니다. 액세스 제어 네임스페이스 헤더는 ACS 2.0 서비스를 통해 작성된 네임스페이스를 지칭하며, 이 연습에서는 사용되지 않습니다. 디렉터리 헤더를 클릭하면 디렉터리 테넌트에 대한 작업을 수행할 수 있는 관리 포털 영역으로 이동합니다. 이 연습의 작업은 대부분 해당 영역에서 수행합니다.

  2. 위의 스크린샷에 나와 있는 것처럼 Azure 관리 포털에서는 사용자에게 디렉터리 테넌트가 연결되어 있지 않음을 감지하여 테넌트 만들기 옵션을 제공합니다. 디렉터리 만들기를 클릭합니다.



    엔터프라이즈 테넌트 추가

    디렉터리 테넌트를 자동으로 만들기 위한 필수 정보를 수집하는 대화 상자가 표시됩니다. 각 필드를 아래쪽에서부터 살펴보겠습니다.

    • 조직 이름: 필수 필드입니다. 회사 이름을 표시해야 할 때마다 해당 값을 모니커로 사용합니다.

    • 국가 또는 지역: 이 드롭다운에서 선택한 값에 따라 테넌트를 만들 위치가 결정됩니다. 디렉터리에는 중요한 정보가 저장되므로 회사를 운영하는 국가의 개인 정보 관련 규정을 고려하십시오.

    • 도메인 이름: 이 필드는 중요한 정보인 테넌트와 관련된 디렉터리 테넌트 도메인 이름 부분을 나타냅니다. 이 이름을 통해 각 디렉터리 테넌트를 구분할 수 있습니다.

      각 디렉터리 테넌트는 작성 시 <tenantname>.onmicrosoft.com 형식의 도메인으로 식별됩니다. 해당 도메인은 모든 디렉터리 사용자의 UPN에 사용되며 일반적으로 디렉터리 테넌트를 식별해야 할 때마다 사용됩니다. 테넌트를 만든 후에는 사용자가 소유한 추가 도메인을 등록할 수 있습니다. 자세한 내용은 도메인 관리를 참조하십시오.

      도메인은 고유해야 합니다. UI 유효성 검사 논리를 통해 고유한 이름을 선택할 수 있습니다. 회사를 지칭하는 핸들을 선택하는 것이 좋습니다. 이렇게 하면 사용자와 파트너가 디렉터리 테넌트와 보다 쉽게 상호 작용할 수 있습니다.

    디렉터리 테넌트를 만들 때는 해당 대화 상자에 정보를 입력하기만 하면 됩니다. 오른쪽 아래의 확인 단추를 클릭하는 즉시 Azure AD가 지정된 매개 변수에 따라 새 테넌트를 자동으로 만듭니다. 아래 화면에서 새 항목을 확인할 수 있습니다.



    엔터프라이즈 디렉터리

    note참고
    디렉터리 테넌트를 만들면 클라우드에 사용자와 자격 증명을 저장하도록 구성됩니다. 디렉터리 테넌트를 온-프레미스에 배포된 Windows Server Active Directory와 통합하려는 경우 여기서 자세한 지침을 확인할 수 있습니다.

  3. 새로 만든 디렉터리 항목을 클릭하여 사용자 관리 UI를 표시합니다.



    모든 테넌트 사용자

    디렉터리 테넌트는 새 테넌트를 만든 Azure 구독을 관리하는 Microsoft 계정을 제외하면 처음에는 비어 있습니다.

    해당 Microsoft 계정은 테넌트에 대한 전역 관리자 권한이 있음을 나타내기 위해 여기에 표시됩니다. 그러나 해당 권한은 Azure 관리 포털 UI를 통해 수행하는 작업에 대해서만 적용됩니다. 따라서 해당 Microsoft 계정은 웹 SSO와 같은 흐름의 경우 디렉터리 테넌트에 대해 실제로 인증할 수는 없으므로 이 문서에서 설명하는 웹 SSO 연습의 테스트 사용자로 사용할 수 없습니다.

    이 문서 뒷부분에 나오는 웹 SSO 시나리오를 연습할 수 있도록 디렉터리에 새 사용자를 추가하겠습니다.

  4. 화면 아래쪽 모음에서 사용자 추가 명령을 클릭합니다. 그러면 아래와 같은 대화 상자가 표시됩니다.



    사용자 추가

    사용자 추가 대화 상자에서는 먼저 디렉터리 사용자를 만들 것인지 아니면 기존 Microsoft 계정(현재 구독 관리에 사용 중인 Microsoft 계정과 같은 제한이 적용됨)을 추가할 것인지를 묻는 메시지가 표시됩니다. 여기서는 워크플로를 위한 디렉터리 사용자가 필요하므로 조직의 새 사용자 항목을 선택합니다.

  5. 사용자 이름을 선택하고 오른쪽 아래의 화살표를 클릭합니다.



    사용자 추가

    다음 화면에서는 몇 가지 기본 사용자 특성을 선택할 수 있습니다. 사용자에게 할당하는 역할에 따라 해당 사용자가 디렉터리 액세스 시 수행할 수 있는 작업이 결정됩니다.

    계정의 유효한 전자 메일을 입력해야 합니다. Multi-Factor Authentication 미리 보기를 사용해 볼 수 있는 옵션도 제공됩니다.

  6. 값을 입력한 후 오른쪽 아래의 화살표를 클릭하여 다음 화면으로 이동합니다.



    임시 암호

    마지막 단계에서는 관리 포털에서 최초 로그인 시 사용해야 하는 임시 암호를 만듭니다. 사용자는 최초 로그인 시 암호를 변경해야 합니다. 임시 암호는 시나리오를 테스트하는 데 필요한 모든 구성 요소가 준비되면 사용해야 하므로 저장해 두십시오.

    이제 웹 SSO 시나리오에서 인증 기관을 제공하는 데 필요한 모든 구성 요소인 디렉터리 테넌트 및 해당 테넌트의 유효한 사용자가 준비되었습니다.

이 섹션에서는 Contoso 직원의 할 일 항목을 유지 관리하는 할 일 목록 서비스를 만듭니다. 이 서비스는 ASP.NET Web API로 구성되며, JSON 웹 토큰 처리기를 사용하여 권한이 있는 사용자와 클라이언트만 서비스에 액세스할 수 있도록 합니다.

  1. Visual Studio를 시작하고 파일, 새로 만들기, 프로젝트를 차례로 클릭합니다.

  2. 새 프로젝트 대화 상자 왼쪽 메뉴의 Visual C# 템플릿에서 을 선택한 다음ASP.NET MVC 4 웹 응용 프로그램 템플릿을 선택합니다. 새 프로젝트는 "ToDoListService"로, 솔루션은 "ToDoListApp"으로 이름을 지정하고 확인을 클릭합니다.

  3. 새 ASP.NET MVC 4 프로젝트 대화 상자가 표시됩니다. Web API 템플릿을 선택하고 확인을 클릭합니다. Web API 템플릿이 간단한 Web API 프로젝트에 필요한 스캐폴딩을 만듭니다.



    새 ASP.NET MVC 4 프로젝트

  4. 새 프로젝트를 만든 후 F5 키를 눌러 프로젝트를 실행합니다. 그러면 브라우저가 열리고 아래 스크린샷과 같은 페이지가 표시됩니다.



    결과 새 ASP.NET 프로젝트

    열린 웹 브라우저에는 위 이미지의 http://localhost:29062/와 같이 Visual Studio에서 응용 프로그램에 자동으로 할당한 URL이 표시됩니다. 이 값은 다음 구성 값을 나타내므로 나중에 사용해야 합니다.

    • 할 일 목록 서비스 프로젝트의 대상 그룹

    • Azure AD에 등록되어 있으며 할 일 목록 클라이언트가 서비스를 식별하는 데 사용하는 앱 ID URI

    프로덕션 응용 프로그램의 경우 이 값은 https://todolistservice.azurewebsites.net과 같이 응용 프로그램을 호스팅하는 사이트의 기준 주소인 것이 좋습니다. 그러나 테넌트 내에서 고유한 어떤 URI든 사용할 수 있습니다.

    Important중요
    공격자가 인증 토큰을 도용하고 권한 상승 공격을 수행할 수 없도록 프로덕션 서비스는 SSL을 사용하여 서비스에 대한 통신을 보호해야 합니다.

  5. 웹 브라우저를 닫고 다음 단계로 진행합니다.

다음으로는 프로젝트에 필요한 종속성을 추가합니다.

  1. 솔루션 탐색기에서 참조를 마우스 오른쪽 단추로 클릭하고 참조 추가를 클릭합니다.

  2. 참조 관리자 대화 상자에서 System.IdentityModel 어셈블리를 선택하고 확인을 클릭합니다. 참조가 프로젝트에 추가됩니다.

  3. 참조 폴더를 마우스 오른쪽 단추로 클릭한 다음 "NuGet 패키지 관리..."를 클릭하여 JSON 웹 토큰 처리기 NuGet 패키지를 설치합니다.

  4. NuGet 패키지 관리 대화 상자에서 'jwt 처리기'를 검색합니다. 결과가 표시되면 Microsoft .NET Framework용 JSON 웹 토큰 처리기를 선택하고 설치를 클릭합니다.

  5. 라이선스 승인 대화 상자가 나타납니다. 사용 약관에 동의하면 동의함을 클릭합니다. 그러면 JWT 처리기가 다운로드되어 프로젝트에서 참조됩니다.

  6. 같은 NuGet 패키지 관리 대화 상자에서 '토큰 유효성 검사 확장'을 검색합니다. 결과가 표시되면 Microsoft .NET Framework 4.5용 Microsoft 토큰 유효성 검사 확장을 선택하고 설치를 클릭합니다.

  7. 라이선스 승인 대화 상자가 나타납니다. 사용 약관에 동의하면 동의함을 클릭합니다. 그러면 토큰 유효성 검사 확장이 다운로드되어 프로젝트에서 참조됩니다.

이제 필요한 종속성을 추가했으므로 JWT 처리기를 사용하여 Azure AD에서 발급하는 토큰의 유효성을 검사하도록 Global.asax.cs 파일을 업데이트해야 합니다. 클라이언트가 Web API 서비스를 호출하면 권한 부여 헤더에서 토큰을 읽습니다.

토큰의 유효성을 검사하여 다음 사항을 확인합니다.

  • 토큰 발급자가 올바르며 Azure AD 테넌트의 발급자임

  • 토큰이 올바른 x509 인증서를 사용하여 암호화 방식으로 서명됨

  • 토큰이 호출 응용 프로그램에 필요한 사용 권한을 부여함

아래에는 업데이트된 Global.asax.cs 파일의 전체 내용과 변경 내용에 대한 설명이 순서대로 나와 있습니다.

using System;
using System.Collections.Generic;
using System.IdentityModel.Metadata;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Xml;
using System.Xml.Linq;

namespace ToDoListService
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
        }
    }

    internal class TokenValidationHandler : DelegatingHandler
    {
        // Domain name or Tenant name
        const string domainName = "[company/domain name like treyresearch.onmicrosoft.com]";
        const string audience = "[service App ID URI configured in tenant/domain for the Service App ex: http://localhost:40316/]";

        static DateTime _stsMetadataRetrievalTime= DateTime.MinValue;
        static List<X509SecurityToken> _signingTokens = null;
        static string _issuer = string.Empty;
        

        // SendAsync is used to validate incoming requests contain a valid access token, and sets the current user identity 
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string jwtToken;
            string issuer;
            List<X509SecurityToken> signingTokens;

            if (!TryRetrieveToken(request, out jwtToken))
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }

            try
            {
                // Get tenant information that's used to validate incoming jwt tokens
                GetTenantInformation(string.Format("https://login.windows.net/{0}/federationmetadata/2007-06/federationmetadata.xml", domainName), out issuer, out signingTokens);
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler()
            {
                CertificateValidator = X509CertificateValidator.None
            };

            TokenValidationParameters validationParameters = new TokenValidationParameters
            {
                AllowedAudience = audience,
                ValidIssuer = issuer,
                SigningTokens = signingTokens
            };

            try
            {

                 // Validate token
                ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken,
                                                validationParameters);

                //set the ClaimsPrincipal on the current thread.
                Thread.CurrentPrincipal = claimsPrincipal;

                // set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.User = claimsPrincipal;
                }            

                // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);
            }
            catch (SecurityTokenValidationException)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }
        }

        // Reads the token from the authorization header on the incoming request
        private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
        {
            token = null;
            string authzHeader;

            if (!request.Headers.Contains("Authorization"))
            {
                return false;
            }

            authzHeader = request.Headers.GetValues("Authorization").First<string>();

            // Verify Authorization header contains 'Bearer' scheme
            token = authzHeader.StartsWith("Bearer ") ? authzHeader.Split(' ')[1] : null;

            if (null == token)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Parses the federation metadata document and gets issuer Name and Signing Certificates
        /// </summary>
        /// <param name="metadataAddress">URL of the Federation Metadata document</param>
        /// <param name="issuer">Issuer Name</param>
        /// <param name="signingTokens">Signing Certificates in the form of X509SecurityToken</param>
        static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
        {
            signingTokens = new List<X509SecurityToken>();

            // The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.            
            if (DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24
                || string.IsNullOrEmpty(_issuer)
                || _signingTokens == null)
            {
                MetadataSerializer serializer = new MetadataSerializer()
                {
                    CertificateValidationMode = X509CertificateValidationMode.None
                };
                MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));

                EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;

                // get the issuer name
                if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
                {
                    _issuer = entityDescriptor.EntityId.Id;
                }

                // get the signing certs
                _signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);

                _stsMetadataRetrievalTime = DateTime.UtcNow;
            }

            issuer = _issuer;
            signingTokens = _signingTokens;
        }

        static List<X509SecurityToken> ReadSigningCertsFromMetadata(EntityDescriptor entityDescriptor)
        {
            List<X509SecurityToken> stsSigningTokens = new List<X509SecurityToken>();

            SecurityTokenServiceDescriptor stsd = entityDescriptor.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First();

            if (stsd != null && stsd.Keys != null)
            {
                IEnumerable<X509RawDataKeyIdentifierClause> x509DataClauses = stsd.Keys.Where(key => key.KeyInfo != null && (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).
                                                             Select(key => key.KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First());

                stsSigningTokens.AddRange(x509DataClauses.Select(clause => new X509SecurityToken(new X509Certificate2(clause.GetX509RawData()))));
            }
            else
            {
                throw new InvalidOperationException("There is no RoleDescriptor of type SecurityTokenServiceType in the metadata");
            }

            return stsSigningTokens;
        }
    }
}


이 코드를 실행하기 전에 다음 상수를 설정해야 합니다.

  • domainName: Azure AD 테넌트에 등록한 도메인 이름입니다. 예를 들면 다음과 같습니다. contoso.onmicrosoft.com

  • audience: 할 일 목록 서비스의 앱 ID URI입니다. 나중에 Azure AD에 이 URI를 등록할 것입니다. 이 상수의 표준 값은 할 일 목록 서비스를 실행 중인 기준 주소입니다. 예를 들어 Visual Studio에서 개발하는 경우 이 주소는 다음과 같습니다. http://localhost:29062/

기준 주소를 서비스의 audience 값으로 사용하는 것이 좋습니다. 이렇게 하면 호출자가 대상 서비스의 ID를 서비스 주소에서 파생시킬 수 있기 때문입니다.

이제 위에 나와 있는 코드에 대해 자세히 설명하겠습니다.

Global.asax.cs에서는 다음 작업을 수행하기 위해 파이프라인에서 토큰 처리기가 구성됩니다.

  • 요청의 권한 부여 헤더에서 토큰 획득

  • 테넌트의 보안 토큰 서비스에서 메타데이터 검색

  • 메타데이터의 서명 키와 발급자를 사용하여 토큰 유효성 검사

  • 현재 스레드의 ID를 토큰의 사용자 ID로 설정

  • 사용자 대신 Web API를 호출하는 데 필요한 사용 권한이 호출 응용 프로그램(예: 할 일 목록 클라이언트)에 부여되었는지 확인

다음 using 지시문이 Global.asax.cs 파일에 추가되었습니다.

using System;
using System.Collections.Generic;
using System.IdentityModel.Metadata;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Xml;
using System.Xml.Linq;

다음으로는 DelegatingHandler 클래스에서 파생된 TokenValidationHandler 클래스가 추가되었습니다. TokenValidationHandler는 서비스에 인증하는 데 사용되는 토큰 유효성 검사를 위한 대부분의 작업을 수행합니다.

처리기 작성을 시작하려면 다음 코드를 추가합니다.

internal class TokenValidationHandler : DelegatingHandler
    {
        // Domain name or Tenant name
        const string domainName = "[company/domain name like treyresearch.onmicrosoft.com]";
        const string audience = "[service App ID URI configured in tenant/domain for the Service App ex: http://localhost:40316/]";

        static DateTime _stsMetadataRetrievalTime= DateTime.MinValue;
        static List<X509SecurityToken> _signingTokens = null;
        static string _issuer = string.Empty;

domainNameaudience를 설정해야 합니다.

domainName은 Azure AD에 등록할 때 등록한 도메인 이름입니다. 이 값의 예는 다음과 같습니다. contoso.onmicrosoft.com

audience는 할 일 목록 서비스의 앱 ID URI입니다. 이 값은 서비스를 실행 중인 로컬 주소에서 파생됩니다. 예를 들면 http://localhost:29062/와 같습니다. 앱 ID URI는 서비스에 인증하는 데 사용되는 토큰의 대상 그룹 클레임으로 설정되며, 토큰에 대한 올바른 대상을 나타냅니다. 따라서 할 일 목록 서비스는 토큰이 다른 서비스용이 아님을 확인하고 토큰을 거부할 수 있습니다. 이를 통해 전달 공격을 방지할 수 있습니다.

_issuer_signingTokens 필드는 설정되지 않습니다. 이러한 필드는 Azure AD에서 호스팅하는 페더레이션 메타데이터 문서의 정보를 사용하여 나중에 설정되며, 이후 값을 동적으로 업데이트할 수 있도록 합니다. 응용 프로그램에서 이 정보를 업데이트할 수 있어야 합니다(특히 인증서 서명용으로). 이러한 정보는 인증서 롤오버가 수행되면 시간이 지남에 따라 변경될 수 있기 때문입니다.

페더레이션 메타데이터 문서를 읽고 발급자 및 인증서 정보를 검색하기 위해 다음 코드가 추가됩니다.

        /// <summary>
        /// Parses the federation metadata document and gets issuer Name and Signing Certificates
        /// </summary>
        /// <param name="metadataAddress">URL of the Federation Metadata document</param>
        /// <param name="issuer">Issuer Name</param>
        /// <param name="signingTokens">Signing Certificates in the form of X509SecurityToken</param>
        static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
        {
            signingTokens = new List<X509SecurityToken>();

            // The issuer and signingTokens are cached for 24 hours. They are updated if any of the conditions in the if condition is true.            
            if (DateTime.UtcNow.Subtract(_stsMetadataRetrievalTime).TotalHours > 24
                || string.IsNullOrEmpty(_issuer)
                || _signingTokens == null)
            {
                MetadataSerializer serializer = new MetadataSerializer()
                {
                    // turning off certificate validation for demo. Don't use this in production code.
                    CertificateValidationMode = X509CertificateValidationMode.None
                };
                MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));

                EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;

                // get the issuer name
                if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
                {
                    _issuer = entityDescriptor.EntityId.Id;
                }

                // get the signing certs
                _signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);

                _stsMetadataRetrievalTime = DateTime.UtcNow;
            }

            issuer = _issuer;
            signingTokens = _signingTokens;
        }

페더레이션 메타데이터 파일의 URL을 지정하면 이 코드는 파일의 내용을 읽고 원하는 정보를 반환합니다.

페더레이션 메타데이터는 모든 요청에 대해서가 아니라 정기적으로만 읽으면 되므로 여기서는 간단한 메타데이터 캐싱 형식이 구현되었습니다. 즉, 메타데이터는 처음 사용할 때나 마지막으로 읽은 지 24시간이 지난 후에 검색됩니다. 페더레이션 메타데이터에 대한 가장 중요한 업데이트는 서명 인증서 롤오버 시에 수행됩니다. 롤오버 중에는 만료되는 인증서와 새 인증서를 모두 며칠 동안 사용할 수 있습니다. 이와 같은 연장 기간 동안 서비스는 두 인증서를 모두 가져올 수 있습니다.

다음으로는 TokenValidationHandler 클래스에 있는 SendAsync() 메서드를 살펴봅니다.

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

SendAsync()는 할 일 목록 서비스를 요청할 때마다 호출되며, 요청이 올바른 토큰을 포함하는지를 확인하여 들어오는 모든 요청을 인증한 다음 인증된 사용자에 대해 현재 사용자 ID를 설정합니다. 다음 코드는 할 일 목록 서비스에 대해 수행 중인 요청의 권한 부여 헤더에서 토큰을 읽습니다.

if (!TryRetrieveToken(request, out jwtToken))
{
    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}

위의 코드는 다음 코드에 나와 있는 것처럼 헤더에서 토큰을 읽는 TryRetieveToken()을 사용합니다.

// Reads the token from the authorization header on the incoming request
        private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
        {
            token = null;
            string authzHeader;

            if (!request.Headers.Contains("Authorization"))
            {
                return false;
            }

            authzHeader = request.Headers.GetValues("Authorization").First<string>();

            // Verify Authorization header contains 'Bearer' scheme
            token = authzHeader.StartsWith("Bearer ") ? authzHeader.Split(' ')[1] : null;

            if (null == token)
            {
                return false;
            }

            return true;
        }

위 코드는 권한 부여 헤더 문자열에서 토큰을 확인합니다. 헤더 값은 웹 호출에서 토큰을 전달하는 방법을 설명하는 RFC 6750 "OAuth 2.0 Authorization Framework: Bearer Token Usage"에 따라 "Bearer"로 시작해야 합니다. 헤더에 토큰이 있으면 true가 반환됩니다.

헤더에서 토큰을 검색한 후에는 토큰 유효성을 검사해야 합니다. 유효성 검사 프로세스에서는 Azure AD 테넌트 메타데이터, 발급자 및 대상 그룹에서 검색한 인증서에 대해 토큰 서명의 유효성을 검사해야 합니다. JWT 처리기는 아래에 나와 있는 SendAsync() 메서드에서 작성되는 TokenValidationParameters 개체에 설정된 값을 기준으로 이 유효성 검사를 수행합니다.

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            string jwtToken;
            string issuer;
            List<X509SecurityToken> signingTokens;

            if (!TryRetrieveToken(request, out jwtToken))
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }

            try
            {
                // Get tenant information that's used to validate incoming jwt tokens
                GetTenantInformation(string.Format("https://login.windows.net/{0}/federationmetadata/2007-06/federationmetadata.xml", domainName), out issuer, out signingTokens);
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler()
            {
                CertificateValidator = X509CertificateValidator.None
            };

            TokenValidationParameters validationParameters = new TokenValidationParameters
            {
                AllowedAudience = audience,
                ValidIssuer = issuer,
                SigningTokens = signingTokens
            };

            try
            {

                 // Validate token
                ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken,
                                                validationParameters);

                //set the ClaimsPrincipal on the current thread.
                Thread.CurrentPrincipal = claimsPrincipal;

                // set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
                if (HttpContext.Current != null)
                {
                    HttpContext.Current.User = claimsPrincipal;
                }            

                // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);
            }
            catch (SecurityTokenValidationException)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
            }
            catch (Exception)
            {
                return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.InternalServerError));
            }
        }

토큰 유효성 검사가 정상적으로 완료되면 토큰의 클레임을 사용하여 현재 스레드 및 HttpContext에 대해 호출자 ID를 설정합니다. ASP.NET과 IIS 간의 전환 중에 컨텍스트가 손실되지 않도록 하려면 현재 스레드와 HttpContext가 모두 필요합니다. 클레임에는 사용자와 호출자(할 일 목록 클라이언트)에 대한 정보가 모두 포함됩니다.

토큰 유효성 검사가 실패하면 HTTP 권한이 없음 오류가 반환됩니다. 마지막으로 메서드가 반환되기 전에 토큰에 포함되어 있었던 사용 권한을 확인합니다. 현재 클레임 주체에서 "scp"(범위) 클레임을 선택하여 이 확인을 수행할 수 있습니다. 필요한 값은 호출자가 할 일 항목을 만들고 읽을 수 있도록 하는 "user_impersonation"입니다.

                 // Verify that required permission is set in the scope claim
                if (ClaimsPrincipal.Current.FindFirst("scp").Value != "user_impersonation")
                {
                    return Task.FromResult<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.Unauthorized));
                }

                return base.SendAsync(request, cancellationToken);

테넌트 관리자가 클라이언트 앱을 테넌트에 추가하도록 동의하면 할 일 목록 클라이언트에 부여되는 사용 권한이 구성됩니다. 이 작업은 이후 단계에서 처리됩니다.

이제 TokenValidationHandler의 작동 방식을 파악했으므로 Application_Start() 메서드에 해당 처리기를 등록해야 합니다.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
            GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
        }

위 코드 조각의 마지막 줄은 들어오는 각 요청에 대해 처리기를 요청 파이프라인에 추가합니다.

다음으로는 할 일 항목의 모델을 추가합니다. 이 모델은 할 일 항목 데이터가 포함된 작은 클래스입니다.

  1. 솔루션 탐색기에서 모델 폴더를 마우스 오른쪽 단추로 클릭하고 추가, 클래스를 차례로 클릭합니다.



    새 클래스 추가

  2. 새 항목 추가 대화 상자에서 이름을 ToDoItem으로 입력하고 추가를 클릭합니다.



    새 클래스 추가

  3. 새 클래스가 프로젝트에 추가되었습니다. 기본 코드를 다음 코드로 바꿉니다.

    namespace ToDoListService.Models
    {
        public class ToDoItem
        {
            public string Title { get; set; }
            public string Owner { get; set; }
        }
    }
    
    

  1. 솔루션 탐색기에서 컨트롤러 폴더를 마우스 오른쪽 단추로 클릭하고 추가, 컨트롤러를 차례로 클릭합니다.



    새 컨트롤러 추가

  2. 컨트롤러 추가 대화 상자에서 이름을 ToDoListController로 입력합니다. 템플릿 드롭다운 메뉴에서 빈 읽기/쓰기 작업이 포함된 API 컨트롤러를 선택한 다음 추가를 클릭합니다.



    컨트롤러 추가

  3. ToDoListController.cs를 열고 기존 코드를 다음 코드로 바꿉니다.

    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Web.Http;
    using ToDoListService.Models;
    
    namespace ToDoListService.Controllers
    {
        public class ToDoListController : ApiController
        {
            // ToDo items list for all users
            static ConcurrentBag<ToDoItem> todoBag = new ConcurrentBag<ToDoItem>();
    
            // GET api/todolist returns 
            public IEnumerable<ToDoItem> Get()
            {
                Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier);
    
                return from todo in todoBag
                       where todo.Owner == subject.Value
                       select todo;
            }
    
            // POST api/todolist
            public void Post(ToDoItem todo)
            {
                if (null != todo && !string.IsNullOrWhiteSpace(todo.Title))
                {
                    todoBag.Add(new ToDoItem { Title = todo.Title, Owner = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value });
                }
            }
        }
    }
    
    이제 추가한 코드에 대해 설명합니다. ConcurrentBag 개체는 모든 사용자에 대해 할 일 항목을 저장하기 위한 스레드로부터 안전한 개체입니다. 실제 서비스에서 이러한 항목은 데이터베이스에 저장되지만 여기서는 작업을 쉽게 수행할 수 있도록 데이터를 메모리에 저장합니다.

    // Todo items list for all users
    static ConcurrentBag<ToDoItem> todoBag = new ConcurrentBag<ToDoItem>();
    
    할 일 항목을 저장할 위치를 지정하면 ConcurrentBag에 항목을 추가하는 기능을 포함하도록 Post() 메서드가 업데이트됩니다.

    // POST api/todolist
    public void Post(ToDoItem todo)
    {
        if (null != todo && !string.IsNullOrWhiteSpace(todo.Title))
        {
            todoBag.Add(new ToDoItem { Title = todo.Title, Owner = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value });
        }
    }
    
    메서드 서명은 ToDoItem 개체를 허용하도록 업데이트되었으며, 이로 인해 호출자에서 개체로의 폼 POST가 직렬화됩니다. 또한 이 메서드를 사용하면 제목이 비어 있는 ToDoItem이 추가되지 않습니다.

    ToDoItem이 만들어져 할 일 목록에 저장됩니다. 항목 소유자는 사용자에 대해 고유함이 보장되는 개체 식별자로 설정됩니다. 이 메서드의 ClaimsPrincipal은 이전에 Global.asax.cs에서 설정되었으며, Azure AD의 토큰에 포함된 사용자 ID를 반영합니다. 따라서 사용자는 자신의 할 일 항목에만 액세스할 수 있습니다.

    개체 ID, 즉 사용자 ID를 확인하는 데 사용되는 oid 클레임 유형은 Azure AD에서 사용자 계정의 개체 ID입니다.

    다음으로는 Get() 메서드에 대해 설명합니다.

            // GET api/todolist returns 
            public IEnumerable<ToDoItem> Get()
            {
                Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier);
    
                return from todo in todoBag
                       where todo.Owner == subject.Value
                       select todo;
            }
    
    
    서명은 IEnumerable<ToDoItem>(현재 사용자가 소유한 할 일 항목으로 설정됨)을 반환하도록 업데이트됩니다.

    현재 ClaimsPrincipal을 확인한 다음 해당 사용자가 소유한 항목만 반환하는 방식으로 현재 사용자의 ID를 확인합니다.

  4. F5 키를 눌러 서비스가 실행되는지 확인합니다.

이제 Web API를 작성했으므로 클라이언트 응용 프로그램을 작성할 수 있습니다. 이 응용 프로그램은 사용자에게 할 일 항목을 표시하고 새 항목을 추가할 수 있도록 하는 할 일 목록 클라이언트가 됩니다.

클라이언트 작성을 시작한 다음 Azure AD 테넌트로 전환하여 클라이언트와 서비스를 등록합니다. 할 일 목록 클라이언트에 사용할 클라이언트 ID를 Azure AD에서 가져오려면 이 작업을 수행해야 합니다.

아래 그림에는 작성할 응용 프로그램이 나와 있습니다. 사용자는 이 응용 프로그램을 통해 할 일 목록 클라이언트에 서비스 호출 권한을 부여할 수 있으며 작업을 추가하거나 확인할 수 있습니다.

할 일 목록 응용 프로그램

응용 프로그램 작성 단계는 다음과 같습니다.

  1. Windows 스토어 응용 프로그램용 프로젝트 만들기

  2. XAML에서 사용자 인터페이스 작성

  3. 코드 숨김에서 AAL을 사용하여 Web API에 인증

  4. 다른 사용자가 할 일 항목을 볼 수 있도록 응용 프로그램과 연결된 계정을 제거하는 옵션 추가

Windows 8 개발자 라이선스가 아직 없는 경우에는 응용 프로그램 작성을 시작하기 전에 라이선스를 얻어야 합니다. 이 라이선스는 무료이며 이 단계를 수행하면 얻을 수 있습니다.

  1. Visual Studio에서 기존 솔루션에 새 프로젝트를 만듭니다. 솔루션 탐색기에서 솔루션을 마우스 오른쪽 단추로 클릭하고 추가, 새 프로젝트를 차례로 클릭합니다.

  2. 새 프로젝트 추가 대화 상자 왼쪽의 Visual C# 드롭다운에서 Windows 스토어를 선택한 다음 새 응용 프로그램(XAML)을 클릭합니다.



    새 프로젝트 추가



    새 프로젝트 추가

  3. 프로젝트 이름을 "TodoListClient"로 지정하고 확인을 클릭합니다.

할 일 목록 클라이언트 프로젝트용으로 Windows 스토어용 AAL(Azure 인증 라이브러리) NuGet 패키지를 설치해야 합니다. NuGet 패키지를 열고 온라인에서 "AAL"을 검색한 다음 Windows 스토어용 AAL 패키지를 선택합니다.

AAL이 할 일 목록 클라이언트에서 작동하도록 하려면 응용 프로그램에 다음 기능을 부여해야 합니다.

  • 엔터프라이즈 인증

  • 인터넷(클라이언트)

  • 개인 네트워크(클라이언트 및 서버)

  • 공유 사용자 인증서

이러한 기능을 설정하려면 Package.appxmanifest 파일을 엽니다.

Package.appxmanifest 파일 열기

그런 다음 기능 탭으로 이동하여 필요한 기능을 선택합니다.

필요한 기능 선택

사용자가 AAD에 인증하여 할 일 목록 클라이언트에 서비스 호출 권한을 부여하려면 할 일 목록 클라이언트가 토큰을 획득하기 위해 AAL을 호출할 때 AAL이 Windows 웹 인증 브로커를 열어야 할 수 있습니다. 웹 인증 브로커는 Windows 스토어 응용 프로그램에서 로그인 웹 페이지를 렌더링하는 데 사용됩니다.

웹 인증 브로커에서 호스팅되는 웹 페이지에서 브로커에 등록된 콜백 URI로 이동하면 브로커는 닫힙니다. WebAuthenticationBroker.GetCurrentApplicationCallbackUri()를 호출하면 기본 콜백 URI를 확인할 수 있습니다. URI는 앱의 SID를 포함하며 형식은 다음과 같습니다. ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/

Azure AD는 이 URI를 사용하여 할 일 목록 클라이언트의 OAuth 2.0 요청에 응답합니다. 따라서 응답이 잘못된 응용 프로그램으로 반환되지 않도록 하려면 Azure AD에서 이 URI를 등록해야 합니다.

  1. MainPage.xaml.cs 파일에 다음 using 지시문을 추가합니다. using Windows.Security.Authentication.Web;

  2. MainPage.xaml.csOnNavigatedTo() 메서드가 다음과 같이 표시되도록 아래 코드를 추가합니다.

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        string redirectUri = WebAuthenticationBroker.GetCurrentApplicationCallbackUri().ToString();
    }
    
  3. 방금 추가한 코드 줄에 중단점을 설정하고 할 일 목록 클라이언트 프로젝트를 실행합니다.



    OnNavigatedTo 메서드

  4. 중단점에 도달하면 F11 키를 눌러 다음 단계의 코드를 실행하고 redirectUri의 값을 저장합니다. 이 값은 할 일 목록 클라이언트의 리디렉션 URI로, Azure AD에 클라이언트를 등록할 때 필요합니다.

    note참고
    Azure AD에서 앱에 대해 서비스 사용자를 올바르게 구성하려면 할 일 목록 클라이언트 앱 URL이 필요합니다. 예를 들면 다음과 같습니다. ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/

지금까지 할 일 목록 Web API 프로젝트와 할 일 목록 클라이언트를 만들었습니다. 다음으로는 Azure 관리 포털을 통해 이 프로젝트와 클라이언트를 Azure AD에 등록해야 합니다. 이 등록 프로세스를 수행하면 할 일 목록 클라이언트에 클라이언트의 사용자 대신 할 일 목록 Web API를 호출할 수 있는 권한이 부여됩니다. 자세히 설명하자면, 클라이언트의 사용자가 Azure AD에 인증한 후에 user_impersonation 권한이 있는 scp 클레임을 포함하는 토큰이 반환되므로 클라이언트에 Web API 호출 권한이 부여되는 것입니다.

  1. 웹 브라우저에서 https://manage.windowsazure.com으로 이동합니다.

  2. Azure 관리 포털에서 왼쪽 메뉴의 Active Directory 아이콘을 클릭하고 조직 이름을 클릭합니다.

  3. 빠른 시작 메뉴에서 응용 프로그램 탭을 클릭합니다.

  4. 명령 모음에서 추가를 클릭합니다.

  5. 마법사의 첫 페이지에 있는 이름 필드에 Web API의 이름을 입력합니다. Azure AD 디렉터리에 등록된 클라이언트 응용 프로그램을 확인할 때 이 이름을 통해 Web API를 쉽게 식별할 수 있습니다. 여기서는 이름을 ToDo List Web API로 지정합니다. 유형 아래에는 웹 응용 프로그램 및/또는 Web API 옵션이 이미 선택되어 있으므로 화살표를 클릭하여 계속 진행합니다.

  6. 다음 페이지에서 Web API의 앱 URL앱 ID URI에 대한 정보를 입력합니다. 앱 URL은 Web API의 주소이고 앱 ID URI는 앱의 논리적 식별자입니다. 여기서는 이 두 값이 같으므로 http://localhost:29062/로 설정하고 다음 화살표를 클릭합니다.

  7. 다음 페이지에서 Web API에 필요한 디렉터리 액세스 유형을 선택합니다. 여기서는 Graph API에 액세스하지 않을 것이므로 Single Sign-On을 선택한 다음 확인 표시 단추를 클릭하여 작업을 완료해야 합니다.

    Web API를 추가한 후에는 클라이언트 응용 프로그램을 등록해야 합니다. 다음 단계에서는 이 프로세스를 완료하는 방법을 설명합니다.

  8. Azure 관리 포털에서 조직 디렉터리의 응용 프로그램 탭으로 돌아갑니다.

  9. 명령 모음에서 추가를 클릭합니다.

  10. 마법사의 첫 페이지에서 클라이언트 응용 프로그램의 이름을 ToDo List Client와 같이 입력하고 유형 아래에서 네이티브 클라이언트 응용 프로그램 옵션을 선택합니다. 화살표를 클릭하여 계속합니다.

  11. 다음 페이지에서 클라이언트 응용 프로그램의 리디렉션 URI를 입력합니다. Azure AD는 이 URI에서 OAuth 2.0 요청에 대한 응답으로 리디렉션됩니다. 여기에는 이전 섹션에서 생성된 ms-app://s-1-15-2-1482697178-942858973-646103894-873360561-3829487091-4151517581-178579545/와 같은 ms-app://… 값을 사용합니다. 확인 표시 단추를 클릭합니다.

    이제 클라이언트 응용 프로그램이 Azure AD에 추가되었습니다. 다음 단계에서는 Web API에 대한 클라이언트 응용 프로그램의 액세스 권한을 구성하는 방법을 설명합니다.

  12. 클라이언트 응용 프로그램의 빠른 시작 페이지가 표시됩니다. 위쪽 메뉴에서 구성을 클릭하면 응용 프로그램 구성 페이지가 표시됩니다.

  13. 구성 페이지의 Web API 섹션에서 이 네이티브 클라이언트 응용 프로그램에 대해 Web API 액세스 구성의 드롭다운 메뉴를 확장하고 이전에 추가한 할 일 목록 Web API를 클릭합니다.

  14. 서비스를 선택한 후 명령 모음에서 저장을 클릭합니다.

계속해서 할 일 목록 클라이언트를 작성합니다.

클라이언트에서 가장 먼저 작성할 요소는 할 일 목록 클라이언트의 사용자 인터페이스입니다.

MainPage.xaml.cs 파일을 열고 기본 태그를 다음 태그로 바꿉니다.

<Page
    x:Class="ToDoListClient.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ToDoListClient"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="120"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="120"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Column="1" Grid.Row="1" Style="{StaticResource PageHeaderTextStyle}" Text="ToDo List"/>
        <Grid Grid.Column="1" Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition Width="300" />
            </Grid.ColumnDefinitions>
            <TextBox x:Name="ToDoText" Grid.Column="0" Grid.Row="2"  Margin="5" />
            <Button Grid.Column="1" Margin="10,0,0,0" Click="Button_Click_Add_Todo" >Add ToDo Item</Button>
            <HyperlinkButton Grid.Column="2" HorizontalAlignment="Right" Content="Remove Account" Click="HyperlinkButton_Click_Remove_Account"/>
        </Grid>
        <GridView x:Name="ToDoList" Grid.Column="1" Grid.Row="3" Margin="0,10,0,0" >
            <GridView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Width="200" Height="150" Margin="10" Background="#FFA2A2A4" >
                        <TextBlock Text="{Binding Title}" FontSize="24" TextWrapping="Wrap" Margin="10"/>
                    </StackPanel>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>
    </Grid>
</Page>

그러면 표 내에 간단한 표 레이아웃(열 3개/행 4개)이 적용됩니다.

  • 사용자는 <TextBox>에 할 일 항목 제목을 입력합니다.

  • <Button>은 할 일 항목을 제출하는 데 사용됩니다.

  • <HyperlinkButton>은 사용자 세션 데이터를 제거하는 데 사용됩니다.

  • <GridView>는 사용자의 모든 할 일 항목을 표시하는 데 사용됩니다. 코드 숨김 파일에서는 할 일 목록에서 GridView로 데이터를 바인딩합니다.

이제 UI 작성을 위한 태그가 완성되었습니다. 다음으로는 코드 숨김 파일을 추가합니다.

이제 Web API를 사용한 할 일 항목 추가 및 가져오기를 수행할 수 있도록 코드 숨김 파일을 추가합니다. 클라이언트 코드는 서비스에 액세스하기 위해 AAL을 사용하여 Azure AD의 권한 부여 토큰을 요청합니다.

MainPage.xaml.cs 파일을 열고 아래 코드를 추가합니다.

using Microsoft.Preview.WindowsAzure.ActiveDirectory.Authentication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using Windows.Data.Json;
using Windows.Security.Authentication.Web;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234238

namespace TodoListClient
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        //Tenant name
        const string domainName = "[Your Domain Name. Example: contoso.onmicrosoft.com]";

        //Client app information
        const string clientID = "[Your client id. Example: de119d9a-c0b9-4ac0-b794-ca82e9d7dcd4]";

        //Resource information
        const string resourceAppIDUri = "[Your service application Uri. Example: http://localhost:29062/]";
const string resourceBaseAddress = "[The base address at which your service can be reached. Example: http://localhost:29062/]";

        private HttpClient httpClient = new HttpClient();

        private AuthenticationContext authenticationContext = new AuthenticationContext("https://login.windows.net/" + domainName);

        public MainPage()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// Invoked when this page is about to be displayed in a Frame.
        /// </summary>
        /// <param name="e">Event data that describes how this page was reached.  The Parameter
        /// property is typically used to configure the page.</param>
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            GetTodoList();
        }

        // Get the current user's todo items
        private async void GetTodoList()
        {
            HttpResponseMessage response;

            // Acquire a token from AAL. AAL will cache the authorization state in a persistent cache.
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Call the todolist web api
            response = await httpClient.GetAsync(resourceBaseAddress + "/api/todolist");

            // Verify the call to the todo list service succeeded
            if (response.IsSuccessStatusCode)
            {
                // Read the response as a Json Array and databind to the GridView to display todo items
                var todoArray = JsonArray.Parse(await response.Content.ReadAsStringAsync());

                TodoList.ItemsSource = from todo in todoArray
                                       select new
                                       {
                                           Title = todo.GetObject()["Title"].GetString()
                                       };
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

        // Add a new todo item
        private async void Button_Click_Add_Todo(object sender, RoutedEventArgs e)
        {
            // Acquire a token from AAL. AAL will cache the authorization state
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Forms encode todo item, to POST to the todo list web api
            HttpContent content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("Title", TodoText.Text) });

            // Call the todolist web api
            var response = await httpClient.PostAsync(resourceBaseAddress + "/api/todolist", content);

            if (response.IsSuccessStatusCode)
            {
                TodoText.Text = "";
                GetTodoList();
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

        // Clear the authorization state in the application
        private void HyperlinkButton_Click_Remove_Account(object sender, RoutedEventArgs e)
        {
            // Clear session state from the token cache
            (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();

            // Reset UI elements
            TodoList.ItemsSource = null;
            TodoText.Text = "";
        }

        // Displays appropriate error when acquiring a token fails
        private async void DisplayErrorWhenAcquireTokenFails(AuthenticationResult result)
        {
            MessageDialog dialog;

            switch (result.Error)
            {
                case "authentication_canceled":
                    // User cancelled, no need to display a message
                    break;
                case "temporarily_unavailable":
                case "server_error":
                    dialog = new MessageDialog("Please retry the operation. If the error continues, please contact your administrator.", "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
                default:
                    // An error occurred when acquiring a token, show the error description in a MessageDialog 
                    dialog = new MessageDialog(string.Format("If the error continues, please contact your administrator.\n\nError: {0}\n\nError Description:\n\n{1}", result.Error, result.ErrorDescription), "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

        // Displays appropriate error when calling a resource service fails
        private async void DisplayErrorWhenCallingResourceServiceFails(HttpStatusCode statuscode)
        {
            MessageDialog dialog;

            switch (statuscode)
            {
                case HttpStatusCode.Unauthorized:

                    // An unauthorized error occurred, indicating the security token provided did not satisfy the service requirements
                    // acquiring a new token may fix the issue, this requires clearing state in the AAL cache.
                    // 
                    dialog = new MessageDialog("Would you like to reset your connection? This may help fix the problem.", "Sorry, accessing your todo list has hit a problem.");

                    dialog.Commands.Add(new UICommand("Yes", (c) =>
                    {
                        (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();
                        GetTodoList();
                    }));

                    dialog.Commands.Add(new UICommand("No"));

                    await dialog.ShowAsync();
                    break;

                default:
                    dialog = new MessageDialog("If the error continues, please contact your administrator.”, “Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }
    }
}
note참고
위의 코드를 실행하기 전에 다음 3개 필드를 설정해야 합니다.

  1. domainName: contoso.onmicrosoft.com과 같은 Azure AD 테넌트의 도메인 이름입니다.

  2. clientID: Azure AD에 클라이언트를 등록할 때 만들어진 3b616fda-41f9-47cc-8f88-35481e93c2dc와 같은 클라이언트 ID입니다. Azure 관리 포털의 응용 프로그램 구성 페이지에서 이 값을 가져올 수 있습니다.

  3. resourceAppIDUri: 할 일 목록 Web API의 앱 ID URI입니다. 이 값은 http://localhost:29062/와 같이 서비스 기준 주소와 동일한 값으로 설정하는 것이 표준 방법입니다.

  4. resourceBaseAddress: 할 일 목록 Web API의 기준 주소입니다. 이 값은 http://localhost:29062/와 같이 서비스가 호스팅되는 기준 주소와 일치해야 합니다.

다음으로는 코드 숨김에 추가한 코드에 대해 설명합니다. 먼저 다음 using 지시문이 MainPage.xaml.cs 맨 위에 추가되었습니다.

using Microsoft.Preview.WindowsAzure.ActiveDirectory.Authentication;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using Windows.Data.Json;
using Windows.Security.Authentication.Web;
using Windows.UI.Popups;

그리고 할 일 목록 클라이언트와 할 일 목록 서비스의 솔루션 관련 값이 추가되었습니다. 이러한 값에 대한 설명은 이전 섹션을 참조하십시오.

public sealed partial class MainPage : Page
{
    //Domain Name
    const string domainName = "[Your Domain Name. Example: contoso.onmicrosoft.com]";

    //Client app information
    const string clientID = "[Your client id. Example: de119d9a-c0b9-4ac0-b794-ca82e9d7dcd4]";

    //Resource information
    const string resourceAppIDUri = "[Your service application URI. Example: http://localhost:29062/]";
    const string resourceBaseAddress = "[The base address at which your service can be reached. Example: http://localhost:29062/]";

할 일 목록 서비스를 호출하기 위해 HttpClient의 전용 인스턴스 필드가 추가되었습니다. private HttpClient httpClient;

그리고 AuthenticationContext의 또 다른 전용 인스턴스 필드(AAL 클래스)가 추가되었습니다. 이 클래스는 테넌트 식별자로 구성되며, 올바른 Azure AD 테넌트에 대한 권한 부여를 요청하도록 AAL에 지시합니다. private AuthenticationContext authenticationContext = new AuthenticationContext("https://login.windows.net/" + domainName);

다음으로는 GetToDoList() 메서드를 살펴보겠습니다. 이 메서드는 할 일 항목을 검색하기 위해 할 일 목록 서비스를 호출합니다. 서비스에 인증을 할 수 있도록 AAL AuthenticationContext 클래스를 사용하여 토큰을 요청하는 호출이 수행됩니다.

할 일 목록 서비스의 앱 ID URI가 첫 번째 매개 변수로 전달됩니다. 두 번째 매개 변수인 할 일 목록 클라이언트의 clientID는 토큰 요청에서 호출자를 식별합니다.

이 호출은 비동기식으로 수행되므로 호출이 반환될 때까지 await 키워드를 사용하여 스레드를 차단합니다. 그러려면 GetTodoList() 메서드가 async 한정자를 사용해야 합니다.

        // Get the current user's todo items
        private async void GetTodoList()
        {
            HttpResponseMessage response;

            // Acquire a token from AAL. AAL will cache the authorization state in a persistent cache.
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

AcquireTokenAsync() 호출을 수행할 때는 다양한 작업이 수행될 수 있습니다. 이 호출을 처음 수행할 때는 Windows 웹 인증 브로커가 Azure AD 로그인 페이지를 열어서 표시합니다. 아래에 해당 페이지가 나와 있습니다.

조직 계정에 로그인

사용자가 한 번 로그인하고 나면 Azure AD에 인증된 사용자 대신 서비스를 호출하는 권한이 할 일 목록 클라이언트에 부여됩니다. 그러면 Azure AD에서 새로 고침 토큰이 클라이언트에 반환됩니다.

AAL 라이브러리는 Azure AD에서 반환된 새로 고침 토큰을 캐시합니다. 새로 고침 토큰이 캐시에 있으면 AcquireToken() 호출에서 사용자에게 토큰이 필요하다는 메시지를 다시 표시하지 않아도 됩니다.

AAL이 새로 고침 토큰을 사용하여 클라이언트의 액세스 토큰을 요청하면 서비스에 인증할 수 있도록 클라이언트에 대해 발급된 보안 토큰에 사용자와 클라이언트의 ID가 인코딩됩니다. 따라서 서비스가 두 ID 모두에 대해 호출 권한을 부여할 수 있습니다.

AcquireToken() 호출이 반환되고 나면 결과를 점검하여 호출 성공 여부를 확인합니다.

    // Verify that an access token was successfully acquired
    if (AuthenticationStatus.Succeeded != result.Status)
    {
    DisplayErrorWhenAcquireTokenFails(result);
    return;
    }

호출이 실패하면 호출에서 반환된 AuthorizationStatus를 사용하여 오류를 표시하기 위한 호출이 수행됩니다. DisplayErrorWhenAcquireTokenFails()를 사용하여 오류 확인을 수행한 다음 필요에 따라 사용자에게 메시지를 표시합니다. 메서드 본문이 아래에 나와 있습니다.

        // Displays appropriate error when acquiring a token fails
        private async void DisplayErrorWhenAcquireTokenFails(AuthenticationResult result)
        {
            MessageDialog dialog;

            switch (result.Error)
            {
                case "authentication_canceled":
// User cancelled, no need to display a message
                    break;
                case "temporarily_unavailable":
                case "server_error":
                    dialog = new MessageDialog("Please retry the operation. If the error continues, please contact your administrator.", "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
                default:
                    // An error occurred when acquiring a token, show the error description in a MessageDialog 
                    dialog = new MessageDialog(string.Format("If the error continues, please contact your administrator.\n\nError: {0}\n\n Error Description: {1}",result.Error, result.ErrorDescription), "Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

처리되는 오류 범주는 다음의 3가지입니다.

  • 사용자 취소: 사용자가 인증 대화 상자를 취소하면 "authentication_canceled" 오류가 반환됩니다. 이 작업은 사용자가 시작했으므로 사용자에게 메시지를 표시할 필요는 없습니다.

  • 다시 시도 가능한 오류: "temporarily_unavailable" 또는 "server_error"가 반환되면 높은 로드 또는 기타 원인으로 인해 Azure AD 서비스에서 일시적인 문제가 발생한 것일 수 있습니다. 이 경우 사용자는 작업을 다시 시도할 수 있습니다.

  • 기타: 일반적으로 구성 문제로 인해 발생하는 기타 모든 오류로, 작업을 다시 시도해도 문제가 해결될 가능성은 높지 않습니다.

이제 GetTodoList() 메서드로 돌아갑니다. 여기서는 오류가 발생하지 않았으며 AAL 캐시의 토큰이나 Azure AD의 새 토큰이 AAL에 의해 반환되었다고 가정합니다. 이 토큰을 사용하여 할 일 목록 서비스에 대한 요청에서 권한 부여 헤더를 설정합니다. 권한 부여 헤더를 설정할 때는 OAuth 2.0 전달자 토큰을 사용하여 리소스에 액세스하는 방법을 정의하는 RFC 6750 "OAuth 2.0 Authorization Framework: Bearer Token Usage"에 따라 토큰 앞에 "Bearer"가 추가됩니다.

// Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
// the resource service.
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

그런 다음 할 일 목록 서비스 Web API에 대해 HTTP GET을 실행하여 사용자의 할 일 항목 목록을 가져옵니다.

// Call the todolist web api
response = await httpClient.GetAsync(resourceBaseAddress + "/api/todolist");

호출이 정상적으로 실행되면 Linq 쿼리를 통해 할 일 목록 서비스에서 반환된 JSON 배열을 사용하여 UI의 GridView 개체에서 데이터 원본으로 사용할 수 있는 컬렉션을 생성합니다. 그러면 데이터 바인딩을 사용해 GridView가 자동으로 채워집니다. MainPage.xaml에 정의되어 있는 GridView ItemTemplate은 서비스에서 반환하는 할 일 항목의 Title 속성에 텍스트 상자를 바인딩합니다.

// Verify the call to the ToDo List Service succeeded
if (response.IsSuccessStatusCode)
{
    // Read the response as a Json Array and databind to the GridView to display todo items
    var todoArray = JsonArray.Parse(await response.Content.ReadAsStringAsync());

    TodoList.ItemsSource = from todo in todoArray
                                       select new
                                       {
                                           Title = todo.GetObject()["Title"].GetString()
                                       };
}
else
{
    DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
}

Web API 호출이 실패하면 DisplayErrorWhenCallingResourceServiceFails()가 호출되어 사용자에게 메시지를 표시합니다. 오류 처리 과정에서는 권한이 없음 오류가 반환되는 특수한 사례를 해결합니다. 서비스 액세스에 사용되는 토큰이 거부되면 할 일 목록 서비스가 이 오류를 처리합니다. 여기에는 토큰이 만료된 경우(AAL은 토큰 만료 전에 새 토큰을 요청하므로 이러한 경우가 발생할 가능성은 거의 없음) 또는 기타 이유로 인해 토큰이 유효성 검사를 통과하지 못한 경우가 포함됩니다. 이 경우 사용자에게는 코드를 통해 AAL 캐시를 지워서 토큰 획득을 다시 시작하는 옵션이 제공됩니다.

호출 중인 서비스의 동작에 따라 사례를 더 추가할 수 있습니다.

        // Displays appropriate error when calling a resource service fails
        private async void DisplayErrorWhenCallingResourceServiceFails(HttpStatusCode statuscode)
        {
            MessageDialog dialog;

            switch (statuscode)
            {
                case HttpStatusCode.Unauthorized:

                    // An unauthorized error occurred, indicating the security token provided did not satisfy the service requirements
                    // acquiring a new token may fix the issue, this requires clearing state in the AAL cache.
                    // 
                    dialog = new MessageDialog("Would you like to reset your connection? This may help fix the problem.", "Sorry, accessing your todo list has hit a problem.");

                    dialog.Commands.Add(new UICommand("Yes", (c) =>
                    {
                        (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();
                        GetTodoList();
                    }));

                    dialog.Commands.Add(new UICommand("No"));

                    await dialog.ShowAsync();
                    break;

                default:
                    dialog = new MessageDialog("If the error continues, please contact your administrator.”, “Sorry, an error has occurred.");
                    await dialog.ShowAsync();
                    break;
            }
        }

다음으로는 사용자의 할 일 목록에 새 할 일 항목을 추가하는 코드를 살펴보겠습니다. 이 메서드는 GetTodoList() 메서드 호출과 매우 비슷합니다. AAL의 AuthenticationContext 클래스를 사용하여 토큰을 획득합니다. AAL이 새로 고침 토큰을 이미 캐시한 경우에는 UI 메시지가 표시되지 않습니다. 이 토큰은 할 일 목록 서비스에 인증하는 데 사용되며, 할 일 항목을 포함하는 폼 인코딩된 메시지와 함께 POST 요청이 할 일 목록 서비스로 전송됩니다. 그러면 할 일 항목이 서비스의 사용자 할 일 목록에 추가됩니다.

        // Add a new todo item
        private async void Button_Click_Add_Todo(object sender, RoutedEventArgs e)
        {
            // Aquire a token from AAL. AAL will cache the authorization state
            AuthenticationResult result = await authenticationContext.AcquireTokenAsync(resourceAppIDUri, clientID);

            // Verify that an access token was successfully acquired
            if (AuthenticationStatus.Succeeded != result.Status)
            {
                DisplayErrorWhenAcquireTokenFails(result);
                return;
            }

            // Once the token has been returned by AAL, add it to the http authorization header, before making the call to access
            // the resoruce service.
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            // Forms encode todo item, to POST to the todolist web api
            HttpContent content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("Title", TodoText.Text) });

            // Call the todolist web api
            var response = await httpClient.PostAsync(resourceBaseAddress + "/api/todolist", content);

            if (response.IsSuccessStatusCode)
            {
                TodoText.Text = "";
                GetTodoList();
            }
            else
            {
                DisplayErrorWhenCallingResourceServiceFails(response.StatusCode);
            }
        }

할 일 추가 단추를 사용하여 UI의 클릭 이벤트를 통해 이 메서드를 호출합니다.

<Button Grid.Column="1" Margin="10,0,0,0" Click="Button_Click_Add_Todo" >Add ToDo</Button>

이제 사용자는 할 일 목록 서비스를 호출할 권한을 할 일 목록 클라이언트에 부여할 수 있으며 항목을 보고 목록에 추가할 수 있습니다. 다음으로는 클라이언트에서 새로 고침 토큰을 지우는 코드를 살펴보겠습니다. 이 코드는 AAL 캐시를 지우고 다른 사용자가 할 일 목록 클라이언트를 사용하도록 허용합니다. 토큰 캐시는 사용자 지정 구현으로 설정할 수 있으며 기본적으로 DefaultTokenCache 유형 캐시로 설정됩니다.

  // Clear the authorization state in the application
private void HyperlinkButton_Click_Remove_Account(object sender, RoutedEventArgs e)
{
    // Clear session state from the token cache
    (AuthenticationContext.TokenCache as DefaultTokenCache).Clear();

    // Reset UI elements
    TodoList.ItemsSource = null;
    TodoText.Text = "";
}

AAL의 일부분으로 제공되는 DefaultTokenCacheAuthenticationContext에서 사용되는 기본 토큰 캐시입니다. DefaultTokenCache는 Azure AAD 테넌트, 리소스 및 클라이언트를 기반으로 토큰을 캐시하므로 캐시에는 새로 고침 토큰과 액세스 토큰이 여러 개 있을 수 있습니다.

Microsoft 계정에 연결된 Windows 8 장치에서 실행 중인 앱에서 DefaultTokenCache를 사용하는 경우에는 캐시의 내용이 같은 계정에 연결된 컴퓨터 간에 로밍됩니다.

위의 코드는 AuthenticationContext TokenCache에 액세스하여 모든 권한 부여 상태를 지웁니다. 이 이벤트 처리기를 UI의 클릭 이벤트에 연결해야 합니다.

<HyperlinkButton Grid.Column="2" HorizontalAlignment="Right" Content="Remove Account" Click="HyperlinkButton_Click_Remove_Account"/>

이제 할 일 목록 클라이언트 코드가 완성되었으므로 서비스와 클라이언트를 테스트할 수 있습니다.

이제 할 일 목록 클라이언트를 테스트하겠습니다. 클라이언트와 서비스가 모두 실행되고 있는지 확인합니다. 클라이언트를 시작하면 아래 페이지가 표시됩니다. AAL을 처음 호출하면 웹 인증 브로커가 열리고, 사용자는 Azure AD 로그인 페이지에 자격 증명을 입력합니다.

조직 계정에 로그인

이제 사용자는 제목을 입력하고 할 일 항목 추가 단추를 클릭하여 할 일 목록에 항목을 입력할 수 있습니다.

할 일 목록 응용 프로그램 먼저 실행

단추를 클릭하면 AAL이 다시 호출됩니다. 이번에는 캐시된 토큰이 반환되어 할 일 목록 서비스를 호출하는 데 사용됩니다. 항목이 추가되고 사용자의 할 일 항목 목록이 반환됩니다.

할 일 목록 응용 프로그램 먼저 실행

다른 사용자로 할 일 클라이언트를 사용하려면 Azure AD 테넌트에 로그인합니다. Azure 구독 내에서 디렉터리를 만든 경우 Azure 관리 포털로 이동한 다음 테넌트 관리자 계정을 사용해 로그인합니다.

Active Directory 탭에서 자신의 디렉터리를 선택합니다.

아래쪽 도구 모음에서 사용자 추가를 선택하여 새 사용자를 만듭니다.

사용자 추가

사용자 만들기를 안내하는 대화 상자가 표시됩니다.

새 사용자 추가

계정이 추가되면 할 일 목록 클라이언트로 돌아가서 계정 제거 링크를 클릭합니다. 그러면 클라이언트에서 사용자 상태가 지워집니다.

이제 할 일 항목 추가 단추를 클릭하고 AAL을 호출하면 사용자에게 인증하라는 메시지가 표시됩니다. 이번에는 새 사용자 계정을 사용하여 인증합니다.

앱에 로그인

인증 후에는 새 사용자의 할 일 목록을 관리할 수 있습니다.

완료된 앱

사용자 계정 간을 전환하면서 서비스가 각 사용자에 대해 올바른 항목을 반환하는지 테스트할 수 있습니다.

이제 할 일 목록 응용 프로그램 작성이 완료되었습니다.

이 섹션에서는 Azure 웹 사이트에 할 일 목록 서비스를 배포하는 데 필요한 단계를 설명합니다. 이러한 단계는 원하는 경우 수행하면 됩니다. 이전에 Azure 웹 사이트를 만들지 않은 경우에는 이 문서를 참조하여 작업을 시작하십시오. 웹 사이트의 URL을 지정한 후에 작업을 중지하고 다음 단계를 수행하여 서비스의 대상 그룹을 업데이트하십시오.

서비스의 호스트 이름이 변경되므로 서비스의 앱 ID URI도 업데이트해야 합니다. 여기서는 예제 Azure 웹 사이트로 todolistservice.azurewebsites.net을 사용합니다. 이 사이트를 사용하려면 TodoListService 프로젝트를 변경해야 합니다. 프로젝트에서 Global.asax.cs를 열고 Azure 웹 사이트의 URL과 일치하도록 대상 그룹을 업데이트합니다. 예를 들어 대상 그룹을 다음과 같은 새 리소스 앱 ID URI로 업데이트합니다.

const string audience = "https://todolistservice.azurewebsites.net";

서비스를 이와 같이 변경하면 배포할 수 있는 상태가 됩니다.

리소스(할 일 목록 서비스)는 Azure로 배포하고 나면 새 앱 ID URI가 지정되므로 해당 변경 내용에 따라 클라이언트를 적절하게 업데이트해야 합니다. 또한 클라이언트가 새 URL을 사용하도록 업데이트해야 합니다.

MainPage.xaml.cs에서 다음과 같이 새 호스팅 주소를 사용하도록 리소스 상수를 업데이트합니다.

const string resourceAppIDUri = "https://todolistservice.azurewebsites.net";
const string resourceBaseAddress = "https://todolistservice.azurewebsites.net";

그러면 클라이언트가 서비스의 올바른 URL로 연결하며 AAL에서 이 서비스용 액세스 토큰을 요청합니다.

마지막 단계에서는 Azure 웹 사이트에서 실행 중인 Azure 호스팅 할 일 목록 서비스 인스턴스에 대해 서비스 등록을 추가합니다. 할 일 목록 클라이언트에도 Azure 호스팅 서비스를 호출하기 위한 권한을 부여해야 합니다.

구성을 이와 같이 변경하려면 Azure 관리 포털에서 Web API 정보를 업데이트해야 합니다.

  1. 웹 브라우저에서 https://manage.windowsazure.com으로 이동합니다.

  2. Azure 관리 포털에서 왼쪽 메뉴의 Active Directory 아이콘을 클릭하고 원하는 디렉터리를 클릭합니다.

  3. 디렉터리의 빠른 시작 페이지에서 페이지 위쪽의 응용 프로그램 탭을 클릭합니다.

  4. 등록된 응용 프로그램 목록에서 할 일 목록 서비스를 클릭합니다.

  5. 응용 프로그램의 빠른 시작 페이지에서 페이지 위쪽의 구성 탭을 클릭합니다.

  6. Single Sign-On 섹션에서 앱 ID URI 및 회신 URL을 http://todolistservice.azurewebsites.net 주소로 업데이트하고 명령 모음에서 저장을 클릭합니다.

이와 같이 변경한 후 할 일 목록 클라이언트를 실행하고 Azure 웹 사이트에 할 일 항목을 몇 개 저장하여 클라이언트를 테스트해 봅니다. 할 일 항목은 서비스의 메모리에 저장되므로 서비스를 재순환하면 항목이 사라집니다.

이것으로 이 연습을 완료합니다. 이 연습에서 사용된 기술에 대한 자세한 내용은 아래의 관련 항목을 참조하십시오. 또한 이 연습을 통해 완성되는 제품은 MSDN 코드 갤러리 코드 샘플: REST 서비스에 대한 Windows 스토어 응용 프로그램 - 브라우저 대화 상자를 통해 AAD로 인증

참고 항목

커뮤니티 추가 항목

표시:
© 2014 Microsoft