Как получить доступ к двум отдельным веб-API, защищенным с помощью Azure AD B2C, из веб-приложения

#azure #azure-active-directory #azure-ad-b2c

#azure #azure-active-directory #azure-ad-b2c

Вопрос:

У нас есть два отдельных основных API-интерфейса dotnet (API1 и API2), которые защищены с помощью Azure ad b2c. Оба этих API зарегистрированы на клиенте b2c, и их области действия открыты. У нас есть клиентское веб-приложение, предназначенное для доступа к вышеуказанным защищенным API. Это веб-приложение было зарегистрировано как приложение в клиенте b2c и имеет разрешения api, установленные для вышеуказанных API, с соответствующими определенными областями.

Мы используем MSAL.net с помощью политики входа для входа пользователя в веб-приложение. для вызова аутентификации требуются упомянутые области. Итак, мы добавляем область API1 в вызов. (примечание: одна область одного ресурса может быть добавлена в вызов аутентификации, показанный ниже)

 public void ConfigureAuth(IAppBuilder app)
    {
        // Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            // ASP.NET web host compatible cookie manager
            CookieManager = new SystemWebChunkingCookieManager()
        });

        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                // Generate the metadata address using the tenant and policy information
                MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.Tenant, Globals.DefaultPolicy),

                // These are standard OpenID Connect parameters, with values pulled from web.config
                ClientId = Globals.ClientId,
                RedirectUri = Globals.RedirectUri,
                PostLogoutRedirectUri = Globals.RedirectUri,

                // Specify the callbacks for each type of notifications
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    RedirectToIdentityProvider = OnRedirectToIdentityProvider,
                    AuthorizationCodeReceived = OnAuthorizationCodeReceived,
                    AuthenticationFailed = OnAuthenticationFailed,
                },

                // Specify the claim type that specifies the Name property.
                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    ValidateIssuer = false
                },

                // Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
                Scope = $"openid profile offline_access {Globals.ReadTasksScope} {Globals.WriteTasksScope}",

                // ASP.NET web host compatible cookie manager
                CookieManager = new SystemWebCookieManager()
            }
        );
    }
 

Метод OnAuthorizationCodeRecieved в Startup.Auth.cs получил код, полученный в результате вышеуказанного вызова аутентификации, и использует его для получения токена доступа на основе предоставленных областей и сохраняет его в кэше. показано ниже

 private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
    {
        try
        {
            /*
             The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
             At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
             but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
             receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
             This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
             Azure AD and has a full set of claims.
             */
            IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));

            // Upon successful sign in, get amp; cache a token using MSAL
            AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();
            

        }
        catch (Exception ex)
        {
            throw new HttpResponseException(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.BadRequest,
                ReasonPhrase = $"Unable to get authorization code {ex.Message}.".Replace("n", "").Replace("r", "")
            });
        }
    }
 

Затем этот токен доступа используется в TasksController для вызова AcquireTokenSilent, который извлекает токен доступа из кэша, который затем используется в вызове api.

 public async Task<ActionResult> Index()
    {
        try
        {
            // Retrieve the token with the specified scopes
            var scope = new string[] { Globals.ReadTasksScope };
            
            IConfidentialClientApplication cca = MsalAppBuilder.BuildConfidentialClientApplication();
            var accounts = await cca.GetAccountsAsync();
            AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault()).ExecuteAsync();
            
            HttpClient client = new HttpClient();
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiEndpoint);

            // Add token to the Authorization header and make the request
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
            HttpResponseMessage response = await client.SendAsync(request);

            // Handle the response
            switch (response.StatusCode)
            {
                case HttpStatusCode.OK:
                    String responseString = await response.Content.ReadAsStringAsync();
                    JArray tasks = JArray.Parse(responseString);
                    ViewBag.Tasks = tasks;
                    return View();
                case HttpStatusCode.Unauthorized:
                    return ErrorAction("Please sign in again. "   response.ReasonPhrase);
                default:
                    return ErrorAction("Error. Status code = "   response.StatusCode   ": "   response.ReasonPhrase);
            }
        }
        catch (MsalUiRequiredException ex)
        {
            /*
                If the tokens have expired or become invalid for any reason, ask the user to sign in again.
                Another cause of this exception is when you restart the app using InMemory cache.
                It will get wiped out while the user will be authenticated still because of their cookies, requiring the TokenCache to be initialized again
                through the sign in flow.
            */
            return new RedirectResult("/Account/SignUpSignIn?redirectUrl=/Tasks");
        }
        catch (Exception ex)
        {
            return ErrorAction("Error reading to do list: "   ex.Message);
        }
    }
 

Проблема заключается в том, что код, полученный методом OnAuthorizationCodeRecieved, может использоваться только для получения токена доступа для API1, поскольку его область действия была упомянута в вызове аутентификации. При попытке получить токен доступа для API2 он возвращает null.

Вопрос: Как настроить веб-приложение, чтобы оно могло получать доступ к нескольким защищенным API?

Пожалуйста, предложите.

Код можно найти в примере https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi

Ответ №1:

Один токен доступа может содержать области только для одной аудитории.

У вас есть 2 варианта:

  1. Объедините обе службы в одну регистрацию приложения и предоставьте разные области.
  2. Запросить несколько токенов — по одному на службу. Если ваша политика единого входа правильно настроена в B2C, это должно происходить незаметно для пользователя.

Я рекомендую использовать вариант 1, если вы являетесь владельцем обеих служб (что, похоже, у вас есть). Несколько советов, связанных с этой опцией.

  • При объявлении областей в объединенной регистрации приложения используйте синтаксис точки {LogicalService}.{Операция}. Если вы сделаете это, области будут сгруппированы по логическим службам на портале Azure.
  • Убедитесь, что вы проверяете области в своей службе. Проверка только аудитории недостаточно хороша и позволит злоумышленнику совершать боковые перемещения с помощью токена, привязанного к другой службе.

Комментарии:

1. Как мне проверить область действия токена доступа?

2. В этой статье из Auth0 есть действительно хороший учебник о том, как создать пользовательский атрибут авторизации, который извлекает утверждение области видимости («scp») из токена и проверяет область видимости для каждого метода контроллера. Это, безусловно, можно сделать более глобально, если у вас есть только 1 область для каждой службы. auth0.com/docs/quickstart/backend/aspnet-core-webapi /…

3. Не могли бы вы также рассказать мне, как работать с несколькими API с Azure B2C. У меня есть производственная и тестовая платформа (означает две ссылки на API), так как я могу достичь этой цели, чтобы из интерфейсного приложения оно переходило к обоим в соответствии с нашими потребностями?

4. Я рекомендую использовать несколько клиентов B2C — по одному для каждой среды. Поначалу кажется сложной задачей синхронизировать регистрации приложений, пользовательские политики и веб-файлы, но вы можете автоматизировать развертывание обоих с помощью Graph API и просто поменять местами переменные, зависящие от среды. Более высокие первоначальные инвестиции, но окупятся в долгосрочной перспективе.