Asp Net Core Identity UserManager change context - asp.net-core

I have implemented the microsoft identity framework, I'm using the UserManager via dependecy injection, since it is a multi-tenancy project I would need to extend the UserManager class to pass the desired context, I searched on google but I could not find or better adapt anything For my case.
_userManagerRepository = new UserManagerRepository(new PortaleContext(tenantContext.Tenant))
_userMgr = new UserManager<ApplicationUser>(userStore,null,null,null,null,null,null,null,null);
But when I run this method:
var passwordResetToken = await _userMgr.GeneratePasswordResetTokenAsync(user)
I get the following error:
<div class="titleerror">NotSupportedException: No IUserTokenProvider named 'Default' is registered.</div>
<p class="location">Microsoft.AspNetCore.Identity.UserManager+<VerifyUserTokenAsync>d__122.MoveNext()</p>
And this is a part of my startup class:
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.User.RequireUniqueEmail = true;
config.Password.RequiredLength = 8;
// config.Cookies.ApplicationCookie.LoginPath = "/App/Login";
config.SignIn.RequireConfirmedEmail = false;
config.SignIn.RequireConfirmedPhoneNumber = false;
})
.AddEntityFrameworkStores<AdminContext>()
.AddDefaultTokenProviders();
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.User.RequireUniqueEmail = true;
config.Password.RequiredLength = 8;
config.Password.RequireNonAlphanumeric = false;
config.Password.RequireUppercase = false;
// config.Cookies.ApplicationCookie.LoginPath = "/App/Login";
config.SignIn.RequireConfirmedEmail = false;
config.SignIn.RequireConfirmedPhoneNumber = false;
})
.AddEntityFrameworkStores<PortaleContext>()
.AddDefaultTokenProviders();
In a nutshell, depending on which address the request arrives, the usermanager must update to its context

This article references an error when calling AddDefaultTokenProviders() Twice. This may be your issue, I am looking for a similar solution but this one didn't work for me, but maybe you haven't seen it yet and it works for you.
https://github.com/aspnet/Identity/issues/972

Related

Custom parameter with Microsoft Identity Platform and Azure AD B2C - how to add information using the 'State' paramater?

