Power Virtual Agents에서 Azure Active Directory로 Single Sign-On 구성

Power Virtual Agents는 Single Sign-On(SSO)을 지원합니다. 즉, 봇이 배포된 페이지에 이미 로그인한 경우 챗봇이 사용자를 로그인할 수 있습니다.

예를 들어, 봇은 회사 인트라넷 또는 사용자가 이미 로그인한 앱에서 호스팅됩니다.

필수 구성 요소

기술 개요

다음 그림은 Power Virtual Agents에서 로그인 프롬프트(SSO)를 보지 않고 사용자가 로그인하는 방법을 보여줍니다.

SSO 인증 흐름 설명

  1. 봇 사용자는 로그인 토픽을 트리거하는 다음과 같은 문구를 입력합니다. 이 토픽은 사용자를 로그인하고 사용자의 인증된 토큰(AuthToken 변수)를 사용하도록 설계되었습니다.

  2. Power Virtual Agents는 사용자가 구성된 ID 공급자로 로그인할 수 있도록 로그인 프롬프트를 보냅니다.

  3. 봇의 맞춤 캔버스는 이 로그인 프롬프트를 인터셉트하고 Azure Active Directory(Azure AD)의 OBO(On-Behalf-Of) 토큰을 요청합니다. 캔버스는 봇에게 토큰을 보냅니다.

  4. OBO 토큰을 수령하면 봇은 OBO 토큰을 "액세스 토큰"과 교환하고 이 값을 사용하여 AuthToken 변수를 채웁니다. 이때 IsLoggedIn 변수도 설정됩니다.

Single Sign-On 구성

Power Virtual Agents용 SSO를 구성하기 위한 4 가지 주요 단계가 있습니다.

  1. 사용자 지정 캔버스에 대한 Azure AD에서 앱 등록을 만듭니다.

  2. 봇의 인증 앱 등록에서 봇의 사용자 지정 범위를 정의하십시오. 범위를 정의하면 캔버스와 인증 앱 등록 간에 신뢰 관계가 생성됩니다.

  3. Power Virtual Agents에서 인증을 구성하여 Single Sign-On을 활성화합니다.

  4. Single Sign-On을 사용하도록 사용자 지정 캔버스 HTML 코드를 구성하십시오.

사용자 지정 캔버스에 대한 Azure AD에서 앱 등록 만들기

Single Sign-On을 사용하려면 Azure AD에서 사용자 지정 캔버스를 앱으로 등록해야 합니다.

그러려면 Azure AD로 인증을 구성할 때 생성된 별도의 앱 등록이어야 합니다.

그런 다음 사용자 지정 캔버스를 가리키도록 앱 등록을 리디렉션해야 합니다.

봇의 캔버스를 위한 앱 등록 만들기

  1. Azure 포털에 로그인합니다.

  2. 아이콘을 선택하거나 상단 검색 창에서 검색하여 앱 등록으로 이동합니다.

  3. 새 등록을 선택합니다.

    새 등록 버튼이 강조 표시된 앱 등록 블레이드의 스크린샷

  4. 등록의 이름을 입력하십시오. 등록하려는 캔버스의 봇 이름을 사용하고 "캔버스"를 포함하여 인증을 위한 앱 등록과 구분하는 데 도움이 될 수 있습니다.
    예를 들어 봇의 이름이 "Contoso sales help"인 경우 앱 등록 이름을 "ContosoSalesCanvas" 또는 이와 유사한 것으로 지정할 수 있습니다.

  5. 모든 조직 디렉터리의 계정(모든 Azure AD 디렉터리 - 다중 테넌트) 및 개인 Microsoft 계정(예: Skype, Xbox) 을 선택합니다.

  6. 다음 단계에서 해당 정보를 입력하므로 지금은 리디렉션 URI 섹션을 비워 두십시오. 등록을 선택합니다.

