无法从都由Azure AD B2C保护的MVC Web应用程序访问WebApi [英] Unable to access a WebApi from a MVC Web app where both are secured by Azure AD B2C

查看:44
本文介绍了无法从都由Azure AD B2C保护的MVC Web应用程序访问WebApi的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个Web应用程序(MVC)和一个WebApi,它们都由同一租户中的ADB2C保护.该Web应用程序希望使用简单的HttpClient调用WebApi. 这是具有最新Visaul Studio项目模板的.NET Core 2.1.

I have a web app (MVC) and a WebApi that are both secured by ADB2C in the same tenant. The web app wants to call the WebApi with a simple HttpClient. This is .NET Core 2.1 with the latest Visaul Studio project templates.

可以提出以下几点:

  1. 我可以使用B2C成功登录并注册Web应用.我正在使用新的.NET Core 2.1模板,其中B2C用最少的代码脚手架安装到了项目中.

  1. I can successfully sign in and sign up to the web app using B2C. I am soley using the new .NET Core 2.1 template where B2C is scaffolded into the project with minimum code.

我也使用向导创建了WebApi项目(此WebAPI的名称).然后,我可以成功地使用Postman测试在WebApi中签名,其中Postman在租户中注册为自己的Web应用程序.

I create the WebApi project using the wizard also (this tiem for a WebAPI). I can then sucessfully use Postman to test signing in the WebApi, where Postman is registered in the tenant as its own web app. This is described here

我仅在本地计算机上测试所有这一切,但尚未部署到azurewebsites.net

I am testing all this solely on my local machine, I have not yet deployed to azurewebsites.net

我可以判断该网络应用正在使用

I can tell that the web app is using

services.AddAuthentication( AzureADB2CDefaults.AuthenticationScheme )
                .AddAzureADB2C( options =>
                {
                    Configuration.Bind( "AzureAdB2C", options );
                } );

WebApi startup.cs使用了承载令牌:

The WebApi startup.cs is using the bearer token:

services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
                .AddAzureADB2CBearer(options =>
                {
                    Configuration.Bind( "AzureAdB2C", options );
                } );

因此,现在我的Web应用程序中有一个控制器动作,该动作要在WebApi项目中调用ValuesController以获得虚拟值.

So now I have a controller action in my web app that wants to call the ValuesController in the WebApi project to get the dummy values.

这是我不理解的,因为使用以下代码,我能够获取令牌:

This is what I don't understand, because using the following code, I am able to acquire the token:

var httpClient = new HttpClient
            {
                BaseAddress = new Uri( _configuration["WebApi:BaseUrl"] )
            };

            var clientId = _configuration["AzureAdB2C:ClientId"]; //the client ID for the web app (not web api!)
            var clientSecret = _configuration["AzureAdB2C:ClientSecret"]; //the secret for the web app
            var authority = _configuration["AzureAdB2C:Authority"]; //the instance name and the custom domain name for this tenant
            var id = _configuration["WebApi:Id"]; //the complete Url including the suffix of the web api

            var authContext = new AuthenticationContext( authority );
            var credentials = new ClientCredential( clientId, clientSecret );
            var authResult = await authContext.AcquireTokenAsync( id, credentials );

            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", authResult.AccessToken );

至此,我有了令牌.我可以使用 http://jwt.ms 解密令牌,但该令牌不包含声明.不明白为什么会这样.

At this point I have the token. I can decrypt the token using http://jwt.ms but the token does not contain claims. Don't understand why that is.

但是当我调用GetStringAsync()时,在ValuesController中的Get()操作上收到了401未经授权的异常.

But when I call GetStringAsync() I get a 401 not authorized exception on the Get() action in the ValuesController.

//this fails with a 401
var result = await _httpClient.GetStringAsync( "api/values" );
//HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).

对于上面代码中的"id",我使用的是完整的WebApi URL,如Azure门户中WebApi的属性所示.

在"API访问权限"下,我已授予Web应用程序对门户网站中的Web api应用程序的访问权限.

我想念什么?我不明白为什么邮递员可以工作(已在ADB2C中注册为网络应用程序),而这不是

这些是API Access中定义的范围:

These are the scopes desfined in API Access:

推荐答案

于2018年6月27日开始编辑

我已经在 GitHub 上创建了一个代码示例,用于ASP.NET Core 2.1 Web应用程序,该Web应用程序使用针对Azure AD B2C的ASP.NET Core 2.1身份验证中间件针对Azure AD B2C对最终用户进行身份验证,使用MSAL.NET获取访问令牌,然后使用此访问令牌访问Web API.

I've created a code sample at GitHub for an ASP.NET Core 2.1 web application that authenticates an end user against Azure AD B2C using the ASP.NET Core 2.1 authentication middleware for Azure AD B2C, acquires an access token using MSAL.NET, and accesses a web API using this access token.

以下答案总结了代码示例已实现的内容.

The following answer summarizes what has been implemented for the code sample.

于2018年6月27日结束编辑

要使ASP.NET Core 2.1 Web应用程序获取用于API应用程序的访问令牌,则必须:

For a ASP.NET Core 2.1 web application to acquire an access token for use with an API application, then you must:

  1. 创建一个选项类,以使用API​​选项扩展Azure AD B2C身份验证选项:

public class AzureADB2CWithApiOptions : AzureADB2COptions
{
    public string ApiScopes { get; set; }

    public string ApiUrl { get; set; }

    public string Authority => $"{Instance}/{Domain}/{DefaultPolicy}/v2.0";

    public string RedirectUri { get; set; }
}

  1. 将这些API选项添加到 appsettings.json 文件中:
  1. Add these API options to the appsettings.json file:

