dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.55k stars 25.3k forks source link

Context.UserIdentifier always is empty #12786

Closed arthas1888 closed 5 years ago

arthas1888 commented 5 years ago

I've tried to test Users in SignalR however this param always is empty, where do I set this one?


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

BrennanConroy commented 5 years ago

Context.UserIdentifier will be null unless you have Authentication in your app. You can read about Authentication and Authorization at https://docs.microsoft.com/aspnet/core/signalr/authn-and-authz?view=aspnetcore-2.2.

By default SignalR will set UserIdentifier to the users ClaimType.NameIdentifier. You can override that behavior by providing your own implementation of IUserIdProvider in startup. e.g. services.AddSingleton<IUserIdProvider, NameUserIdProvider>(); There is an example of this in the doc I linked above.

arthas1888 commented 5 years ago

I did that, my users are authenticated, because my hub is not public

BrennanConroy commented 5 years ago

How are you doing authentication? Do authenticated users have a NameIdentifier claim?

arthas1888 commented 5 years ago

I use authentication with openiddict core, and I put that claim in the ClaimsPrincipal when the token is being created

BrennanConroy commented 5 years ago

Could you provide a sample app that demonstrates the problem?

Or could you put the following code in your Configure method:

app.UseAuthorization();
app.Use(next =>
{
    return context =>
    {
        return next(context);
    };
});

And debug the app, putting a breakpoint on return next(context);, then viewing context.User.Identities[0].Claims and showing what claims are there?

For example: image

arthas1888 commented 5 years ago

Thank you for your help, at last it works but by default Context.UserIdentifier always is null, I had put this code:

public class NameUserIdProvider : IUserIdProvider
    {
        public virtual string GetUserId(HubConnectionContext connection)
        {
            return connection.User?.Identity?.Name;
        }
    }

After that Context.UserIdentifier has value, so is it correct? Always I need add to the project the following code:

services.AddSingleton<IUserIdProvider, NameUserIdProvider>(); this is my claims:

image

BrennanConroy commented 5 years ago

Huh, that is very odd, you have the NameIdentifier claim, so the default UserIdentifier shouldn't be null.

Could you share your Configure method? I'd like to see the order of your middleware.

arthas1888 commented 5 years ago

