问题
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.
The following points can be made:
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.
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
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 );
} );
The WebApi startup.cs is using the bearer token:
services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
.AddAzureADB2CBearer(options =>
{
Configuration.Bind( "AzureAdB2C", options );
} );
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 );
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.
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).
For the "id" in teh code above I am using the complete WebApi URL as seen in the WebApi's properties in the Azure portal.
Under "API access" I have granted the web application access to the web api application in the portal.
What am I missing? I don't get why Postman works (which is registered in ADB2C as a web app), and this doesn't
These are the scopes desfined in API Access:
回答1:
Begin edit on 27 June 2018
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.
End edit on 27 June 2018
For a ASP.NET Core 2.1 web application to acquire an access token for use with an API application, then you must:
- Create an options class that extends the Azure AD B2C authentication options with the API options:
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; }
}
- 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"
}
}
}
- 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);
}
}
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.
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).
- 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);
}
}
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.
}
}
回答2:
You do not understand different flows in OAuth. In your MVC front end you use Authorization Code Grant flow to authorize the user. Then in the backend, you discard the user and use the Client Credentials flow to get a token for the service you want to call.
To some extend this is fine. As long as you do not need end user context in your last service-to-service call. However, the client credentials flow is currently not supported in Azure AD B2C:
Daemons/server-side apps
Apps that contain long-running processes or that operate without the presence of a user also need a way to access secured resources such as web APIs. These apps can authenticate and get tokens by using the app's identity (rather than a user's delegated identity) and by using the OAuth 2.0 client credentials flow.
This flow is not currently supported by Azure AD B2C. These apps can get tokens only after an interactive user flow has occurred.
You have to obtain all access_tokens using the Authorization Code Grant flow. You may take a look at this sample on how to achieve this.
来源:https://stackoverflow.com/questions/50917054/unable-to-access-a-webapi-from-a-mvc-web-app-where-both-are-secured-by-azure-ad