I'm following this tutorial: https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=aspnetcore
According to other docs, I can use the 'state' parameter to pass in custom data and this will be returned back to the app once the user is logged in
However, OIDC also uses this state param to add its own encoded data to prevent xsite hacking - I cant seem to find the correct place in the middleware to hook into this and add my custom data
There's a similar discussion on this thread: Custom parameter with Microsoft.Owin.Security.OpenIdConnect and AzureAD v 2.0 endpoint but I'm using AddMicrosoftIdentityWebApp whereas they're using UseOpenIdConnectAuthentication and I don't know how to hook into the right place in the middleware to add my custom data then retrieve it when on the return.
I'd like to be able to do something like in the code below - when I set break points state is null outgoing and incoming, however the querystring that redirects the user to Azure has a state param that is filled in by the middleware, but if i do it like this, then I get an infinite redirect loop
public static class ServicesExtensions
{
public static void AddMicrosoftIdentityPlatformAuthentication(this IServiceCollection services, IConfigurationSection azureAdConfig)
{
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
options.ClientId = azureAdConfig["ClientId"];
options.Domain = azureAdConfig["Domain"];
options.Instance = azureAdConfig["Instance"];
options.CallbackPath = azureAdConfig["CallbackPath"];
options.SignUpSignInPolicyId = azureAdConfig["SignUpSignInPolicyId"];
options.Events.OnRedirectToIdentityProvider = context =>
{
//todo- ideally we want to be able to add a returnurl to the state parameter and read it back
//however the state param is maintained auto and used to prevent xsite attacks so we can just add our info
//as we get an infinite loop back to az b2 - see https://blogs.aaddevsup.xyz/2019/11/state-parameter-in-mvc-application/
//save the url of the page that prompted the login request
//var queryString = context.HttpContext.Request.QueryString.HasValue
// ? context.HttpContext.Request.QueryString.Value
// : string.Empty;
//if (queryString == null) return Task.CompletedTask;
//var queryStringParameters = HttpUtility.ParseQueryString(queryString);
//context.ProtocolMessage.State = queryStringParameters["returnUrl"]?.Replace("~", "");
return Task.CompletedTask;
};
options.Events.OnMessageReceived = context =>
{
//todo read returnurl from state
//redirect to the stored url returned
//var returnUrl = context.ProtocolMessage.State;
//context.HandleResponse();
//context.Response.Redirect(returnUrl);
return Task.CompletedTask;
};
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.HttpContext.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
}
}
Use AAD B2C docs
https://learn.microsoft.com/en-us/azure/active-directory-b2c/enable-authentication-web-application-options#support-advanced-scenarios
Then follow this
https://learn.microsoft.com/en-us/azure/active-directory-b2c/enable-authentication-web-application-options#pass-an-id-token-hint
Just change context.ProtocolMessage.IdTokenHint to context.ProtocolMessage.State.
Ok, I've got it to work
Couple of things I discovered, but not sure why - I managed to pass a guid in state and get it back without getting that infinite loop, so I thought I'd try the url again but base64 encode, which worked. I did have some further issues which was solved by doing the following:
public static void AddMicrosoftIdentityPlatformAuthentication(this IServiceCollection services, IConfigurationSection azureAdConfig)
{
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
options.ClientId = azureAdConfig["ClientId"];
options.Domain = azureAdConfig["Domain"];
options.Instance = azureAdConfig["Instance"];
options.CallbackPath = azureAdConfig["CallbackPath"];
options.SignUpSignInPolicyId = azureAdConfig["SignUpSignInPolicyId"];
options.Events.OnRedirectToIdentityProvider = context =>
{
var queryString = context.HttpContext.Request.QueryString.HasValue
? context.HttpContext.Request.QueryString.Value
: string.Empty;
if (queryString == null) return Task.CompletedTask;
var queryStringParameters = HttpUtility.ParseQueryString(queryString);
var encodedData = queryStringParameters["returnUrl"]?.Replace("~", "").Base64Encode();
context.ProtocolMessage.State = encodedData;
return Task.CompletedTask;
};
options.Events.OnTokenValidated = context =>
{
var url = context.ProtocolMessage.State.Base64Decode();
var claims = new List<Claim> { new Claim("returnUrl", url) };
var appIdentity = new ClaimsIdentity(claims);
context.Principal?.AddIdentity(appIdentity);
return Task.CompletedTask;
};
options.Events.OnTicketReceived = context =>
{
if (context.Principal == null) return Task.CompletedTask;
var url = context.Principal.FindFirst("returnUrl")?.Value;
context.ReturnUri = url;
return Task.CompletedTask;
};
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.HttpContext.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
}
so now it all works nicely - the user can hit a protected route, get bumped to the login then redirected on return
Maybe not the most elegant soln and I'm not 100% sure of the how or why, but it works

I cannot Access the currently logged in user in my controllers actions (Blazor ASP.NET Core hosted .NET 6 and IdentityServer JWT)

