I am trying to use the localStorageService in my CustomAuthStateProvider class so I can create a AuthenticationState based on a key in LocalStorage (Just to learn and to practice). Howevever, when I run my application I get an error telling me that no suitable constructor can be found for CustomAuthStateProvider. The error makes sense but I don't understand how I can fix it and haven't found much online.
Here is the error:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: A suitable constructor for type 'BlazorBattles.Client.CustomAuthStateProvider' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
System.InvalidOperationException: A suitable constructor for type 'BlazorBattles.Client.CustomAuthStateProvider' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
Here is my CustomAuthStateProvider implimenting AuthenticationStateProvider:
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly ILocalStorageService _localStorageService;
CustomAuthStateProvider(ILocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
if (await _localStorageService.GetItemAsync<bool>("isAuthenticated"))
{
ClaimsIdentity claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "Thomas")
}, "Test Authentication");
ClaimsPrincipal user = new ClaimsPrincipal(claimsIdentity);
AuthenticationState state = new AuthenticationState(user);
//Tell all the components that the Auth state has changed
NotifyAuthenticationStateChanged(Task.FromResult(state));
return (state);
}
//This will result in an unauthorised user because it does not have a claims identity
return (new AuthenticationState(new ClaimsPrincipal()));
}
}
Here is my Program.cs
using BlazorBattles.Client;
using BlazorBattles.Client.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Blazored.Toast;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddBlazoredToast();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IBananaService, BananaService>();
builder.Services.AddScoped<IUnitService, UnitService>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
await builder.Build().RunAsync();
I am using V4.3.0 for Blazored.LocalStorage and V6 for Microsoft.AspNetCore.Components.Authorization
Thanks.
It works as expected when I remove the constructor and references to LocalStorage but when I try to inject LocalStorage to use it then I get the error. I'm not sure how to make use of the constrctor correctly in this specific case?
Update:
The solution to my problem here is to add the public keyword for the constructor
Try to register CustomAuthStateProvider service like this:
// Make the same instance accessible as both AuthenticationStateProvider and CustomAuthStateProvider
builder.Services.AddScoped<CustomAuthStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthStateProvider>());
I think your main issue is your custom AuthenticationStateProvider inheritance.
Here is my "Pass Through" WASM provider that injects (but never uses) Local Storage. It just gets the user from the base code. Note it's inheritance.
public class CustomAuthenticationStateProvider
: RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, MsalProviderOptions>
{
private readonly ILocalStorageService _localStorageService;
public CustomAuthenticationStateProvider(
IJSRuntime jsRuntime,
IOptionsSnapshot<RemoteAuthenticationOptions<MsalProviderOptions>> options,
NavigationManager navigation,
AccountClaimsPrincipalFactory<RemoteUserAccount> accountClaimsPrincipalFactory,
ILocalStorageService localStorageService
)
: base(jsRuntime, options, navigation, accountClaimsPrincipalFactory)
{
_localStorageService= localStorageService;
}
public async override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var auth = await base.GetAuthenticationStateAsync();
return new AuthenticationState(auth.User ?? new ClaimsPrincipal());
}
}
For reference here's my Program using AzureAD for authentication.
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddHttpClient("Blazr.AzureOIDC.WASM.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
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("Blazr.AzureOIDC.WASM.ServerAPI"));
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://api.id.uri/access_as_user");
});
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
await builder.Build().RunAsync();
The issue with my code above is that I had missed out the public keyword in my constructor and now it works as expected. A huge thank you to everyone who commented on my post and provided potential solutions, I appreciate the time you took to help me out!
Original code:
CustomAuthStateProvider(ILocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
Updated code:
public CustomAuthStateProvider(ILocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
Related
I am using Blazor WASM with AzureB2C to call an API hosted in Azure Functions. I would like to call my API on a successful login to add/update user info into a database. I have been following this guide. When trying to inject my typed httpclient into the AccountClaimsPrincipalFactory I am met with a runtime error:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: ValueFactory attempted to access the Value property of this instance.
System.InvalidOperationException: ValueFactory attempted to access the Value property of this instance.
This shows in the browser, but the app compiles and runs just fine. The codes works great if I don't inject my PlatformServiceClient, but I need to make the API call to record the user. The following files are involved. I adjusted some things to simplify. This seems like the appropriate approach, but I have not seen examples where an api call was made in the claims factory.
CustomAccountFactory.cs
public class CustomAccountFactory: AccountClaimsPrincipalFactory<CustomUserAccount>
{
public IPlatformServiceClient client { get; set; }
public CustomAccountFactory(NavigationManager navigationManager,
IPlatformServiceClient platformServiceClient,
IAccessTokenProviderAccessor accessor) : base(accessor)
{
client = platformServiceClient;
}
public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
CustomUserAccount account, RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity.IsAuthenticated)
{
//call the API here
await client.RegisterUserAsync();
}
return initialUser;
}
}
Program.cs excerpt
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient<IPlatformServiceClient, PlatformServiceClient>(
client => client.BaseAddress = new Uri(builder.Configuration["PlatformServiceUrl"]))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, CustomUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
options.ProviderOptions.DefaultAccessTokenScopes.Add("access_as_user");
options.ProviderOptions.LoginMode = "redirect";
options.UserOptions.RoleClaim = "roles";
}).AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, CustomUserAccount, CustomAccountFactory>();
CustomAuthorizationMessageHandler.cs
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "http://localhost:7071" },
scopes: new[] { "access_as_user" });
}
}
I solved this by creating a named instance of the client and passing an IHttpClientFactory into the CustomAccountFactory.
builder.Services.AddHttpClient<PlatformServiceClient>("PlatformServiceClient",
client => client.BaseAddress = new Uri(builder.Configuration["PlatformServiceUrl"]))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
There I can create a client, but I have to setup my urls manually vs using the typed client where I have this work already done.
var client = factory.CreateClient("PlatformServiceClient");
var response = await client.GetAsync("/user/me");
I also registered the new client prior to calling AddMsalAuthenication
builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("PlatformServiceClient"));
I did all of this following the code found here by Coding Flamingo. It is all working as expected.
Here are some artifacts to help understand the issue:
Sample Code - Github repo
Deployed Application - no longer available
Update: I have followed this YouTube video which I now believe to be the correct way of accessing information about the authenticated user in dependent services for a Blazor Server application: https://www.youtube.com/watch?v=Eh4xPgP5PsM.
I've updated the Github code to reflect that solution.
I have the following classes that I register using dependency injection in my ASP.NET MVC Core application.
public class UserContext
{
ClaimsPrincipal _principal;
public UserContext(ClaimsPrincipal principal) => _principal = principal;
public bool IsAuthenticated => _principal.Identity.IsAuthenticated;
}
public class WrapperService
{
UserContext _userContext;
public WrapperService(UserContext context) => _userContext = context;
public bool UserHasSpecialAccess()
{
return _userContext.IsAuthenticated;
}
}
The IoC dependency registrations are configured in Startup.cs
services.AddScoped<ClaimsPrincipal>(x =>
{
var context = x.GetRequiredService<IHttpContextAccessor>();
return context.HttpContext.User;
});
services.AddScoped<UserContext>();
services.AddScoped<WrapperService>();
I recently enabled Blazor in the MVC application and wanted to use my DI registered services from within my Blazor components.
I injected the service in a Blazor component in order to use it like so:
#inject WrapperService _Wrapper
However, when I attempt to use the service from a server side handler, the request fails with an exception complaining that the services could not be constructed - due to IHttpContext not existing on subsequent calls to the server.
<button #onclick="HandleClick">Check Access</button>
async Task HandleClick()
{
var hasPermission = _Wrapper.UserHasSpecialAccess(); // fails 😔
}
I think I understand why the use of IHttpContextAccessor is not working/recommended in Blazor Server apps. My question is, how can I access the claims I need in my services without it?
The odd thing to me is that this all works when I run it under IIS Express in my development environment, but fails when I deploy and attempt to run it from within an Azure AppService.
This is what work for me, writing a derived class for AuthenticationStateProvider.
public class AppAuthenticationStateProvider : AuthenticationStateProvider
{
private ClaimsPrincipal principal;
// Constructor, only needed when injections required
public AppAuthenticationStateProvider(/* INJECTIONS HERE */)
: base()
{
principal ??= new();
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return Task.FromResult(new AuthenticationState(principal));
}
// Method called from login form view
public async Task LogIn(/* USER AND PASSWORD */)
{
// Create session
principal = new ClaimsPrincipal(...);
var task = Task.FromResult(new AuthenticationState(principal));
NotifyAuthenticationStateChanged(task);
}
// Method called from logout form view
public async Task LogOut()
{
// Close session
principal = new();
var task = Task.FromResult(new AuthenticationState(principal));
NotifyAuthenticationStateChanged(task);
}
Then, at program/startup you add these lines:
// Example for .Net 6
builder.Services.AddScoped<AuthenticationStateProvider, AppAuthenticationStateProvider>();
builder.Services.AddScoped<ClaimsPrincipal>(s =>
{
var stateprovider = s.GetRequiredService<AuthenticationStateProvider>();
var state = stateprovider.GetAuthenticationStateAsync().Result;
return state.User;
});
That's it. Now you can inject ClaimsPrincipal wherever you want.
You can inject AuthenticationStateProvider into your Service constructor and then use
var principal = await _authenticationStateProvider.GetAuthenticationStateAsync();
AuthenticationStateProvider is a Scoped service so yours has to be too.
Use CascadingAuthenticationState to access the claims principal
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-5.0#expose-the-authentication-state-as-a-cascading-parameter-1
If you need to use your own logic, you will need to implement your own authentication state provider.
If you want to use a service to use ClaimsPrincipal you can do the following:
ClaimsPrincipalUserService.cs
ClaimsPrincipal claimsPrincipal;
void SetClaimsPrincipal(ClaimsPrincipal cp)
{
claimsPrincipal = cp;
// any logic + notifications which need to be raised when
// ClaimsPrincipal has changes
}
Inject this service as scoped in the startup.
In the layout
MainLayout.razor
#inject ClaimsPrincipalUserService cpus;
[CascadingParameter]
public Task<AuthenticationState> State {get;set;}
protected override async Task OnInitializedAsync()
{
var state = await State;
var user = state.User; // Get claims principal.
cpus.SetClaimsPrincipal(user);
}
I am trying to setup logging in my server side Blazor app.
Here is a class I have that gets data from an API:
public class LicenseService
{
private ILogger _logger;
public LicenseService(ILogger<LicenseService> logger)
{
_logger = logger;
}
public async Task<List<Licenses>> GetLicensesAsync()
{
List<Licenses> model = null;
var client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
var task = await client.GetAsync("https://my.domain/v1/licenses");
if(!task.IsSuccessStatusCode)
{
_logger.LogError($"Result was {task.StatusCode}");
}
_logger.LogError($"Result was {task.StatusCode}");
var jsonString = await task.Content.ReadAsStringAsync();
model = JsonConvert.DeserializeObject<List<Licenses>>(jsonString);
return model;
}
}
Here is a .razor page that calls that class. What do I pass in to new LicenseService();? I can't instantiate a new instance of Ilogger or Ilogger<LicenseService>.
private async Task<List<Licenses>> GetLicenseData()
{
LicenseService service = new LicenseService();
var licenseData = await service.GetLicensesAsync();
return licenseData;
}
I'm sure this is because I don't really understand how dependency injection works in dotnet core.
Ideally, if you are using dependency injection as it's designed, you wouldn't be creating a new instance of LicenseService there at all, but rather injecting the dependency, as the name suggests.
During your Startup, you might have the following service registration (or something similar).
services.AddScoped<LicenseService>();
Within your Razor page's constructor, the service container can now automatically resolve this dependency for you, taking care of all of its dependencies, too, assuming those dependencies were also registered.
public class YourPageModel : PageModel
{
private readonly LicenseService _licenseService;
public YourPageModel (LicenseService licenseService)
{
_licenseService = licenseService;
}
public async Task<List<Licenses>> GetLicenseData()
{
var licenseData = await _licenseService.GetLicensesAsync();
return licenseData;
}
}
Update:
The service can be injected into your Blazor component with DI using this syntax at the top:
#inject LicenseService licenseService
And then referenced as licenseService in your method. See this link for an example:
https://learn.microsoft.com/en-us/aspnet/core/tutorials/build-your-first-blazor-app?view=aspnetcore-3.1#dependency-injection
I added AWSSDK.S3 to my project because I want to use S3FileInfo to access files on S3 in a clean way, and I registered an instance of the AmazonS3Client on Autofac to get it in my services.
Something like this for the registration on Autofac:
builder.Register(context => {
var credentials = new BasicAWSCredentials("accessKeyId", "SecretAccessKey");
var config = new AmazonS3Config {
RegionEndpoint = RegionEndpoint.GetBySystemName("regionEndpoint")
};
return new AmazonS3Client(credentials, config);
}).As<IAmazonS3>().SingleInstance();
The point is that if I want to add more configurations to access different buckets on different accounts like this I cannot.
What's the cleanest way to register on Autofac more instances of IAmazonS3 with different configurations?
There is many way to do what you want. It depends on where you get your credentials.
If you have know the credentials when autofac is building you can use named instance
builder.Register(context => {
var credentials = new BasicAWSCredentials("accessKeyId1", "SecretAccessKey1");
var config = new AmazonS3Config {
RegionEndpoint = RegionEndpoint.GetBySystemName("regionEndpoint")
};
return new AmazonS3Client(credentials, config);
}).Named<IAmazonS3>("client1").SingleInstance();
builder.Register(context => {
var credentials = new BasicAWSCredentials("accessKeyId2", "SecretAccessKey2");
var config = new AmazonS3Config {
RegionEndpoint = RegionEndpoint.GetBySystemName("regionEndpoint")
};
return new AmazonS3Client(credentials, config);
}).Named<IAmazonS3>("client2").SingleInstance();
To resolve them you can use IIndex<String, IAmazonS3> or use the WithParameter method at registration or an autofac module.
More information on Named instances are available on the documentation : Named and Keyed services
If you have the credentials at runtime you can resolve a factory. let's say ServiceX needs a IAmazonS3Client instance you can have a dependency on Func<BasicAwsCredentials, AmazonS3Config, IAmazonS3Client> and Autofac will do the magic for you.
public class ServiceX
{
public ServiceX(Func<BasicAwsCredentials, AmazonS3Config, IAmazonS3Client> factory)
{
this._amazonS3Factory = factory;
}
private readonly Func<BasicAwsCredentials, AmazonS3Config, IAmazonS3Client> _amazonS3Factory;
public void Do()
{
IAmazonS3Client client = this._amazonS3FActory(credentials, config);
// do something with client
}
}
Not finding an answer that fits, after thinking a bit about it I decided to follow this approach, probably not very clean but it works:
public interface IAmazonS3FirstConfig : IAmazonS3
{
}
public class AmazonS3ClientFirstConfig : AmazonS3Client, IAmazonS3FirstConfig
{
public AmazonS3ClientFirstConfig(BasicAWSCredentials credentials, AmazonS3Config config)
: base(credentials, config)
{
}
}
Using the ad-hoc class and interface to register this configuration, and another couple for the second configuration.
I am trying to implement IdentityServer3 into an existing project that uses Autofac. The problem I have come across is that when I set up my custom services, if I run my project and try to authenticate I get this error:
"An error occurred when trying to create a controller of type 'TokenEndpointController'. Make sure that the controller has a parameterless public constructor."
Now I know this is a generic autofac error when a service has not been set up correctly.
The error actually moans about my custom UserService stating:
None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'Business.IdentityServer.IdentityServerUserService' can be invoked with the available services and parameters:
Cannot resolve parameter 'Business.Providers.IUserProvider userProvider' of constructor 'Void .ctor(Business.Providers.IUserProvider)'.
Now I already had a UserProvider before I started using IdentityServer3 and it was set up in autofac like this:
builder.RegisterType<DatabaseContext>().As<DbContext>().InstancePerDependency();
builder.RegisterType<UserProvider>().As<IUserProvider>().InstancePerDependency();
This was working before, so I know that the UserProvider does actually have all it's dependencies.
My UserService looks like this:
public class IdentityServerUserService : UserServiceBase
{
private readonly IUserProvider _userProvider;
public IdentityServerUserService(IUserProvider userProvider)
{
_userProvider = userProvider;
}
public override async Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
var user = await _userProvider.FindAsync(context.UserName, context.Password);
if (user != null && !user.Disabled)
{
// Get the UserClaims
// Add the user to our context
context.AuthenticateResult = new AuthenticateResult(user.Id, user.UserName, new List<Claim>());
}
}
}
Does anyone know how I can resolve this issue?
This was due to how I was configuring the factory. I now have it like this:
private static IdentityServerServiceFactory Configure(this IdentityServerServiceFactory factory, CormarConfig config)
{
var serviceOptions = new EntityFrameworkServiceOptions { ConnectionString = config.SqlConnectionString };
factory.RegisterOperationalServices(serviceOptions);
factory.RegisterConfigurationServices(serviceOptions);
factory.CorsPolicyService = new Registration<ICorsPolicyService>(new DefaultCorsPolicyService { AllowAll = true }); // Allow all domains to access authentication
factory.Register<DbContext>(new Registration<DbContext>(dr => dr.ResolveFromAutofacOwinLifetimeScope<DbContext>()));
factory.UserService = new Registration<IUserService>(dr => dr.ResolveFromAutofacOwinLifetimeScope<IUserService>());
factory.ClientStore = new Registration<IClientStore>(dr => dr.ResolveFromAutofacOwinLifetimeScope<IClientStore>());
factory.ScopeStore = new Registration<IScopeStore>(dr => dr.ResolveFromAutofacOwinLifetimeScope<IScopeStore>());
return factory;
}
My user service is still the same, so everything works.