Problem:
In my ASP.NET Core 3.1 MVC Web app I Use the services.AddSignIn(Configuration);
in my startup.cs (provided by the Microsoft.Identity.Web 0.1.5-preview) and I want to register a User upon logging in, the callback method ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync();
always returns null.
Background:
Details: My appsettings.json:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "something.onmicrosoft.com",
"TenantId": "GUID",
"ClientId": "GUID",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc",
"ClientSecret": "GUID"
}
Startup cs:
services.AddDbContext<UserDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"), u => u.MigrationsAssembly("DatabaseLayer.Core")));
services.AddIdentity<AppUser, IdentityRole>(options =>
{
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<UserDbContext>()
.AddDefaultTokenProviders();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
// Handling SameSite cookie according to https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
options.HandleSameSiteCookieCompatibility();
});
services.AddOptions();
services.AddSignIn(Configuration);
Question: What do I need to configure to get any externallogins returned in order to register the users?
It is possible by saving the uid
and utid
claims from the MSAL access token in the local user claims store. These claims are being looked up by the MSAL library when keying into their token store.
Code example in a Blazor 8 Web App template, in the file Components\Account\Pages\ExternalLogin.razor
in function OnValidSubmitAsync
:
private async Task OnValidSubmitAsync()
{
...
var claimsStore = GetClaimsStore();
string? OptClaim(List<string> claimTypes) =>
claimTypes.Select(s => externalLoginInfo.Principal.FindFirstValue(s))
.SingleOrDefault(s => s is not null);
const string tenantIdClaimType = "utid";
const string tenantIdClaimTypeAlt =
"http://schemas.microsoft.com/identity/claims/tenantid";
const string objectIdClaimType = "uid";
const string objectIdClaimTypeAlt =
"http://schemas.microsoft.com/identity/claims/objectidentifier";
var tenantId = OptClaim([tenantIdClaimType, tenantIdClaimTypeAlt]);
var objectId = OptClaim([objectIdClaimType, objectIdClaimTypeAlt]);
if (tenantId is not null) {
await claimsStore.AddClaimsAsync(
user,
[new(tenantIdClaimType, tenantId)],
default);
}
if (objectId is not null) {
await claimsStore.AddClaimsAsync(
user,
[new(objectIdClaimType, objectId)],
default);
}
EDIT: and here's the definition for GetClaimsStore
for completeness:
private IUserClaimStore<ApplicationUser> GetClaimsStore() {
if (!UserManager.SupportsUserClaim) {
throw new NotSupportedException(
"The default UI requires a user store with claims support.");
}
return (IUserClaimStore<ApplicationUser>)UserStore;
}
EDIT #2: It is also necessary to obtain the external login info from the MSAL-created Cookies
scheme, since it doesn't use Identity.External
, here is the modification to OnInitializeAsync
:
// This method is a slightly modified version of
// SignInManager.GetExternalLoginInfoAsync
private async Task<ExternalLoginInfo?> GetMsalInfo() {
var auth = await HttpContext.AuthenticateAsync("Cookies");
var items = auth?.Properties?.Items;
if (auth?.Principal == null || items == null
|| !items.TryGetValue("LoginProvider", out var provider))
{
return null;
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier)
?? auth.Principal.FindFirstValue("sub");
if (providerKey == null || provider == null)
{
return null;
}
var providerDisplayName =
(await SignInManager.GetExternalAuthenticationSchemesAsync())
.FirstOrDefault(p => p.Name == provider)
?.DisplayName ?? provider;
return new ExternalLoginInfo(
auth.Principal,
provider,
providerKey,
providerDisplayName)
{
AuthenticationTokens = auth.Properties?.GetTokens(),
AuthenticationProperties = auth.Properties,
};
}
protected override async Task OnInitializedAsync()
{
...
var info = await GetMsalInfo();
info ??= await SignInManager.GetExternalLoginInfoAsync();
...