I created a Hosted ASP.NET core Blazor WASM app using the Individual Authentication VS 2022 template which also includes Duende Identity Server configuration, apart from having to switch from Blazor UI to .cshtml for authentication views, it has done what is expected of it. A big problem I'm having is that I can't seem to access the currently logged in user from within my controllers neither using the User property from ControllerBase, nor via the IHttpContextAccessor, the Claims Principle instances all seem to be null when inspected via the debugging mode, meanwhile on the client-side WASM I can access my subject Id, email or whatever I specify in the ProfileService just fine (Which is not really useful unless I would want to fetch the user from the server via the subject ID sent from the client using a parameter or something... which would be disastrous I know..)
Here's my Client/Program.cs:
using System.Globalization;
using CurrieTechnologies.Razor.SweetAlert2;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using Proj.Client;
using Proj.Client.Auth;
using Proj.Client.Helpers;
using Proj.Client.Repository;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
var services = builder.Services;
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after ");
services.AddHttpClient<IHttpService>("Proj.ServerAPI",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("Proj.ServerAPI"));
// SERVICES
services.AddOptions();
services.AddLocalization();
// builder.Services.AddScoped<DialogService>();
// builder.Services.AddScoped<NotificationService>();
// builder.Services.AddScoped<TooltipService>();
// builder.Services.AddScoped<ContextMenuService>();
services.AddApiAuthorization()
.AddAccountClaimsPrincipalFactory<CustomUserFactory>();
var host = builder.Build();
var js = host.Services.GetRequiredService<IJSRuntime>();
var culture = await js.InvokeAsync<string>("getFromLocalStorage", "culture");
CultureInfo selectedCulture;
selectedCulture = culture == null ? new CultureInfo("en") : new CultureInfo(culture);
CultureInfo.DefaultThreadCurrentCulture = selectedCulture;
CultureInfo.DefaultThreadCurrentUICulture = selectedCulture;
await host.RunAsync();
And the Server/Program.cs:
using System.IdentityModel.Tokens.Jwt;
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.EntityFrameworkCore;
using Proj.Server.Data;
using Proj.Server.DbInitializer;
using Proj.Server.Models;
using Proj.Server.Services;
using Proj.Server.Utils;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("MySQLConnectionLocal");
var serverVersion = new MySqlServerVersion(ServerVersion.AutoDetect(connectionString));
services.AddDbContext<ApplicationDbContext>(
dbContextOptions => dbContextOptions
.UseMySql(connectionString, serverVersion)
// The following three options help with debugging, but should
// be changed or removed for production.
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging()
.EnableDetailedErrors()
);
services.AddAutoMapper(typeof(MappingConfig));
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Stores.MaxLengthForKeys = 80;
options.User.RequireUniqueEmail = true;
options.Tokens.ProviderMap.Add("CustomEmailConfirmation",
new TokenProviderDescriptor(
typeof(CustomEmailConfirmationTokenProvider<ApplicationUser>)));
options.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddTransient<CustomEmailConfirmationTokenProvider<ApplicationUser>>();
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(builder.Configuration);
services.AddHttpContextAccessor();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
.AddDeveloperSigningCredential();
builder.Services.AddTransient<IProfileService, ProfileService>();
builder.Services.AddTransient<IFileStorageService, InAppStorageService>();
builder.Services.AddDataProtection();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
services.AddAuthentication()
.AddIdentityServerJwt()
.AddGoogle(googleOptions =>
{
googleOptions.ClientId = builder.Configuration["Authentication:Google:ClientId"];
googleOptions.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
}).AddFacebook(facebookOptions =>
{
facebookOptions.AppId = builder.Configuration["Authentication:Facebook:AppId"];
facebookOptions.AppSecret = builder.Configuration["Authentication:Facebook:AppSecret"];
});
services.Configure<DataProtectionTokenProviderOptions>(o =>
o.TokenLifespan = TimeSpan.FromHours(3));
services.ConfigureApplicationCookie(o =>
{
o.ExpireTimeSpan = TimeSpan.FromDays(5);
o.SlidingExpiration = true;
});
services.AddControllersWithViews();
services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.Run();
And I think that the ProfileService Impelementation would also be Useful:
using IdentityModel;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
namespace Proj.Server.Services;
public class ProfileService : IProfileService
{
public ProfileService()
{
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
context.IssuedClaims.AddRange(nameClaim);
var subClaim = context.Subject.FindAll(JwtClaimTypes.Subject);
context.IssuedClaims.AddRange(subClaim);
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
context.IssuedClaims.AddRange(roleClaims);
await Task.CompletedTask;
}
public async Task IsActiveAsync(IsActiveContext context)
{
await Task.CompletedTask;
}
}
I'd be grateful if anyone could point me in the right direction,
Thanks :)
Set the options in AddApiAuthorization :
builder.Services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
const string OpenId = "openid";
options.IdentityResources[OpenId].UserClaims.Add(JwtClaimTypes.Email);
options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Email);
options.IdentityResources[OpenId].UserClaims.Add(JwtClaimTypes.Id);
options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Id);
options.IdentityResources[OpenId].UserClaims.Add(JwtClaimTypes.Name);
options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Name);
options.IdentityResources[OpenId].UserClaims.Add(JwtClaimTypes.Role);
options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Role);
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
In your controller:
var name = User.FindFirst(ClaimTypes.Name)?.Value;
Turns out this question also describes the same problem I have, By removing
.AddIdentityServerJwt()
From the line
services.AddAuthentication()
.AddIdentityServerJwt()
Everything started working as expected...
Now being the beginner I am, I'm not certain whether or not I should've commented out that line, it may have been important, so if anyone knows a better solution by all means share...
Thanks.

OpenIddict Console Application - Get All Application Clients - ListSync Fails

