web apps - mattchenderson/microsoft-identity-web GitHub Wiki
Currently, ASP.NET Core 3.1 web app templates (dot net new mvc -auth
) create web apps that sign in users 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 creates project templates using directly Microsoft.Identity.Web
Assuming you have a similar configuration in appsettings.json
to enable the web app:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "msidentitysamplestesting.onmicrosoft.com",
"TenantId": "7f58f645-c190-4ce5-9de4-e2b7acd2a6ab",
"ClientId": "86699d80-dd21-476a-bcd1-7c1a3d471f75",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc",
// Only if you want to call an API
"ClientSecret": "[Copy the client secret added to the app from the Azure portal]"
},
...
}
To enable users to sign in with the Microsoft identity platform:
-
Add the Microsoft.Identity.Web and Microsoft.Identity.Web.UI NuGet packages (currently in Preview)
-
Remove the AzureAD.UI and AzureADB2C.UI NuGet packages
-
Replace this code in your web application's Startup.cs file:
using Microsoft.Identity.Web; public class Startup { ... public void ConfigureServices(IServiceCollection services) { ... services.AddAuthentication(AzureADDefaults.AuthenticationScheme) .AddAzureAD(options => Configuration.Bind("AzureAd", options)); ... } ... }
... by the following code:
using Microsoft.Identity.Web; public class Startup { ... public void ConfigureServices(IServiceCollection services) { ... services.AddAuthentication(AzureADDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration); ... } ... }
This method adds authentication with the Microsoft identity platform. This includes validating the token in all scenarios (single- and multi-tenant applications) in the Azure public and national clouds.
You also need to call AddMicrosoftIdentityUI() if you want to benefit from the sign-in / sign-out.
For instance for Razor pages, you'd want something like the following in Startup.ConfigureServices(IServiceCollection services)
:
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
Finally, in Startup.Configure(IApplicationBuilder app, IWebHostEnvironment env)
you want to make sure that you map the controllers which are provided by Microsoft.Identity.Web.UI.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// More code
app.UseAuthentication();
app.UseAuthorization();
// More code
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages(); // If Razor pages
endpoints.MapControllers(); // Needs to be added
});
}
You can enable your web app to allow users to sign up and create a new guest account. First, set up your tenant and the app as described in Add a self-service sign-up user flow to an app. Next, set Prompt
property in OpenIdConnectOptions
(or in MicrosoftIdentityOptions
which inherits from it) to "create"
to trigger the sign-up experience. Your app can have a Sign up button linking to an Action which sets the Prompt
property, like in the example below. After the user goes through the sign-up process, they will be logged into the app.
[HttpGet("{scheme?}")]
public IActionResult SignUp([FromRoute] string scheme)
{
scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
var parameters = new Dictionary<string, object>
{
{ "prompt", "create" },
};
OAuthChallengeProperties oAuthChallengeProperties = new OAuthChallengeProperties(new Dictionary<string, string>(), parameters);
oAuthChallengeProperties.RedirectUri = Url.Content("~/");
return Challenge(
oAuthChallengeProperties,
scheme);
}
AddMicrosoftIdentityWebApp
(applied to authentication builders) has another override, which takes delegates instead of a configuration section. The override with
a configuration section actually calls the override with delegates. See the source code for AddMicrosoftIdentityWebApp with configuration section
In advanced scenarios you might want to add configuration by code, or if you want to subscribe to OpenIdConnect events. For instance if you want to provide a custom processing when the token is validated, you could use code like the following:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAD", options);
options.Events ??= new OpenIdConnectEvents();
options.Events.OnTokenValidated += OnTokenValidatedFunc;
});
with OnTokenValidatedFunc
like the following:
private async Task OnTokenValidatedFunc(TokenValidatedContext context)
{
// Custom code here
await Task.CompletedTask.ConfigureAwait(false);
}
In the above code, your handler will be executed after any existing handlers. In the below code, your code will be executed before any other existing handlers.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAD", options);
options.Events ??= new OpenIdConnectEvents();
var existingHandlers = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = OnTokenValidatedFunc;
options.Events.OnTokenValidated += existingHandlers;
});
This override also allows you to set an optional delegate to initialize the CookiesAuthenticationOptions
.
In more advanced scenarios, for instance if you use several IdPs, you might want to specify an authentication scheme (here using the default authentication scheme, which makes this code snippet equivalent to the previous one)
using Microsoft.Identity.Web;
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication("MyAuthenticationScheme")
.AddMicrosoftIdentityWebApp(Configuration,
openIdConnectAuthenticationScheme: "MyAuthenticationScheme");
...
}
...
}
See also:
- ASP.NET Core Web app incremental tutorial chapter 1.1, Sign in users in your organization
- Web App that signs-in users scenario overview in the Microsoft identity platform documentation, and related articles
The principle is the same, except that the appsettings.json has generally a section named "AzureAdB2C" (but you can choose the name you want, provided it's consistent with what you use in the AddMicrosoftIdentityWebApp
method, and you need to declare policies.
{
"AzureAdB2C": {
"Instance": "https://fabrikamb2c.b2clogin.com",
"ClientId": "fdb91ff5-5ce6-41f3-bdbd-8267c817015d",
"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
The startup.cs file is then:
using Microsoft.Identity.Web;
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration, "AzureAdB2C");
...
}
...
}
See also:
- ASP.NET Core Web app incremental tutorial chapter 1.5, Sign in users with Azure AD B2C
This example, demonstrates how to programmatically trigger a B2C user flow from a controller action or razor. In the AuthorizeForScopes
attribute, you will define the scopes and B2C user flow, and then include the same in GetAccessTokenForUserAsync
. Microsoft Identity Web will handle the challenge for you.
[AuthorizeForScopes(Scopes = new string[] { Scope }, UserFlow = EditProfile)] // Must be the same user flow as used in `GetAccessTokenForUserAsync()`
public async Task<ActionResult> ClaimsEditProfile()
{
// We get a token, but we don't use it. It's only to trigger the user flow
await _tokenAcquisition.GetAccessTokenForUserAsync(
new string[] { Scope },
userFlow: EditProfile);
return View(Claims, null);
}
If you want your web app to call web APIs, add the .EnableTokenAcquisitionToCallDownstreamApi()
line, and then choose a token cache implementation, for example .AddInMemoryTokenCaches()
:
using Microsoft.Identity.Web;
public class Startup
{
const string scopesToRequest = "user.read";
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { scopesToRequest })
.AddInMemoryTokenCaches();
...
}
...
}
By default, AddMicrosoftIdentityWebAppAuthentication
and the override of AddMicrosoftIdentityWebApp
taking a configuration object get the configuration from the "AzureAD" section of the configuration files. It has
several parameters you can change.
The proposed token cache serialization is in memory. You can also use the session cache, or various distributed caches.
Note that you don't need to pass-in the scopes to request when calling EnableTokenAcquisitionToCallDownstreamApi
. You can do that just in time in the controller (see Web app controller below)
using Microsoft.Identity.Web;
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
...
}
...
}
For your web app to call web APIs on behalf of the signed-in user, add a parameter of type ITokenAcquisition
to the constructor of your controller (the ITokenAcquisition
service will be injected by dependency injection by ASP.NET Core).
using Microsoft.Identity.Web;
[Authorize]
public class HomeController : Controller
{
readonly ITokenAcquisition tokenAcquisition;
public HomeController(ITokenAcquisition tokenAcquisition)
{
this.tokenAcquisition = tokenAcquisition;
}
...
Then, in your controller actions, call ITokenAcquisition.GetAccessTokenForUserAsync
, passing the scopes for which to request a token. The other methods of ITokenAcquisition are used from the EnableTokenAcquisitionToCallDownstreamApi()
method and similar methods for web APIs (see below).
[Authorize]
public class HomeController : Controller
{
readonly ITokenAcquisition tokenAcquisition;
...
[AuthorizeForScopes(Scopes = new[] { "user.read" })]
public async Task<IActionResult> Action()
{
string[] scopes = new []{"user.read"};
string token = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
...
// call the downstream API with the bearer token in the Authorize header
}
The controller action is decorated with the AuthorizeForScopesAttribute
which enables it to process the MsalUiRequiredException
that could be thrown by the service implementing ITokenAcquisition.GetAccessTokenForUserAsync
. The web app can then interact with the user and ask them to consent to the scopes, or re-sign in if needed.
If your application wants to call a 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"};
string token = await tokenAcquisition.GetAccessTokenForAppAsync(scopes);
...
// call the downstream API with the bearer token in the Authorize header
}
For more details on the end to end scenario, see:
In web apps, Microsoft.Identity.Web leverages the following OAuth 2.0 protocols:
- OAuth 2.0 authorization code flow and Token refresh for GetTokenForUser
- OAuth 2.0 client credentials flow for GetTokenForApp.
If you have an AAD B2C web app, which just signs in users (does not call a web API), and you include a valid client secret for the web app in the appsettings.json
file, you will get this error when switching between policies:
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler: Error: Message contains error: 'invalid_grant', error_description: 'AADB2C90088: The provided grant has not been issued for this endpoint. Actual Value : B2C_1_susi_v3 and Expected Value : B2C_1_edit_profile
Do one of the following:
-
Remove the client secret in
appsettings.json
. If the web app is only signing in users and NOT calling a downstream API, it does not need to have the client secret included in theappsettings.json
file. -
Enable MSAL .NET to correctly handle the auth code redemntion by including the following in
Startup.cs
:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration, "AzureAdB2C")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
This occurs because each policy, or user flow, is a separate authorization server in AAD B2C, meaning each user flow issues their own tokens.
When only signing-in users, your client app only needs the ID token, which contains information about the signed in user. However, if the client secret is included in the appsettings.json
, Microsoft Identity Web assumes the web app will eventually call a downstream API, so it requests a code and ID token. ASP .NET then receives the code, but cannot complete the authorization code flow, so MSAL .NET is invoked to redeem the code for tokens, but the ID token and the code were not provided by the same authority (for example, one for su_si policy and one for edit_profile policy), so an invalid_grant error is thrown.