리디렉션 URL 추가

  1. 등록이 완료되면 개요 페이지에서 열립니다. 인증으로 이동한 다음 플랫폼 추가를 선택합니다.

  2. 플랫폼 구성 블레이드에서 을 선택합니다.

  3. 리디렉션 URI 아래에서 채팅 캔버스가 호스팅되는 페이지에 전체 URL을 추가하십시오. 암시적 허용 섹션에서 ID 토큰액세스 토큰 확인란을 선택합니다.

  4. 구성을 선택하여 변경 내용을 확인합니다.

  5. API 권한으로 이동합니다. <your tenant name>에 대해 관리자 동의 부여를 선택한 다음 를 선택합니다.

    중요

    사용자가 각 애플리케이션에 동의하해야 하는 것을 방지하려면 전역 관리자, 애플리케이션 관리자 또는 클라우드 애플리케이션 관리자가 앱 등록에 테넌트 수준 동의를 부여해야 합니다.

봇에 대한 사용자 지정 범위 정의

인증 앱 등록 내에서 캔버스 앱 등록에 대한 API를 노출하여 사용자 지정 범위를 정의하십시오. 범위를 사용하여 사용자 및 관리자 역할과 액세스 권한을 결정할 수 있습니다.

이 단계에서는 인증용 인증 앱 등록과 사용자 지정 캔버스의 앱 등록 간에 신뢰 관계를 생성합니다.

봇에 대한 사용자 지정 범위 정의

  1. 인증을 구성할 때 만든 앱 등록을 엽니다.

  2. API 권한으로 이동하여 봇에 올바른 권한이 추가되었는지 확인하십시오. <your tenant name>에 대해 관리자 동의 부여를 선택한 다음 를 선택합니다.

    중요

    사용자가 각 애플리케이션에 동의하해야 하는 것을 방지하려면 전역 관리자, 애플리케이션 관리자 또는 클라우드 애플리케이션 관리자가 앱 등록에 테넌트 수준 동의를 부여해야 합니다.

  3. API 노출로 이동하여 범위 추가를 선택합니다.

  4. Single Sign-On 화면이 표시될 때 사용자에게 표시되어야 하는 표시 정보와 함께 범위 이름을 입력하십시오. 범위 추가를 선택합니다.

  5. 클라이언트 응용 프로그램 추가를 선택합니다.

  6. 캔버스 앱 등록에 대한 개요 페이지의 응용 프로그램(클라이언트) ID클라이언트 ID 필드에 입력합니다. 생성한 나열된 범위의 확인란을 선택하십시오.

  7. 응용 프로그램 추가를 선택합니다.

Power Virtual Agents에서 인증을 구성하여 Single Sign-On 활성화

Power Virtual Agents 인증 구성 페이지의 토큰 교환 URL는 Bot Framework를 통해 요청된 액세스 토큰에 대한 OBO 토큰을 교환하는 데 사용됩니다.

이것은 Azure AD로 호출되어 실제 교환을 수행합니다.

봇의 인증 페이지에 토큰 교환 URL 추가

  1. Power Virtual Agents에 로그인합니다.

  2. 상단 메뉴에서 봇 아이콘을 선택하고 올바른 봇을 선택하여 인증을 활성화하려는 봇을 선택했는지 확인하십시오.

  3. 측면 탐색 창에서 관리를 선택하고 인증 탭으로 이동합니다.

    관리로 이동한 다음 인증으로 이동

  4. 토큰 교환 URL 필드에서 캔버스 앱 등록에 대한 API 노출 블레이드의 전체 범위 URI를 입력하십시오. api://1234-4567/scope.name의 형식입니다.

  5. 저장을 선택하고 봇 콘텐츠를 게시합니다.

Single Sign-On을 사용하도록 사용자 지정 캔버스 HTML 코드 구성