I have written a simple console application to get all application clients from OpenIddict server. I tried all the possibilities and getting the syntax error. The code is below. I did not find any example in Github and found some outdated example (2017) is no longer relevant now. Please help
public static async Task<bool> Test()
{
var services = CreateServices();
var provider = services.BuildServiceProvider();
var scope = provider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<CustomDbContext>();
await context.Database.EnsureCreatedAsync();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
var result = await manager.FindByClientIdAsync("TestApp"); // It Works
IQueryable<OpenIddictEntityFrameworkCoreApplication> _applicationsQuery = Enumerable.Empty<OpenIddictEntityFrameworkCoreApplication>().AsQueryable();
_applicationsQuery.Where(apps => apps.ClientId != "");
var clients = manager.ListAsync<Func<OpenIddictEntityFrameworkCoreApplication>>(_applicationsQuery); //Compiler Error
return (result != null);
}
private static IServiceCollection CreateServices()
{
var services = new ServiceCollection();
services.AddDbContext<CustomDbContext>(opts =>
{
opts.UseSqlServer(
ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString,
b => b.MigrationsAssembly("Program"));
opts.UseOpenIddict();
});
services.AddOpenIddict() // Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore()
.UseDbContext<CustomDbContext>();
// Enable Quartz.NET integration.
options.UseQuartz();
});
return services;
}
ListAsync() returns an IAsyncEnumerable<T> collection, so you can use await foreach to iterate the collection:
await foreach (var application in manager.ListAsync())
{
Console.WriteLine(await manager.GetClientIdAsync(application));
}
You can also reference the System.Linq.Async package and use the async LINQ extensions. For instance, here's how you could retrieve all the client identifiers of all existing applications:
var identifiers = await manager.ListAsync()
.SelectAwait(application => manager.GetClientIdAsync(application))
.ToListAsync();

DI Registration service type .net core 3.0

I have one n-tier layered app and in Infrastructure module where I'm trying to develop sending email for confirmation an user, I'm getting an error.
No service for type
'IMS.Infrastructure.Helpers.CustomEmailConfirmationTokenProvider`1[Microsoft.AspNetCore.Identity.IdentityUser]'
has been registered.
From code what I had did is next:
public class CustomEmailConfirmationTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
{
public CustomEmailConfirmationTokenProvider(IDataProtectionProvider dataProtectionProvider, IOptions<DataProtectionTokenProviderOptions> options, ILogger<DataProtectorTokenProvider<TUser>> logger) : base(dataProtectionProvider, options)
{
}
}
and for creating services:
services.AddDbContext<ApplicationIdentityDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("Default")));
services.AddIdentityCore<ApplicationUser>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 4;
options.SignIn.RequireConfirmedEmail = true;
options.Tokens.ProviderMap.Add("CustomEmailConfirmation",
new TokenProviderDescriptor(
typeof(CustomEmailConfirmationTokenProvider<IdentityUser>)));
options.Tokens.EmailConfirmationTokenProvider = "CustomEmailConfirmation";
})
.AddEntityFrameworkStores<ApplicationIdentityDbContext>();
services.AddTransient<CustomEmailConfirmationTokenProvider<ApplicationUser>>(o =>
{
var service = new CustomEmailConfirmationTokenProvider<ApplicationUser>(o.GetService<IDataProtectionProvider>(), o.GetService<IOptions<DataProtectionTokenProviderOptions>>(), o.GetService<ILogger<DataProtectorTokenProvider<ApplicationUser>>>());
return service;
});
I will need help to understand how service CustomEmailConfirmationTokenProvider is not registered, what I had did wrong ?
Kind Regards,
Danijel
From perspective of IoC container CustomEmailConfirmationTokenProvider<ApplicationUser> and CustomEmailConfirmationTokenProvider<IdentityUser> are two different and unrelated classes.
You should have both usage and registration to have same type of user.

Resolve dependency on token validation and on controller constructor

I'm trying to share a security object through an application using dependency injection:
services.AddScoped<IRequesterFilter, RequesterFilter>();
The object is populated when a token is validated:
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.Authority = "https://xxxxxx.xxx";
options.TokenValidationParameters.ValidateAudience = false;
options.TokenValidationParameters.ValidIssuer = "https://yyyyyy.yyyy";
options.Events = new JwtBearerEvents
{
OnValidatedToken = context =>
{
var requestFilter = context.HttpContext.ApplicationServices.GetService<IRequesterFilter>();
requestFilter.RequesterLevel = RequesterLevelEnum.Client;
requestFilter.AppId = context.AuthenticationTicket.Principal.FindFirst("appid").Value;
return Task.FromResult(0);
}
};
});
But when I get IRequesterFilter from the controller constructor, the object is not initialized:
public ValuesController(IRequesterFilter requestFilter)
{
var x = requestFilter;
}
I'm using ASP.NET Core 1.0 RC1.
You are using the wrong container.
You are using context.HttpContext.ApplicationServices.GetService<IRequesterFilter>(), which is for application managed/application-wide objects, usually singletons.
For scoped services you have to use the scoped container (called RequestServices), which is would be context.HttpContext.RequestServices.GetService<IRequesterFilter>().
Please note that ApplicationServices will be removed from HttpContext with ASP.NET Core RC2, which will be a breaking change in the way are using it right now.