Skip to content

Commit 14dd663

Browse files
author
Tiago Brenck
committed
Addressing PR comments about overwriting custom OIDC and JWT events from the developer
1 parent f44e861 commit 14dd663

File tree

2 files changed

+142
-39
lines changed

2 files changed

+142
-39
lines changed

Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs

Lines changed: 123 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using Microsoft.AspNetCore.Authentication;
45
using Microsoft.AspNetCore.Authentication.JwtBearer;
56
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
67
using Microsoft.Extensions.Configuration;
@@ -59,20 +60,109 @@ public static IServiceCollection AddProtectedWebApi(
5960
bool subscribeToJwtBearerMiddlewareDiagnosticsEvents = false)
6061
{
6162
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
62-
.AddJwtBearer((options => configuration.Bind(configSectionName, options)));
63-
64-
services.Configure<MicrosoftIdentityOptions>(options => configuration.Bind(configSectionName, options));
63+
.AddProtectedWebApi(
64+
configSectionName,
65+
configuration,
66+
options => configuration.Bind(configSectionName, options),
67+
tokenDecryptionCertificate,
68+
subscribeToJwtBearerMiddlewareDiagnosticsEvents);
6569

66-
services.AddHttpContextAccessor();
70+
return services;
71+
}
72+
73+
/// <summary>
74+
/// Protects the Web API with Microsoft identity platform (formerly Azure AD v2.0)
75+
/// This method expects the configuration file will have a section, named "AzureAd" as default, with the necessary settings to initialize authentication options.
76+
/// </summary>
77+
/// <param name="builder">AuthenticationBuilder to which to add this configuration</param>
78+
/// <param name="configuration">The Configuration object</param>
79+
/// <param name="configureOptions">An action to configure JwtBearerOptions</param>
80+
/// <param name="tokenDecryptionCertificate">Token decryption certificate</param>
81+
/// <param name="subscribeToJwtBearerMiddlewareDiagnosticsEvents">
82+
/// Set to true if you want to debug, or just understand the JwtBearer events.
83+
/// </param>
84+
/// <returns></returns>
85+
public static AuthenticationBuilder AddProtectedWebApi(
86+
this AuthenticationBuilder builder,
87+
IConfiguration configuration,
88+
Action<JwtBearerOptions> configureOptions,
89+
X509Certificate2 tokenDecryptionCertificate = null,
90+
bool subscribeToJwtBearerMiddlewareDiagnosticsEvents = false)
91+
{
92+
return AddProtectedWebApi(
93+
builder,
94+
"AzureAd",
95+
configuration,
96+
JwtBearerDefaults.AuthenticationScheme,
97+
configureOptions,
98+
tokenDecryptionCertificate,
99+
subscribeToJwtBearerMiddlewareDiagnosticsEvents);
100+
}
101+
102+
/// <summary>
103+
/// Protects the Web API with Microsoft identity platform (formerly Azure AD v2.0)
104+
/// This method expects the configuration file will have a section, named "AzureAd" as default, with the necessary settings to initialize authentication options.
105+
/// </summary>
106+
/// <param name="builder">AuthenticationBuilder to which to add this configuration</param>
107+
/// <param name="configSectionName">The configuration section with the necessary settings to initialize authentication options</param>
108+
/// <param name="configuration">The Configuration object</param>
109+
/// <param name="configureOptions">An action to configure JwtBearerOptions</param>
110+
/// <param name="tokenDecryptionCertificate">Token decryption certificate</param>
111+
/// <param name="subscribeToJwtBearerMiddlewareDiagnosticsEvents">
112+
/// Set to true if you want to debug, or just understand the JwtBearer events.
113+
/// </param>
114+
/// <returns></returns>
115+
public static AuthenticationBuilder AddProtectedWebApi(
116+
this AuthenticationBuilder builder,
117+
string configSectionName,
118+
IConfiguration configuration,
119+
Action<JwtBearerOptions> configureOptions,
120+
X509Certificate2 tokenDecryptionCertificate = null,
121+
bool subscribeToJwtBearerMiddlewareDiagnosticsEvents = false)
122+
{
123+
return AddProtectedWebApi(
124+
builder,
125+
configSectionName,
126+
configuration,
127+
JwtBearerDefaults.AuthenticationScheme,
128+
configureOptions,
129+
tokenDecryptionCertificate,
130+
subscribeToJwtBearerMiddlewareDiagnosticsEvents);
131+
}
132+
133+
/// <summary>
134+
/// Protects the Web API with Microsoft identity platform (formerly Azure AD v2.0)
135+
/// This method expects the configuration file will have a section, named "AzureAd" as default, with the necessary settings to initialize authentication options.
136+
/// </summary>
137+
/// <param name="builder">AuthenticationBuilder to which to add this configuration</param>
138+
/// <param name="configSectionName">The configuration section with the necessary settings to initialize authentication options</param>
139+
/// <param name="configuration">The Configuration object</param>
140+
/// <param name="jwtBearerScheme">The JwtBearer scheme name to be used. By default it uses "Bearer"</param>
141+
/// <param name="configureOptions">An action to configure JwtBearerOptions</param>
142+
/// <param name="tokenDecryptionCertificate">Token decryption certificate</param>
143+
/// <param name="subscribeToJwtBearerMiddlewareDiagnosticsEvents">
144+
/// Set to true if you want to debug, or just understand the JwtBearer events.
145+
/// </param>
146+
/// <returns></returns>
147+
public static AuthenticationBuilder AddProtectedWebApi(
148+
this AuthenticationBuilder builder,
149+
string configSectionName,
150+
IConfiguration configuration,
151+
string jwtBearerScheme,
152+
Action<JwtBearerOptions> configureOptions,
153+
X509Certificate2 tokenDecryptionCertificate = null,
154+
bool subscribeToJwtBearerMiddlewareDiagnosticsEvents = false)
155+
{
156+
builder.Services.Configure(jwtBearerScheme, configureOptions);
157+
builder.Services.Configure<MicrosoftIdentityOptions>(options => configuration.Bind(configSectionName, options));
158+
159+
builder.Services.AddHttpContextAccessor();
67160

68161
// Change the authentication configuration to accommodate the Microsoft identity platform endpoint (v2.0).
69-
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
162+
builder.AddJwtBearer(jwtBearerScheme, options =>
70163
{
71164
var microsoftIdentityOptions = configuration.GetSection(configSectionName).Get<MicrosoftIdentityOptions>();
72165

73-
// Reinitialize the options as this has changed to JwtBearerOptions to pick configuration values for attributes unique to JwtBearerOptions
74-
configuration.Bind(configSectionName, options);
75-
76166
if (string.IsNullOrWhiteSpace(options.Authority))
77167
options.Authority = AuthorityHelpers.BuildAuthority(microsoftIdentityOptions);
78168

@@ -82,48 +172,52 @@ public static IServiceCollection AddProtectedWebApi(
82172
// The valid audiences are both the Client ID (options.Audience) and api://{ClientID}
83173
options.TokenValidationParameters.ValidAudiences = new string[]
84174
{
85-
// If the developer doesnt set the Audience on JwtBearerOptions, use ClientId from MicrosoftIdentityOptions
175+
// If the developer doesn't set the Audience on JwtBearerOptions, use ClientId from MicrosoftIdentityOptions
86176
options.Audience, $"api://{options.Audience ?? microsoftIdentityOptions.ClientId}"
87177
};
88178

89-
// Instead of using the default validation (validating against a single tenant, as we do in line of business apps),
90-
// we inject our own multi-tenant validation logic (which even accepts both v1.0 and v2.0 tokens)
91-
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetIssuerValidator(options.Authority).Validate;
179+
// If the developer registered an IssuerValidator, do not overwrite it
180+
if (options.TokenValidationParameters.IssuerValidator == null)
181+
{
182+
// Instead of using the default validation (validating against a single tenant, as we do in line of business apps),
183+
// we inject our own multi-tenant validation logic (which even accepts both v1.0 and v2.0 tokens)
184+
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetIssuerValidator(options.Authority).Validate;
185+
}
92186

93187
// If you provide a token decryption certificate, it will be used to decrypt the token
94188
if (tokenDecryptionCertificate != null)
95189
{
96190
options.TokenValidationParameters.TokenDecryptionKey = new X509SecurityKey(tokenDecryptionCertificate);
97191
}
98192

193+
if (options.Events == null)
194+
options.Events = new JwtBearerEvents();
195+
99196
// When an access token for our own Web API is validated, we add it to MSAL.NET's cache so that it can
100197
// be used from the controllers.
101-
options.Events = new JwtBearerEvents();
102-
198+
var tokenValidatedHandler = options.Events.OnTokenValidated;
103199
options.Events.OnTokenValidated = async context =>
104-
{
105-
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented and provisioned.
106-
if (!context.Principal.Claims.Any(x => x.Type == ClaimConstants.Scope)
107-
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Scp)
108-
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Roles))
109-
{
110-
throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
111-
}
112-
113-
await Task.FromResult(0);
114-
};
200+
{
201+
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented and provisioned.
202+
if (!context.Principal.Claims.Any(x => x.Type == ClaimConstants.Scope)
203+
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Scp)
204+
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Roles))
205+
{
206+
throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
207+
}
208+
209+
await tokenValidatedHandler(context).ConfigureAwait(false);
210+
};
115211

