如何从Web应用程序访问使用Azure AD B2C保护的两个独立Web API

如何从Web应用程序访问使用Azure AD B2C保护的两个独立Web API,azure,azure-active-directory,azure-ad-b2c,Azure,Azure Active Directory,Azure Ad B2c,我们有两个SePreate dotnet核心API(API1和API2),它们使用azure ad b2c进行保护。这两个API都在b2c租户上注册,并公开其作用域。 我们有一个客户端web应用程序,用于访问上述受保护的API。此web应用程序已在b2c租户中注册为应用程序,并已为上述定义了适当范围的api设置了api权限 我们使用带有signinpolicy的MSAL.net将用户登录到web应用程序。 身份验证调用需要指定作用域。因此,我们在调用中添加了API1的作用域。 (注意:可以在如下

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

我们使用带有signinpolicy的MSAL.net将用户登录到web应用程序。 身份验证调用需要指定作用域。因此,我们在调用中添加了API1的作用域。 (注意:可以在如下所示的身份验证调用中添加单个资源的一个作用域)

Startup.Auth.cs中的OnAuthorizationCodeReceived方法接收到由于上述身份验证调用而接收到的代码,并使用它根据提供的作用域获取访问令牌,并将其存储在缓存中。如下所示

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()
            }
        );
    }
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,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);
        }
    }

公共异步任务

单个访问令牌只能包含单个访问群体的作用域

您有两种选择:

  • 将这两项服务合并到单个应用程序注册中,并公开不同的作用域
  • 请求多个令牌-每个服务一个令牌。如果在B2C中正确配置了SSO策略,则应在用户不知情的情况下以静默方式执行此操作
  • 如果您同时拥有这两项服务,我建议您使用选项1(听起来您是这样做的)。与此选项相关的一些提示

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

    如何验证访问令牌的作用域?Auth0的这篇文章提供了一个非常好的教程,介绍了如何创建自定义授权属性,该属性从令牌中获取作用域声明(“scp”),并验证每个控制器方法的作用域。如果每个服务只有一个作用域,那么这当然可以在全球范围内完成。