【问题标题】:How to access two separate Web APIs protected using Azure AD B2C from a web app如何从 Web 应用访问使用 Azure AD B2C 保护的两个单独的 Web API
【发布时间】:2020-11-25 11:26:10
【问题描述】:

我们有两个单独的 dotnet 核心 API(API1 和 API2),它们使用 azure ad b2c 进行保护。这两个 api 都在 b2c 租户上注册并暴露了它们的范围。 我们有一个客户端 Web 应用程序可以访问上述受保护的 api。此 Web 应用已在 b2c 租户中注册为应用程序,并为上述 API 设置了 API 权限,并定义了适当的范围。

我们使用带有 signinpolicy 的 MSAL.net 将用户登录到 Web 应用程序。 身份验证调用需要提及范围。所以我们在调用中添加 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()
            }
        );
    }

Startup.Auth.cs 中的 OnAuthorizationCodeRecieved 方法接收到作为上述 auth 调用结果的代码,并使用它根据提供的范围获取访问令牌并将其存储在缓存中。如下图

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 & 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 的访问令牌,因为它的范围在 auth 调用中被提及。尝试获取 API2 的访问令牌时,它返回 null。

问题:如何配置 web 应用程序,使其能够访问多个受保护的 api?

请提出建议。

代码可以从示例https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi中找到

【问题讨论】:

    标签: azure azure-active-directory azure-ad-b2c


    【解决方案1】:

    单个访问令牌只能包含单个受众的范围。

    你有两个选择:

    1. 将这两个服务组合到一个应用注册中并公开不同的范围。
    2. 请求多个令牌 - 每个服务一个。如果您的 SSO 策略在 B2C 中配置正确,这应该会在用户不知情的情况下静默发生。

    如果您同时拥有这两项服务(听起来像您这样做),我建议您使用选项 1。与此选项相关的一些提示。

    • 在组合应用注册中声明范围时,请使用点语法 {LogicalService}.{Operation}。如果这样做,范围将按 Azure 门户中的逻辑服务进行分组。
    • 确保您正在验证服务中的范围。仅验证观众是不够的,并且会允许攻击者使用绑定到另一个服务的令牌进行横向移动。

    【讨论】:

    • 如何验证访问令牌的范围?
    • 这篇来自 Auth0 的文章有一个非常好的教程,介绍了如何创建一个自定义 Authorize 属性,该属性从令牌中获取范围声明(“scp”)并验证每个控制器方法的范围。如果每个服务只有 1 个作用域,这当然可以在全球范围内完成。 auth0.com/docs/quickstart/backend/aspnet-core-webapi/…
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-11-27
    • 1970-01-01
    • 1970-01-01
    • 2023-01-19
    • 2018-06-25
    • 2021-02-28
    • 2020-02-26
    相关资源
    最近更新 更多