web apis - mattchenderson/microsoft-identity-web GitHub Wiki
The Microsoft Identity Web library also enables web APIs to work with the Microsoft identity platform, enabling them to process access tokens for both work and school and Microsoft personal accounts, as well as Azure AD B2C.
Currently, ASP.NET Core 3.1 web app templates (dot net new webapi -auth
) create web APIs that are protected with the Azure AD v1.0 endpoint, allowing users to sign in with their organizational accounts (also called Work or school accounts).
This library adds ServiceCollection
and AuthenticationBuilder
extension methods for use in the ASP.NET Core web app Startup.cs file. These extension methods enable the web app to sign in users with the Microsoft identity platform and, optionally, enable the web app to call APIs on behalf of the signed-in user.
.NET Core 5.0 now has project templates using directly Microsoft.Identity.Web
Assuming you have a similar configuration in appsettings.json
:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "msidentitysamplestesting.onmicrosoft.com",
"TenantId": "7f58f645-c190-4ce5-9de4-e2b7acd2a6ab",
"ClientId": "a4c2469b-cf84-4145-8f5f-cb7bacf814bc"
},
...
}
To enable users to sign in with the Microsoft identity platform:
-
Add the Microsoft.Identity.Web and Microsoft.Identity.Web.UI NuGet packages
-
Remove the AzureAD.UI and AzureADB2C.UI NuGet packages
-
Replace this code in your web API's Startup.cs file:
using Microsoft.Identity.Web; public class Startup { ... public void ConfigureServices(IServiceCollection services) { ... services.AddAuthentication(AzureADDefaults.AuthenticationScheme) .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options)); ... } ... }
...with this code, using the
AuthenticationBuilder
:using Microsoft.Identity.Web; public class Startup { ... public void ConfigureServices(IServiceCollection services) { ... services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(Configuration); ... } ... }
or with this code, using the services directly:
using Microsoft.Identity.Web; public class Startup { ... public void ConfigureServices(IServiceCollection services) { ... services.AddMicrosoftIdentityWebApiAuthentication(Configuration); ... } ... }
This method enables your web API to be protected using the Microsoft identity platform. This includes validating the token in all scenarios (single- and multi-tenant applications) in the Azure public and national clouds.
See also:
- ASP.NET Core Web API incremental tutorial chapter 1.1, Protect the web API
- Protected web API scenario overview in the Microsoft identity platform documentation, and related articles
The configuration file above assumes that the App ID URI for your application (the base segment of scopes exposed by your Web API) is api://{ClientID}. This is the default when your register your application with the application registration portal. However, you can override it. In that case, you'll want to explicitly set the Audience
in your configuration to match the App ID URI for your Web API
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "msidentitysamplestesting.onmicrosoft.com",
"TenantId": "7f58f645-c190-4ce5-9de4-e2b7acd2a6ab",
"ClientId": "a4c2469b-cf84-4145-8f5f-cb7bacf814bc",
"Audience": "api://myappreg.azurewebsites.net"
},
...
}
Assuming you have a similar configuration in appsettings.json
:
{
"AzureAdB2C": {
"Instance": "https://fabrikamb2c.b2clogin.com",
"ClientId": "90c0fe63-bcf2-44d5-8fb7-b8bbc0b29dc6",
"Domain": "fabrikamb2c.onmicrosoft.com",
"SignedOutCallbackPath": "/signout/B2C_1_susi",
"SignUpSignInPolicyId": "b2c_1_susi",
"ResetPasswordPolicyId": "b2c_1_reset",
"EditProfilePolicyId": "b2c_1_edit_profile" // Optional profile editing policy
},
// more here
}
To enable the web API to accept tokens emitted by Azure AD B2C, have the following code in your Web API:
using Microsoft.Identity.Web;
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAdB2C");
...
}
...
}
In the controller methods, protected web APIs needs to verify that the the token used to called them has the right:
- scopes, in the case of APIs called on behalf of a user
- app roles, in the case of APIs called by daemon applications
A web API that is called on behalf of users needs to verify the scopes in the controller actions. This can be done:
- from Microsoft.Identity.Web 1.6, using the
[RequiredScopes]
attribute - before Microsoft.Identity.Web 1.6, using the
VerifyUserHasAnyAcceptedScope
extension method on the HttpContext.
The RequiredScopes
attribute can be set on a controller, a controller action, a razor page to declare the scopes required by a web API
and validate that at least one of these scopes is available in the token
These required scopes can be declared in two ways:
- hardcoding them,
- or declaring them in the configuration.
Depending on the way you chose, use either one or the other of the constructors.
If the token obtained for this API on behalf of the authenticated user does not have any of the scopes, in its scope claim, the attribute ensures that the HTTP response is updated providing a status code 403 (Forbidden) and writes to the response body a message telling which scopes are expected in the token.
[Authorize]
[RequiredScope(HomeController.scopeRequiredByAPI)
public class HomeController : Controller
{
public const string[] scopeRequiredByAPI = new string[] { "access_as_user" };
/// ...
public async Task<IActionResult> Action()
{
}
}
You can also declare these required scopes in the configuration, and reference the configuration key:
For instance if, in the appsettings.json you have the following configuration:
{
"AzureAd" {
// more settings
"Scopes" : "access_as_user access_as_admin"
}
}
then, you can reference it in the attribute:
[Authorize]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")
public class HomeController : Controller
{
/// ...
public async Task<IActionResult> Action()
{
}
}
In versions of Microsoft.Identity.Web prior to 1.6.0, you had to use the VerifyUserHasAnyAcceptedScope
extension method on the HttpContext:
[Authorize]
public class HomeController : Controller
{
static string[] scopeRequiredByAPI = new string[] { "access_as_user" };
...
public async Task<IActionResult> Action()
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByAPI);
{
}
A web API that accepts daemon applications needs to:
- either verify the application roles in the controller actions (See application roles
- or be protected by an ACL-based authorization pattern to control tokens without a roles claim
[Authorize]
public class HomeController : Controller
{
public async Task<IActionResult> Action()
{
HttpContext.ValidateAppRole("acceptedRole1")
// Do the work
}
Alternatively, it can use the [Authorize("role")]
attributes on the controller or an action (or a razor page).
[Authorize("role")]
MyController
But for this, you'll need to map the Role claim to "roles" in the startup.cs
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// The claim in the Jwt token where App roles are available.
options.TokenValidationParameters.RoleClaimType = "roles";
});
This is not the best solution if you also need to do authorization based on groups.
For details see the web app incremental tutorial on authorization by roles and groups.
If you want to enable the ACL-based authorization, you'll need to set the AllowWebApiToBeAuthorizedByACL
to true in the configuration. otherwise, Microsoft Identity Web will no longer throw an exception when neither roles or scopes are not in the Claims provided If you set this property to true in the appsettings.json or programmatically, this is your responsibility to ensure the ACL mechanism.
{
"AzureAD"
{
// other properties
"AllowWebApiToBeAuthorizedByACL" : true,
// other properties
}
}
Web APIs can demand that they receive encrypted tokens to avoid that their client apps be tempted to crack-open the token (even if we discourage it), and therefore get access to claims about the user. For details on how to setup your web API for encyrpted tokens, see Token encryption
If you want your web API to, moreover, call downstream web APIs, add the .EnableTokenAcquisitionToCallDownstreamApi()
line, and then choose a token cache implementation, for example .AddInMemoryTokenCaches()
:
using Microsoft.Identity.Web;
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
...
}
...
}
You can also benefit from a higher level API to call the protected downstream API, serialiazing and deserializing parameters if needed, and handling some of the HTTP errors: the IDownstreamWebApi
. You can register as many of these as downstream web APIs by using AddDownstreamWebApi
in the Startup.cs file:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamWebApi("MyApi", Configuration.GetSection("GraphBeta"))
.AddInMemoryTokenCaches();
The service can be initialized by a section in the appsettings.json like the following:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "[Client_id-of-web-api-eg-2ec40e65-ba09-4853-bcde-bcb60029e596]",
"TenantId": "common",
// To call an API
"ClientSecret": "[Copy the client secret added to the app from the Azure portal]",
"ClientCertificates": [
]
},
"GraphBeta": {
"BaseUrl": "https://graph.microsoft.com/beta",
"Scopes": "user.read"
}
}
As with web apps, you can choose various token cache implementations. For details see Token cache serialization.
Also you can use certificate instead of client secrets. For details see using certificates.
If you're certain that your web API will need some specific scopes, you can optionally pass them as arguments to EnableTokenAcquisitionToCallDownstreamApi
.
To enable your web API to call downstream APIs:
-
Add (as in web apps) a parameter of type
ITokenAcquisition
to the constructor of your controller. TheITokenAcquisition
service will be injected by dependency injection by ASP.NET Core. -
In your controller actions, verify that the token contains the scopes expected by the action. To do so, call the
VerifyUserHasAnyAcceptedScope
extension method on theHttpContext
. -
Alternatively if your web API is called by a daemon app, use
ValidateAppRoles()
-
In your controller actions, call
ITokenAcquisition.GetAccessTokenForUserAsync
, passing the scopes for which to request a token. -
Alternatively if you controller calls a downstream API on behalf of itself (instead of on behalf of the user), call
ITokenAcquisition.GetAccessTokenForApplicationAsync
, passing the scopes for which to request a token.
The following code snippet shows how to combine these steps:
[Authorize]
public class HomeController : Controller
{
readonly ITokenAcquisition tokenAcquisition;
static string[] scopeRequiredByAPI = new string[] { "access_as_user" };
...
public async Task<IActionResult> Action()
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByAPI);
string[] scopes = new []{"user.read"};
try
{
string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
// call the downstream API with the bearer token in the Authorize header
}
catch (MsalUiRequiredException ex)
{
_tokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeader(HttpContext, scopes, ex);
}
...
}
Alternatively you can also inject an instance of IDownstreamWebApi
.
[Authorize]
[AuthorizeForScopes(ScopeKeySection = "TodoList:Scopes")]
public class TodoListController : Controller
{
private IDownstreamWebApi _downstreamWebApi;
public TodoListController(IDownstreamWebApi downstreamWebApi)
{
_downstreamWebApi = downstreamWebApi;
}
public async Task<ActionResult> Details(int id)
{
var value = await _downstreamWebApi.CallWebApiForUserAsync(
"MyApi",
options =>
{
options.RelativePath = $"me";
});
return View(value);
}
Note that, you'll need to:
- create a static page in your Web API to inform the users that they have successfully signed-up to your web API (like wwwroot/index.html)
- you need to add, in the appsettings.json, the following line:
"CallbackPath": "",
For details see the following sample: Sign a user into a Desktop application using Microsoft Identity Platform and call a protected ASP.NET Core Web API, which calls Microsoft Graph on-behalf of the user
When your web API tries to get a token for the downstream API, the token acquisition service may throw a MsalUiRequiredException
. The MsalUiRequiredException
indicates that the user on the client calling the web API needs to perform additional actions, for example, multi-factor authentication.
Given that the web API isn't capable of performing such interaction itself, the exception needs to be passed to the client. To propagate the exception back to the client, catch the exception and call the ITokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeader
method.
A web API that accepts daemon applications need to verify the application roles in the controller actions
HttpContext.ValidateAppRole("acceptedRole1")
If your web API wants to call a downstream web API on behalf of itself (not of behalf of a user), you can use ITokenAcquisition.GetAccessTokenForAppAsync
in the controller. The code in the startup.cs file is the same as when you call an API on behalf of a user, and the constructor of your controller or Razor page injects an ITokenAcquisition
service.
[Authorize]
public class HomeController : Controller
{
readonly ITokenAcquisition tokenAcquisition;
...
public async Task<IActionResult> Action()
{
string[] scopes = new []{"users.read.all"};
HttpContext.ValidateAppRole("acceptedRole1")
string token = await tokenAcquisition.GetAccessTokenForAppAsync(scopes);
...
// call the downstream API with the bearer token in the Authorize header
}
The Azure AD B2C service does not support web APIs calling downstream APIs. See Azure AD B2C limitations for details.
In some more advanced scenarios, apps might need to support multiple authentication schemes. For example, users can be authenticated from Azure AD and Azure AD B2C. In Microsoft Identity Web, this can be setup like so:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
services.AddAuthentication()
.AddMicrosoftIdentityWebApi(Configuration, "AzureAdB2C", "B2CScheme")
.EnableTokenAcquisitionToCallDownstreamApi();
The schemes specified above can also be added to the default authorization policy. This means that you can decorate the controllers with the [Authorize]
attribute and it will accept requests from all default schemes.
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
"B2CScheme")
.RequireAuthenticatedUser()
.Build();
});
More details can be found in ASP.NET documentation:
- Authorize with a specific scheme in ASP.NET Core
- Create an ASP.NET Core web app with user data protected by authorization
gRPC services and Azure functions can also be considered as protected web APIs (as they can be called by client applications). This means that a lot of what is described above also applies to them. For details on how Microsoft identity web helps building protected gRPC and Azure functions see:
- gRPC services
- Use Microsoft identity web with Azure functions as well as the maliksahil/ms-identity-azurefunctions-microsoft-identity-web code sample
A project template exists for gRPC services (worker2), and we are working on a project template for Azure functions.
For more details on the end to end scenario, see:
- Scenario: Protected web API
- Scenario: A web API that calls web APIs
- Scenario: Daemon application that calls web APIs
In web APIs, Microsoft.Identity.Web leverages the following OAuth 2.0 protocols:
- OAuth 2.0 On-Behalf-Of flow and Token refresh for GetTokenForUser.
- OAuth 2.0 client credentials flow for GetTokenForApp.