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

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.

Related

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();

More than one IDP for SAML2 login with Sustainsys using .NET Core

We are using Sustainsys middleware with .NET Core to connect to an SAML2 IDP. It works well.
However, when we add more than one IDP in the Startup.cs we get in trouble. The user will select which IDP to login to and then the code should send a challenge to that IDP.
How to we specify which IDP in the code?
Using standard .NET Framework it is straight forward:
Context.GetOwinContext().Environment.Add("saml2.idp", new Entity(IDP2EntityId));
but there is no such construct in the .NET Core middleware.
Here is my code. Basically I add two IDPs during startup but I don't know how to specify which one during login/challenge? With this code IDP-1 is always selected because it was the first one added.
STARTUP.CS
public void ConfigureServices(IServiceCollection services)
{
var authenticationBuilder = GetAuthenticationBuilder(services);
string authenticationScheme = "saml2.idp"
authenticationBuilder.AddSaml2(authenticationScheme, options =>
{
options.SPOptions = GetSPOptions();
// Add IDP-1
options.IdentityProviders.Add(
new IdentityProvider(new EntityId(IDPEntityUrl1), options.SPOptions)
{
MetadataLocation = IDPMetadataUrl1
});
// Add IDP-2
options.IdentityProviders.Add(
new IdentityProvider(new EntityId(IDPEntityUrl2), options.SPOptions)
{
MetadataLocation = IDPMetadataUrl2
});
}
}
LOGINCONTROLLER.CS
string saml2AuthenticationScheme = "saml2.idp";
var props = new AuthenticationProperties
{
RedirectUri = returnUrl,
Items = { { "scheme", saml2AuthenticationScheme } }
};
return Challenge(properties: props, saml2AuthenticationScheme);
How do I specify which IDP to use in the LoginController?
I found the solution. We studied the Sustainsys code and found the undocumented (?) feature to specify the IDP in the AuthenticationProperties.Items with an "idp" item. Like this:
LoginController.cs
string saml2AuthenticationScheme = "saml2.idp";
var props = new AuthenticationProperties
{
RedirectUri = returnUrl,
Items = { { "scheme", saml2AuthenticationScheme }, { "idp", theSelectedIDPIdentityId } }
};
return Challenge(properties: props, saml2AuthenticationScheme);

Sustainsys Saml2 Handler AuthenticateAsync() method operation is not implemented

I'm trying a simple implementation in my Asp net Core application of Saml2 to integrate with an Ad FS server. I can't figure why I am getting this error. I downloaded the samples from the gitHub and tried to adapt it in my application.
NotImplementedException: The method or operation is not implemented.
Sustainsys.Saml2.AspNetCore2.Saml2Handler.AuthenticateAsync()
Here's my implementation, my application is running on Asp Net Core
On StartUp
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = Saml2Defaults.Scheme;
})
.AddSaml2(options =>
{
options.SPOptions.EntityId = new EntityId("http://myAdfsServer.myDomain.com/adfs/services/trust");
options.SPOptions.ReturnUrl = new Uri("https://localhost:5000");
options.IdentityProviders.Add(
new IdentityProvider(new EntityId("http://myAdfsServer.myDomain.com/adfs/services/trust"), options.SPOptions)
{
LoadMetadata = true,
MetadataLocation = "https://myAdfsServer.myDomain.com/FederationMetadata/2007-06/FederationMetadata.xml"
//MetadataLocation = "FederationMetadata.xml"
});
//options.SPOptions.ServiceCertificates.Add(new X509Certificate2(certificate.ToString()));
})
.AddCookie();
On my Controller
trying something similar to Sustainsys SAML2 Sample for ASP.NET Core WebAPI without Identity
[Authorize(AuthenticationSchemes = Saml2Defaults.Scheme)]
public class AuthenticationController : Controller
{
public AuthenticationController()
{
}
[AllowAnonymous]
public async Task LoginAdfs()
{
string redirectUri = string.Concat("https://localhost:5000", "/verifyAdfs");
try
{
new ChallengeResult(
Saml2Defaults.Scheme,
new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(LoginCallback), new { redirectUri })
});
}catch(Exception e)
{
}
}
[AllowAnonymous]
public async Task<IActionResult> LoginCallback(string returnUrl)
{
var authenticateResult = await HttpContext.AuthenticateAsync(Saml2Defaults.Scheme);
//_log.Information("Authenticate result: {#authenticateResult}", authenticateResult);
// I get false here and no information on claims etc.
if (!authenticateResult.Succeeded)
{
return Unauthorized();
}
var claimsIdentity = new ClaimsIdentity("Email");
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
// _log.Information("Logged in user with following claims: {#Claims}", authenticateResult.Principal.Claims);
await HttpContext.SignInAsync("Email", new ClaimsPrincipal(claimsIdentity));
return LocalRedirect(returnUrl);
}
}
note: I've got a client that won't expose his MetaData in a URL, so I'll need to adapt it and set manually the metadata parameters
I'm stuck in this error, I does not even hit my method LoginAdfs.
The Saml2 handler cannot be used as an authencation scheme, it is a challenge scheme.
I guess that the LoginAdfs() method works fine, but that it's the LoginCallback that fails. The reason should be the call to HttpContext.AuthenticationAsync(Saml2Defaults.Scheme).
You should instead authenticate with the cookie scheme - because that's what keeps the session. Internally when the challenge is completed, the Saml2 handler will use the DefaultSignInScheme to preserve the result in a session (through a cookie, as that's the default sign in scheme).

Asp Net Core Identity UserManager change context

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

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.