dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.32k stars 9.97k forks source link

Using Standard OIDC Authentication and CustomAuthenticationStateProvider Together Causes Error #54268

Closed MustafaSuyi closed 6 months ago

MustafaSuyi commented 7 months ago

Is there an existing issue for this?

Describe the bug

We have a Blazor Web Assembly Client that needs OIDC and custom login together, the existing solutions from the Microsoft documentations work fine when used separately,

builder.Services.AddOidcAuthentication(x => { //options });

Or

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();

But when we use them together, this exception thrown: Unhandled exception rendering component: Specified cast is not valid. System.InvalidCastException: Specified cast is not valid.

I have seen a similar issues here and there but there's no solution using these standard components without implementing multiple interfaces like IRemoteAuthenticationService & IAccessTokenProvider to CustomAuthenticationStateProvider.

Do you plan an update so we can use these together, if not can you point out a work around?

Expected Behavior

No response

Steps To Reproduce

This is a sample CustomProvider, we want to be able to login this way as well as with OIDC

public class CustomAuthenticationStateProvider : AuthenticationStateProvider, IDisposable { private readonly UserService _userService; public User CurrentUser { get; private set; } = new User(string.Empty, string.Empty);

public CustomAuthenticationStateProvider(UserService userService)
{
    _userService = userService;
    AuthenticationStateChanged += OnAuthenticationStateChangedAsync;
}

public async Task LoginAsync(IClinician clinician)
{
    var principal = new ClaimsPrincipal();
    var user = _userService.ConvertClinicianToUser(clinician);

    if (user is not null)
    {
        await _userService.PersistUserToBrowserAsync(user);
        principal = User.ToClaimsPrincipal(user);
    }

    NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(principal)));
}

public async Task LogoutAsync()
{
    await _userService.ClearBrowserUserDataAsync();
    NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(new())));
}

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
    var principal = new ClaimsPrincipal();
    var user = await _userService.FetchUserFromBrowserAsync();

    if (user is not null)
    {
        bool userExists = await _userService.CheckIfUserExistsInDatabaseAsync(user);

        if (userExists)
        {
            principal = User.ToClaimsPrincipal(user);
            CurrentUser = user;
        }
    }

    return new AuthenticationState(principal);
}

public void Dispose()
{
    AuthenticationStateChanged -= OnAuthenticationStateChangedAsync;
}

private async void OnAuthenticationStateChangedAsync(Task<AuthenticationState> task)
{
    var authenticationState = await task;

    if (authenticationState is not null)
    {
        CurrentUser = User.FromClaimsPrincipal(authenticationState.User);
    }
}

}

Exceptions (if any)

crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Specified cast is not valid. System.InvalidCastException: Specified cast is not valid. at Microsoft.Extensions.DependencyInjection.WebAssemblyAuthenticationServiceCollectionExtensions.<>c1`1[[Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationState, Microsoft.AspNetCore.Components.WebAssembly.Authentication, Version=8.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]].b1_0(IServiceProvider sp) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2[[Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeResolverContext, Microsoft.Extensions.DependencyInjection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[System.Object, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].VisitCallSiteMain(ServiceCallSite callSite, RuntimeResolverContext argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2[[Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeResolverContext, Microsoft.Extensions.DependencyInjection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[System.Object, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].VisitCallSite(ServiceCallSite callSite, RuntimeResolverContext argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.RuntimeServiceProviderEngine.<>cDisplayClass4_0.b0(ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) at Microsoft.AspNetCore.Components.ComponentFactory.<>cDisplayClass9_0.gInitialize|1(IServiceProvider serviceProvider, IComponent component) at Microsoft.AspNetCore.Components.ComponentFactory.InstantiateComponent(IServiceProvider serviceProvider, Type componentType, IComponentRenderMode callerSpecifiedRenderMode, Nullable1 parentComponentId) at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateChildComponentOnFrame(RenderTreeFrame[] frames, Int32 frameIndex, Int32 parentComponentId) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(DiffContext& diffContext, Int32 frameIndex) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewSubtree(DiffContext& diffContext, Int32 frameIndex) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InsertNewFrame(DiffContext& diffContext, Int32 newFrameIndex) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl) at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Renderer renderer, RenderBatchBuilder batchBuilder, Int32 componentId, ArrayRange1 oldTree, ArrayRange`1 newTree) at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment, Exception& renderFragmentException) at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry) at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()

.NET Version

.NET 8

Anything else?

No response

PatrickBateman91 commented 6 months ago

Did you find any way to solve this?

MustafaSuyi commented 6 months ago

Nope, I couldn't.

halter73 commented 6 months ago

My response at https://github.com/dotnet/aspnetcore/issues/53732#issuecomment-2021878231 might be helpful for trying to get a custom AuthenticationStateProvider working. If you're hosting your app using ASP.NET Core on the server, I recommend using the server-side AddOpenIdConnect and AddCookie on the server like in the sample does in https://learn.microsoft.com/en-us/aspnet/core/blazor/security/blazor-web-app-with-oidc?view=aspnetcore-8.0&pivots=with-bff-pattern

If you must do everything client-side, you might need to have CustomAuthenticationStateProvider derive from RemoteAuthenticationService<RemoteAuthenticationState, RemoteUserAccount, OidcProviderOptions>. This would allow the IRemoteAuthenticationService<TRemoteAuthenticationState> registration to work. You cannot have multiple AuthenticationStateProvider's working at once. There's no way for the framework to know which one to pick.

halter73 commented 6 months ago

If you need more assistance, we'll need a complete repro of the issue published to GitHub, so we can see exactly what's going wrong.

dotnet-policy-service[bot] commented 6 months ago

Thank you for filing this issue. In order for us to investigate this issue, please provide a minimal repro project that illustrates the problem without unnecessary code. Please share with us in a public GitHub repo because we cannot open ZIP attachments, and don't include any confidential content.

dotnet-policy-service[bot] commented 6 months ago

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment. If it is closed, feel free to comment when you are able to provide the additional information and we will re-investigate.

See our Issue Management Policies for more information.