로그인 카드 요청을 차단하고 OBO 토큰을 교환하도록 봇이 있는 사용자 지정 캔버스 페이지를 업데이트하십시오.

  1. 다음 코드를 <head> 섹션에 <script> 태그에 추가하여 MSAL(Microsoft Authentication Library)을 구성합니다.

  2. 캔버스 앱 등록을 위해 응용 프로그램(클라이언트) IDclientId를 업데이트합니다. <Directory ID>디렉터리(테넌트) ID로 교체합니다. 이 ID는 캔버스 앱 등록의 개요 페이지에서 얻을 수 있습니다.

    <head>
     <script>
       var clientApplication;
         (function () {
           var msalConfig = {
               auth: {
                 clientId: '<Client ID [CanvasClientId]>',
                 authority: 'https://login.microsoftonline.com/<Directory ID>'
               },
               cache: {
                 cacheLocation: 'localStorage',
                 storeAuthStateInCookie: false
               }
           };
           if (!clientApplication) {
             clientApplication = new Msal.UserAgentApplication(msalConfig);
           }
         } ());
     </script>
    </head>
    
  3. <body> 섹션에 다음 <script>를 삽입합니다. 이 스크립트는 메서드를 호출하여 resourceUrl을 검색하고 현재 토큰을 OAuth 프롬프트에서 요청한 토큰으로 교환합니다.

    <script>
    function getOAuthCardResourceUri(activity) {
      if (activity &&
           activity.attachments &&
           activity.attachments[0] &&
           activity.attachments[0].contentType === 'application/vnd.microsoft.card.oauth' &&
           activity.attachments[0].content.tokenExchangeResource) {
             // asking for token exchange with AAD
             return activity.attachments[0].content.tokenExchangeResource.uri;
       }
    }
    
    function exchangeTokenAsync(resourceUri) {
      let user = clientApplication.getAccount();
       if (user) {
         let requestObj = {
           scopes: [resourceUri]
         };
         return clientApplication.acquireTokenSilent(requestObj)
           .then(function (tokenResponse) {
             return tokenResponse.accessToken;
             })
             .catch(function (error) {
               console.log(error);
             });
             }
             else {
             return Promise.resolve(null);
       }
    }
    </script>
    
  4. <body> 섹션에 다음 <script>를 삽입합니다. main 메서드 내에서 봇의 고유 식별자와 함께 store에 조건부를 추가합니다. 또한 고유 ID를 userId 변수로 생성합니다.

  5. <BOT ID>를 봇 ID로 업데이트합니다. 사용 중인 봇의 채널 탭으로 이동하고 Power Virtual Agents 포털에서 모바일 앱을 선택하여 봇의 ID를 볼 수 있습니다.

    <script>
    
    (async function main() {
    
          // Add your BOT ID below 
          var BOT_ID = "<BOT ID>";
          var theURL = "https://powerva.microsoft.com/api/botmanagement/v1/directline/directlinetoken?botId=" + BOT_ID;
    
       const { token } = await fetchJSON(theURL);
       const directLine = window.WebChat.createDirectLine({ token });
       var userID = clientApplication.account?.accountIdentifier != null ? ("Your-customized-prefix-max-20-characters" + clientApplication.account.accountIdentifier).substr(0,64) : (Math.random().toString() + Date.now().toString().substr(0,64)  // Make sure this will not exceed 64 characters 
            const store = WebChat.createStore({}, ({ dispatch }) => next => action => {
             const { type } = action;
             if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
               dispatch({
                 type: 'WEB_CHAT/SEND_EVENT',
                 payload: {
                   name: 'startConversation',
                   type: 'event',
                   value: { text: "hello" }
                 }
               });
               return next(action);
             }
             if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
                 const activity = action.payload.activity;
                 let resourceUri;
                 if (activity.from && activity.from.role === 'bot' &&
                    (resourceUri = getOAuthCardResourceUri(activity))) {
                    exchangeTokenAsync(resourceUri).then(function (token) {
                     if (token) {
                     directLine.postActivity({
                     type: 'invoke',
                     name: 'signin/tokenExchange',
                     value: {
                       id: activity.attachments[0].content.tokenExchangeResource.id,
                       connectionName: activity.attachments[0].content.connectionName,
                       token
                      },
                     "from":{
                       id:userId,  
                       name:clientApplication.account.userName,
                       role:"user"
                     }
                     }).subscribe(
                 id => {
                  return next(action);
      });
      const styleOptions = {
    
        //Add styleOptions to customize Web Chat canvas
        hideUploadButton: true
      };
    
          window.WebChat.renderWebChat(
            {
              directLine: directLine,
              store,
              userID:userId,  
              styleOptions
            },
            document.getElementById('webchat')
          );      
    })().catch(err => console.error("An error occurred: " + err));
    
    </script>
    

전체 샘플 코드

참고로, MSAL 및 저장 조건부 스크립트가 이미 GitHub 리포지토리에 포함된 전체 샘플 코드를 찾을 수 있습니다.