116212
if (subscribeToJwtBearerMiddlewareDiagnosticsEvents)
117213
{
118214
options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
119215
}
120216
});
121217

122-
return services;
218+
return builder;
123219
}
124220

125-
// TODO: pass an option with a section name to bind the options ? or a delegate?
126-
127221
/// <summary>
128222
/// Protects the Web API with Microsoft identity platform (formerly Azure AD v2.0)
129223
/// This supposes that the configuration files have a section named configSectionName (typically "AzureAD")
@@ -141,8 +235,7 @@ public static IServiceCollection AddProtectedWebApiCallsProtectedWebApi(
141235
services.AddHttpContextAccessor();
142236
services.Configure<ConfidentialClientApplicationOptions>(options => configuration.Bind(configSectionName, options));
143237
services.Configure<MicrosoftIdentityOptions>(options => configuration.Bind(configSectionName, options));
144-
145-
// TODO: Pass scheme as parameter?
238+
146239
services.Configure<JwtBearerOptions>(jwtBearerScheme, options =>
147240
{
148241
options.Events.OnTokenValidated = async context =>

Microsoft.Identity.Web/WebAppServiceCollectionExtensions.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,20 +94,22 @@ public static IServiceCollection AddWebAppCallsProtectedWebApi(
9494

9595
// Handling the auth redemption by MSAL.NET so that a token is available in the token cache
9696
// where it will be usable from Controllers later (through the TokenAcquisition service)
97-
var handler = options.Events.OnAuthorizationCodeReceived;
97+
var codeReceivedHandler = options.Events.OnAuthorizationCodeReceived;
9898
options.Events.OnAuthorizationCodeReceived = async context =>
9999
{
100100
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
101101
await tokenAcquisition.AddAccountToCacheFromAuthorizationCodeAsync(context, options.Scope).ConfigureAwait(false);
102-
await handler(context).ConfigureAwait(false);
102+
await codeReceivedHandler(context).ConfigureAwait(false);
103103
};
104104

105105
// Handling the sign-out: removing the account from MSAL.NET cache
106+
var signOutHandler = options.Events.OnRedirectToIdentityProviderForSignOut;
106107
options.Events.OnRedirectToIdentityProviderForSignOut = async context =>
107108
{
108109
// Remove the account from MSAL.NET token cache
109110
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
110111
await tokenAcquisition.RemoveAccountAsync(context).ConfigureAwait(false);
112+
await signOutHandler(context).ConfigureAwait(false);
111113
};
112114
});
113115
return services;
@@ -205,6 +207,7 @@ public static AuthenticationBuilder AddSignIn(
205207

206208
options.TokenValidationParameters.NameClaimType = "preferred_username";
207209

210+
// If the developer registered an IssuerValidator, do not overwrite it
208211
if (options.TokenValidationParameters.IssuerValidator == null)
209212
{
210213
// If you want to restrict the users that can sign-in to several organizations
@@ -215,7 +218,8 @@ public static AuthenticationBuilder AddSignIn(
215218

216219
// Avoids having users being presented the select account dialog when they are already signed-in
217220
// for instance when going through incremental consent
218-
options.Events.OnRedirectToIdentityProvider = context =>
221+
var redirectToIdpHandler = options.Events.OnRedirectToIdentityProvider;
222+
options.Events.OnRedirectToIdentityProvider = async context =>
219223
{
220224
var login = context.Properties.GetParameter<string>(OpenIdConnectParameterNames.LoginHint);
221225
if (!string.IsNullOrWhiteSpace(login))
@@ -242,18 +246,24 @@ public static AuthenticationBuilder AddSignIn(
242246
{
243247
// When a new Challenge is returned using any B2C policy different than sisu, we must change
244248
// the ProtocolMessage.IssuerAddress to the desired policy otherwise the redirect would use the sisu policy
245-
b2COidcHandlers.OnRedirectToIdentityProvider(context);
249+
await b2COidcHandlers.OnRedirectToIdentityProvider(context);
246250
}
247251

248-
return Task.FromResult(0);
252+
await redirectToIdpHandler(context).ConfigureAwait(false);
249253
};
250254

251255
if (microsoftIdentityOptions.IsB2C)
252256
{
253-
// Handles the error when a user cancels an action on the Azure Active Directory B2C UI.
254-
// Handle the error code that Azure Active Directory B2C throws when trying to reset a password from the login page
255-
// because password reset is not supported by a "sign-up or sign-in policy".
256-
options.Events.OnRemoteFailure = b2COidcHandlers.OnRemoteFailure;
257+
var remoteFailureHandler = options.Events.OnRemoteFailure;
258+
options.Events.OnRemoteFailure = async context =>
259+
{
260+
// Handles the error when a user cancels an action on the Azure Active Directory B2C UI.
261+
// Handle the error code that Azure Active Directory B2C throws when trying to reset a password from the login page
262+
// because password reset is not supported by a "sign-up or sign-in policy".
263+
await b2COidcHandlers.OnRemoteFailure(context);
264+
265+
await remoteFailureHandler(context).ConfigureAwait(false);
266+
};
257267
}
258268

259269
if (subscribeToOpenIdConnectMiddlewareDiagnosticsEvents)

0 commit comments

Comments
 (0)