this is my startup methods:

   public void ConfigureServices(IServiceCollection services)
        {
            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.None;
            });

            services.AddDbContextPool<ApplicationDbContext>(options =>
                {
                    options.UseNpgsql(Configuration.GetConnectionString("PsqlConnection"), o => o.UseNetTopologySuite());
                    //options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
                    options.UseOpenIddict();
                }
            );

            services.AddIdentity<ApplicationUser, ApplicationRole>()
                .AddDefaultUI(UIFramework.Bootstrap4)
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddDefaultAWSOptions(Configuration.GetAWSOptions());
            services.AddAWSService<IAmazonS3>();

            services.Configure<BookstoreDatabaseSettings>(
                Configuration.GetSection(nameof(BookstoreDatabaseSettings)));

            services.AddSingleton<IBookstoreDatabaseSettings>(sp =>
                sp.GetRequiredService<IOptions<BookstoreDatabaseSettings>>().Value);

            services.AddMvc()
                .AddJsonOptions(options =>
                {
                    options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver();
                    //options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                    options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
                })
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddSignalR();
            // Configure Identity to use the same JWT claims as OpenIddict instead
            // of the legacy WS-Federation claims it uses by default (ClaimTypes),
            // which saves you from doing the mapping in your authorization controller.
            services.Configure<IdentityOptions>(options =>
            {
                options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
                options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
                options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
            });

            // Adds services required for using options.
            services.AddOptions();
            services.AddMemoryCache();

            services.AddScoped<IRepository<ApplicationUser>, UsersManager>();
            services.AddScoped<IRepository<ApplicationRole>, RolesManager>();
            services.AddScoped<IRepository<IdentityRoleClaim<string>>, ClaimsManager>();
            services.AddScoped<IRepository<Permission>, GenericModelFactory<Permission>>();
            services.AddScoped<IRepository<Audit>, AuditManager>();
            services.AddScoped<IRepository<CommonOption>, GenericModelFactory<CommonOption>>();
            services.AddScoped<IRepository<Brand>, BrandManager>();
            services.AddScoped<IRepository<Model>, ModelManager>();
            services.AddScoped<IRepository<Vehicle>, GenericModelFactory<Vehicle>>();
            services.AddScoped<IRepository<Journey>, JourneyManager>();

            //services.AddSingleton<MongoCRUDService<Chat>();

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new Info
                {
                    Version = "v1.0.0",
                    Title = "Carpooling API 2019",
                    Description = "",
                    TermsOfService = "None",
                    Contact = new Contact { Name = "Ser Soluciones SAS", Email = "contacto@sersoluciones.com", Url = "https://www.sersoluciones.com/" },
                });
                options.DescribeAllEnumsAsStrings();
            });

            // add OpenIddict
            services.AddOpenIddict()
                .AddCore(options =>
                {
                    // AddEntityFrameworkCoreStores() is now UseEntityFrameworkCore().
                    options.UseEntityFrameworkCore()
                           .UseDbContext<ApplicationDbContext>();
                })

                .AddServer(options =>
                {
                    // AddMvcBinders() is now UseMvc().
                    options.UseMvc();
                    options.EnableAuthorizationEndpoint("/connect/authorize")
                           .EnableLogoutEndpoint("/connect/logout")
                           .EnableTokenEndpoint("/connect/token")
                           .EnableUserinfoEndpoint("/api/userinfo");

                    options.AllowAuthorizationCodeFlow()
                           .AllowClientCredentialsFlow()
                           .AllowPasswordFlow()
                           .AllowRefreshTokenFlow();

                    options.RegisterScopes(OpenIdConnectConstants.Scopes.Email,
                                           OpenIdConnectConstants.Scopes.Profile,
                                           OpenIddictConstants.Scopes.Roles);

                    // This API was removed as client identification is now
                    // required by default. You can remove or comment this line.
                    //
                    // options.RequireClientIdentification();

                    options.EnableRequestCaching();

                    // This API was removed as scope validation is now enforced
                    // by default. You can safely remove or comment this line.
                    //
                    // options.EnableScopeValidation();

                    options.SetAccessTokenLifetime(TimeSpan.FromDays(10));

                    options.DisableHttpsRequirement();
                });

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = OAuthValidationDefaults.AuthenticationScheme;
            })
                .AddOAuthValidation(options =>
                {
                    options.Events.OnRetrieveToken = context =>
                    {
                        context.Token = context.Request.Query["access_token"];

                        return Task.CompletedTask;
                    };
                })
                .AddGoogle(options =>
                {
                    options.ClientId = "200532511210-srh778iqpokebpj435lcs3ldf9lshboh.apps.googleusercontent.com";
                    options.ClientSecret = "L-YjJ3dfq-HWO_mN-lGmn4EK";
                });

            // Add application services.
            services.AddSingleton<IEmailSender, AuthMessageSender>();
            //services.AddSingleton<ISmsSender, AuthMessageSender>();

            services.Configure<IdentityOptions>(options =>
            {
                // Password settings
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = true;
                options.Password.RequireLowercase = true;
            });

            services.Configure<GzipCompressionProviderOptions>(options => options.Level = System.IO.Compression.CompressionLevel.Optimal);
            services.AddResponseCompression();

            services.AddCors();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment() || env.IsStaging())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/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.UseResponseCompression();
            //app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseCors(options =>
            {
                // this defines a CORS policy called "default"
                options
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader();
            });
            app.UseAuthentication();

            app.UseMvcWithDefaultRoute();
            #region UseSwagger
            app.UseSwagger();
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "Carpooling API V1");
                c.DocExpansion(DocExpansion.None);
            });
            #endregion

            app.UseSignalR(routes =>
            {
                routes.MapHub<ChatHub>("/chatHub");
            });

            //DBContextSeedData.InitializeAsync(app.ApplicationServices, CancellationToken.None).GetAwaiter().GetResult();
            //DBContextSeedData.SeedRoles(app.ApplicationServices).GetAwaiter().GetResult();
            //DBContextSeedData.SeedPermissions(app.ApplicationServices).GetAwaiter().GetResult();
            //DBContextSeedData.CreateSuperUser(app.ApplicationServices).GetAwaiter().GetResult();

            Console.WriteLine($"Starting Carpooling API, Db in {Configuration["ConnectionStrings:PsqlConnection"]}");
        }
BrennanConroy commented 5 years ago

Thanks for sharing.

If the options.ClientId = "XYZ"; and options.ClientSecret = "ZYX"; are secrets and not meant to be shared, make sure you change your secrets because they are posted in the code snippet.

@Tratcher Can you see anything obvious as to why User?.FindFirst(ClaimTypes.NameIdentifier)?.Value would be null in SignalR based on the above Startup? @arthas1888 provided a screenshot that also shows the NameIdentifier claim in the Claims.

BrennanConroy commented 5 years ago

@arthas1888 Could you load symbols while debugging for "Microsoft.AspNetCore.SignalR.Core" and add a function breakpoint (ctrl-k, b) with the "Function Name:" set as "Microsoft.AspNetCore.SignalR.DefaultUserIdProvider.GetUserId" then connect with a client and see why the NameIdentifier can't be found?

arthas1888 commented 5 years ago

OK, is strange now it works, before when I deleted this line in my startup services.AddSingleton<IUserIdProvider, NameUserIdProvider>(); the hub couldn't find the NameIdentifier but now it can, so I have another question, when I use Authentication under Cookie How does .net Core set this ClaimType.NameIdentifier?

BrennanConroy commented 5 years ago

Well whoever created the Cookie will have set some claims on it before giving it to the client and will know how to read those claims when the client sends the cookie back.

See the docs for an example of creating a cookie with some claims.

arthas1888 commented 5 years ago

Thank you so much, your help has been very useful

BrennanConroy commented 5 years ago

You're welcome, glad to help :)

Can we close the issue?

arthas1888 commented 5 years ago

yes, please :)