{
  "AllowedHosts": "*",
  "AzureADB2C": {
    "ApiScopes": "https://***.onmicrosoft.com/demoapi/demo.read",
    "ApiUrl": "https://***.azurewebsites.net/hello",
    "CallbackPath": "/signin-oidc",
    "ClientId": "***",
    "ClientSecret": "***",
    "Domain": "***.onmicrosoft.com",
    "EditProfilePolicyId": "b2c_1_edit_profile",
    "Instance": "https://login.microsoftonline.com/tfp",
    "RedirectUri": "https://localhost:44316/signin-oidc",
    "ResetPasswordPolicyId": "b2c_1_reset",
    "SignUpSignInPolicyId": "b2c_1_susi"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

  1. 创建一个配置类,该类实现IConfigureNamedOptions<OpenIdConnectOptions>接口,该接口为Azure AD B2C身份验证中间件配置OpenID Connect身份验证选项:
  1. Create a configuration class, which implements the IConfigureNamedOptions<OpenIdConnectOptions> interface, that configures the OpenID Connect authentication options for the Azure AD B2C authentication middleware:

public class AzureADB2COpenIdConnectOptionsConfigurator : IConfigureNamedOptions<OpenIdConnectOptions>
{
    private readonly AzureADB2CWithApiOptions _options;

    public AzureADB2COpenIdConnectOptionsConfigurator(IOptions<AzureADB2CWithApiOptions> optionsAccessor)
    {
        _options = optionsAccessor.Value;
    }

    public void Configure(string name, OpenIdConnectOptions options)
    {
        options.Events.OnAuthorizationCodeReceived = WrapOpenIdConnectEvent(options.Events.OnAuthorizationCodeReceived, OnAuthorizationCodeReceived);
        options.Events.OnRedirectToIdentityProvider = WrapOpenIdConnectEvent(options.Events.OnRedirectToIdentityProvider, OnRedirectToIdentityProvider);
    }

    public void Configure(OpenIdConnectOptions options)
    {
        Configure(Options.DefaultName, options);
    }

    private static Func<TContext, Task> WrapOpenIdConnectEvent<TContext>(Func<TContext, Task> baseEventHandler, Func<TContext, Task> thisEventHandler)
    {
        return new Func<TContext, Task>(async context =>
        {
            await baseEventHandler(context);
            await thisEventHandler(context);
        });
    }

    private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
    {
        var clientCredential = new ClientCredential(context.Options.ClientSecret);
        var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
        var userTokenCache = new SessionTokenCache(context.HttpContext, userId);

        var confidentialClientApplication = new ConfidentialClientApplication(
            context.Options.ClientId,
            context.Options.Authority,
            $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}",
            clientCredential,
            userTokenCache.GetInstance(),
            null);

        try
        {
            var authenticationResult = await confidentialClientApplication.AcquireTokenByAuthorizationCodeAsync(
                context.ProtocolMessage.Code, _options.ApiScopes.Split(' '));
            context.HandleCodeRedemption(authenticationResult.AccessToken, authenticationResult.IdToken);
        }
        catch (Exception ex)
        {
            // TODO: Handle.
            throw;
        }
    }

    public Task OnRedirectToIdentityProvider(RedirectContext context)
    {
        context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.CodeIdToken;
        context.ProtocolMessage.Scope += $" offline_access {_options.ApiScopes}";
        return Task.FromResult(0);
    }
}

在向Azure AD B2C发送身份验证请求之前,此配置类将API作用域添加到身份验证请求的 scope 参数.

Before sending an authentication request to Azure AD B2C, this configuration class add the API scope/s to the scope parameter of the authentication request.

从Azure AD B2C收到授权代码后,配置类使用访问令牌交换此授权代码,并使用Microsoft身份验证库(MSAL)将访问令牌保存到令牌缓存中.

After receiving an authorization code from Azure AD B2C, the configuration class exchanges this authorization code with an access token and saves this access token to a token cache using the Microsoft Authentication Library (MSAL).

  1. Startup类中注册配置类的单个实例:
  1. Register a single instance of the configuration class in the Startup class:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // The following line configures the Azure AD B2C authentication with API options.
        services.Configure<AzureADB2CWithApiOptions>(options => Configuration.Bind("AzureADB2C", options));

        services.Configure<CookiePolicyOptions>(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
            .AddAzureADB2C(options => Configuration.Bind("AzureADB2C", options));

        // The following line registers the OpenID Connect authentication options for the Azure AD B2C authentication middleware.
        services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, AzureADB2COpenIdConnectOptionsConfigurator>();

        services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
}

对于使用Microsoft身份验证库(MSAL)从令牌缓存中加载访问令牌的控制器方法,您必须:

For a controller method to load the access token from the token cache using the Microsoft Authentication Library (MSAL), then you must:

public class HelloController : Controller
{
    private readonly AzureADB2CWithApiOptions _options;

    public HelloController(IOptions<AzureADB2CWithApiOptions> optionsAccessor)
    {
        _options = optionsAccessor.Value;
    }

    public async Task<IActionResult> Index()
    {
        var clientCredential = new ClientCredential(_options.ClientSecret);
        var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
        var userTokenCache = new SessionTokenCache(HttpContext, userId);

        var confidentialClientApplication = new ConfidentialClientApplication(
            _options.ClientId,
            _options.Authority,
            _options.RedirectUri,
            clientCredential,
            userTokenCache.GetInstance(),
            null);

        var authenticationresult = await confidentialClientApplication.AcquireTokenSilentAsync(
            _options.ApiScopes.Split(' '),
            confidentialClientApplication.Users.FirstOrDefault(),
            _options.Authority,
            false);

        // TODO: Invoke the API endpoint by setting the Authorization header to "Bearer" + authenticationResult.AccessToken.
    }
}

这篇关于无法从都由Azure AD B2C保护的MVC Web应用程序访问WebApi的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