/
WebAppServiceCollectionExtensions.cs
250 lines (224 loc) · 13.2 KB
/
WebAppServiceCollectionExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Resource;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.Identity.Web
{
/// <summary>
/// Extensions for IServiceCollection for startup initialization.
/// </summary>
public static class WebAppServiceCollectionExtensions
{
/// <summary>
/// Add authentication with Microsoft identity platform.
/// This method expects the configuration file will have a section named "AzureAd" with the necessary settings to initialize authentication options.
/// </summary>
/// <param name="services">Service collection to which to add this authentication scheme</param>
/// <param name="configuration">The Configuration object</param>
/// <param name="subscribeToOpenIdConnectMiddlewareDiagnosticsEvents">
/// Set to true if you want to debug, or just understand the OpenIdConnect events.
/// </param>
/// <returns></returns>
public static IServiceCollection AddMicrosoftIdentityPlatformAuthentication(
this IServiceCollection services,
IConfiguration configuration,
string configSectionName = "AzureAd",
bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false)
{
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => configuration.Bind(configSectionName, options));
services.Configure<AzureADOptions>(options => configuration.Bind(configSectionName, options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
// Per the code below, this application signs in users in any Work and School
// accounts and any Microsoft Personal Accounts.
// If you want to direct Azure AD to restrict the users that can sign-in, change
// the tenant value of the appsettings.json file in the following way:
// - only Work and School accounts => 'organizations'
// - only Microsoft Personal accounts => 'consumers'
// - Work and School and Personal accounts => 'common'
// If you want to restrict the users that can sign-in to only one tenant
// set the tenant value in the appsettings.json file to the tenant ID
// or domain of this organization
options.Authority = options.Authority + "/v2.0/";
// If you want to restrict the users that can sign-in to several organizations
// Set the tenant value in the appsettings.json file to 'organizations', and add the
// issuers you want to accept to options.TokenValidationParameters.ValidIssuers collection
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetIssuerValidator(options.Authority).Validate;
// Set the nameClaimType to be preferred_username.
// This change is needed because certain token claims from Azure AD V1 endpoint
// (on which the original .NET core template is based) are different than Microsoft identity platform endpoint.
// For more details see [ID Tokens](https://docs.microsoft.com/azure/active-directory/develop/id-tokens)
// and [Access Tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens)
options.TokenValidationParameters.NameClaimType = "preferred_username";
// Avoids having users being presented the select account dialog when they are already signed-in
// for instance when going through incremental consent
options.Events.OnRedirectToIdentityProvider = context =>
{
var login = context.Properties.GetParameter<string>(OpenIdConnectParameterNames.LoginHint);
if (!string.IsNullOrWhiteSpace(login))
{
context.ProtocolMessage.LoginHint = login;
context.ProtocolMessage.DomainHint = context.Properties.GetParameter<string>(
OpenIdConnectParameterNames.DomainHint);
// delete the login_hint and domainHint from the Properties when we are done otherwise
// it will take up extra space in the cookie.
context.Properties.Parameters.Remove(OpenIdConnectParameterNames.LoginHint);
context.Properties.Parameters.Remove(OpenIdConnectParameterNames.DomainHint);
}
// Additional claims
if (context.Properties.Items.ContainsKey(OidcConstants.AdditionalClaims))
{
context.ProtocolMessage.SetParameter(
OidcConstants.AdditionalClaims,
context.Properties.Items[OidcConstants.AdditionalClaims]);
}
return Task.FromResult(0);
};
if (subscribeToOpenIdConnectMiddlewareDiagnosticsEvents)
{
OpenIdConnectMiddlewareDiagnostics.Subscribe(options.Events);
}
});
return services;
}
// TODO: pass an option with a section name to bind the options ? or a delegate?
/// <summary>
/// Add MSAL support to the Web App or Web API
/// </summary>
/// <param name="services">Service collection to which to add authentication</param>
/// <param name="initialScopes">Initial scopes to request at sign-in</param>
/// <returns></returns>
public static IServiceCollection AddMsal(this IServiceCollection services, IConfiguration configuration, IEnumerable<string> initialScopes, string configSectionName = "AzureAd")
{
// Ensure that configuration options for MSAL.NET, HttpContext accessor and the Token acquisition service
// (encapsulating MSAL.NET) are available through dependency injection
services.Configure<ConfidentialClientApplicationOptions>(options => configuration.Bind(configSectionName, options));
services.AddHttpContextAccessor();
services.AddTokenAcquisition();
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
// Response type
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
// This scope is needed to get a refresh token when users sign-in with their Microsoft personal accounts
// (it's required by MSAL.NET and automatically provided when users sign-in with work or school accounts)
options.Scope.Add(OidcConstants.ScopeOfflineAccess);
if (initialScopes != null)
{
foreach (string scope in initialScopes)
{
if (!options.Scope.Contains(scope))
{
options.Scope.Add(scope);
}
}
}
// Handling the auth redemption by MSAL.NET so that a token is available in the token cache
// where it will be usable from Controllers later (through the TokenAcquisition service)
var handler = options.Events.OnAuthorizationCodeReceived;
options.Events.OnAuthorizationCodeReceived = async context =>
{
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
await tokenAcquisition.AddAccountToCacheFromAuthorizationCodeAsync(context, options.Scope).ConfigureAwait(false);
await handler(context).ConfigureAwait(false);
};
// Handling the sign-out: removing the account from MSAL.NET cache
options.Events.OnRedirectToIdentityProviderForSignOut = async context =>
{
// Remove the account from MSAL.NET token cache
var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
await tokenAcquisition.RemoveAccountAsync(context).ConfigureAwait(false);
};
});
return services;
}
/// <summary>
/// Handles SameSite cookie issue according to the https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1.
/// The default list of user-agents that disallow SameSite None, was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
public static CookiePolicyOptions HandleSameSiteCookieCompatibility(this CookiePolicyOptions options)
{
return HandleSameSiteCookieCompatibility(options, DisallowsSameSiteNone);
}
/// <summary>
/// Handles SameSite cookie issue according to the docs: https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
/// The default list of user-agents that disallow SameSite None, was taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
/// </summary>
/// <param name="options"></param>
/// <param name="disallowsSameSiteNone">If you dont want to use the default user-agent list implementation, the method sent in this parameter will be run against the user-agent and if returned true, SameSite value will be set to Unspecified. The default user-agent list used can be found at: https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/</param>
/// <returns></returns>
public static CookiePolicyOptions HandleSameSiteCookieCompatibility(this CookiePolicyOptions options, Func<string, bool> disallowsSameSiteNone)
{
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
options.OnAppendCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions, disallowsSameSiteNone);
options.OnDeleteCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions, disallowsSameSiteNone);
return options;
}
private static void CheckSameSite(HttpContext httpContext, CookieOptions options, Func<string, bool> disallowsSameSiteNone)
{
if (options.SameSite == SameSiteMode.None)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
if (disallowsSameSiteNone(userAgent))
{
options.SameSite = SameSiteMode.Unspecified;
}
}
}
// Method taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
public static bool DisallowsSameSiteNone(string userAgent)
{
if (string.IsNullOrEmpty(userAgent))
{
return false;
}
// Cover all iOS based browsers here. This includes:
// - Safari on iOS 12 for iPhone, iPod Touch, iPad
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad
// All of which are broken by SameSite=None, because they use the iOS networking
// stack.
if (userAgent.Contains("CPU iPhone OS 12") ||
userAgent.Contains("iPad; CPU OS 12"))
{
return true;
}
// Cover Mac OS X based browsers that use the Mac OS networking stack.
// This includes:
// - Safari on Mac OS X.
// This does not include:
// - Chrome on Mac OS X
// Because they do not use the Mac OS networking stack.
if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
userAgent.Contains("Version/") && userAgent.Contains("Safari"))
{
return true;
}
// Cover Chrome 50-69, because some versions are broken by SameSite=None,
// and none in this range require it.
// Note: this covers some pre-Chromium Edge versions,
// but pre-Chromium Edge does not require SameSite=None.
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
{
return true;
}
return false;
}
}
}