无法从MVC Web应用程序访问WebApi,其中两者都由Azure AD B2C保护

时间:2018-06-18 20:31:41

标签: c# asp.net-mvc azure-ad-b2c asp.net-core-2.1

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

可以提出以下几点:

  1. 我可以使用B2C成功登录并注册网络应用。我正在使用新的.NET Core 2.1模板,其中B2C以最少的代码加入到项目中。

  2. 我也使用向导创建WebApi项目(这是WebAPI的结果)。然后,我可以成功使用Postman来测试在WebApi中签名,其中Postman在租户中注册为自己的网络应用程序。这被描述为here

  3. 我正在我的本地机器上测试所有这些,我还没有部署到azurewebsites.net

  4. 我可以说网络应用正在使用

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

    WebApi startup.cs正在使用持票令牌:

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

    所以现在我在我的网络应用程序中有一个控制器动作,想要调用WebApi项目中的ValuesController来获取虚拟值。

    这是我不明白的,因为使用以下代码,我能够获得令牌:

    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解密令牌,但令牌不包含声明。不明白为什么会这样。

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

    //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应用程序的权限。

    我错过了什么?我不知道Postman的工作原理(在ADB2C中注册为网络应用),这不是

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

2 个答案:

答案 0 :(得分:1)

于2018年6月27日开始编辑

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

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

于2018年6月27日结束编辑

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

  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 文件中:
{
  "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身份验证选项:
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 参数。

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

  1. Startup类中注册配置类的单个实例:
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)从令牌缓存中加载访问令牌的控制器方法,您必须:

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.
    }
}

答案 1 :(得分:0)

您不了解OAuth中的不同流程。在您的MVC前端中,您使用Authorization Code Grant flow来授权用户。然后在后端,您丢弃该用户并使用Client Credentials flow获取您要呼叫的服务的令牌。

在某种程度上,这很好。只要您在上次服务到服务呼叫中不需要最终用户上下文。但是,client credentials flow is currently not supported in Azure AD B2C

  

守护程序/服务器端应用

     

包含长时间运行流程的应用或   在没有用户存在的情况下运行也需要一种访问方式   安全的资源,如Web API。这些应用可以进行身份​​验证和   通过使用应用程序的身份(而不是用户的委托)获取令牌   身份)并使用OAuth 2.0客户端凭据流。

     

Azure AD B2C目前不支持此流程。这些应用可以   只有在交互式用户流发生后才能获取令牌。

您必须使用授权代码授予流程获取所有access_tokens。 您可以查看如何实现此目的at this sample