SignalR Hub not receiving user from client WPF (.netcore 3.1) - wpf

I have a WPF client which connects successfully to a Hub, but I cannot pass the user of the client to the Hub.
My connection.User?.Identity?.Name in my class implementing from IUserIdProvider returns null.
For my WPF client I use this to connect against the Hub:
_connection = new HubConnectionBuilder()
.WithUrl(viewModel.Endpoint, opts =>
{
opts.Credentials = new NetworkCredential("user", "password", "domain");
opts.UseDefaultCredentials = true;
})
.Build();
I have then the following provider registered as singleton:
public class NameUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
return connection.User?.Identity?.Name;
}
}
As I mentioned above, the connection.User?.Identity?.Name; is returning null.
I don't know what else I can do to pass the user name from my client (WPF) to my Hub.
By the way, my Startup.cs looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddLogging();
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
services.AddSignalR(hubOptions =>
{
hubOptions.EnableDetailedErrors = true;
});
services.AddScoped<IBuildsService, BuildsService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<SyncCodeHub>("/signalr");
});
}
Any help would be much appreciated.
EDIT:
I update the code with:
services.AddAuthentication(IISDefaults.AuthenticationScheme);
But the problem continues, the identity user (IUserIdProvider) is returning null when called from the WPF client. I'm running the API locally with IISExpress.
EDIT:
From Microsoft docs:
Windows Authentication is only supported by the browser client when using Microsoft Internet Explorer or Microsoft Edge.
So I'm wondering if this is even possible with an Desktop as a client. I assume it should work, so I'm wondering if I'm still missing a point or if this is a bug related to the Version of SignalR I#m using (3.1.3)

You need to configure your ASP.NET Core app to use Windows authentication by calling AddAuthentication in the ConfigureServices method of the Startup class:
services.AddAuthentication(IISDefaults.AuthenticationScheme);
You should also edit your launchSettings.json file according to the docs:
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
"iisExpress": {
"applicationUrl": "http://localhost:52171/",
"sslPort": 44308
}
}

Related

HTTP Error 500 when deploying ASP.NET Core 2.2 to additional Azure Web App

I have already deployed the application to a Azure web app that works just fine. I am using an ASP.NET Core 2.2 application with MVC connected to a SQL server located on Azure. I am working with dotnet SDK version 3.1.301.
The application has a React client. I am trying to make the application multi - tenancy, therefore, each web app has its own Azure Key Vault but a single database. All of the environment variables are located within the key vault, including the connection string to the db.
I have created an additional web app on Azure (this one is for production) connected to an additional Key Vault. After deploying to this web app, I get a HTTP 500 error. In the web activity logs I do not receive any errors. I am using Microsoft Visual 2019 to deploy the applications.
My question is; I have followed all the same steps for the first deployment and I would really like to know why, when spinning up a new app with a new key vault, why I am not getting any errors?
Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
if (context.HostingEnvironment.IsProduction())
{
var builtConfig = config.Build();
var keyVaultUri = builtConfig.GetValue<string>("KEY_VAULT_URI"); //Connection to KEY VAULT
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
config.AddAzureKeyVault(keyVaultUri, keyVaultClient, new DefaultKeyVaultSecretManager());
}
}
).UseStartup<Startup>();
}
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ILearnerService, LearnerService>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(options =>
// Fixed Unexpected end of JSON
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
services.AddDbContext<DataContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddAutoMapper();
services.AddCors();
// Configure DI for application services
services.AddScoped<ILearnerService, LearnerService>();
// In production, the React files will be served from this directory **********
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
// Configure strongly typed settings objects
var appSettingsSection = Configuration;
services.Configure<AppSettings>(appSettingsSection);
services.AddMultiTenancy()
.WithResolutionStrategy<HostResolutionStrategy>()
.WithStore<InMemoryTenantStore>();
// Configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
var encryptionKey = Encoding.UTF8.GetBytes(appSettings.EncryptionSecret);
services.AddAuthorization(options =>
{
options.AddPolicy(TenantFeatures.Inbox, policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser()
.RequireAssertion(context =>
context.User.HasClaim(TenantFeatures.Inbox, "user.Stores.Companies"))
.Build();
});
});
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var learnerService = context.HttpContext.RequestServices.GetRequiredService<ILearnerService>();
var userId = int.Parse(context.Principal.Identity.Name);
var user = learnerService.GetById(userId);
if (user == null)
{
// Return unauthorized if user no longer exists
context.Fail("Unauthorized");
}
// user.Stores.Companies
var tenantInfo = Configuration.GetValue<string>("tesco-training-net");
if (tenantInfo.Contains(TenantFeatures.Inbox))
{
Claim claim = new Claim(TenantFeatures.Inbox, "user.Stores.Companies");
((ClaimsIdentity)context.Principal.Identity).AddClaim(claim);
}
if (tenantInfo.Contains(TenantFeatures.DynamicCV))
{
Claim claim = new Claim(TenantFeatures.DynamicCV, "user.Stores.Companies");
((ClaimsIdentity)context.Principal.Identity).AddClaim(claim);
}
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
RequireSignedTokens = true,
// Fixed [Authorize] decrypts the tokens
TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"]);
services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"]);
}
// 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())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseMultiTenancy();
app.UseMvc();
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
}
EDIT
Within the Application logs, I am getting this:
IIS was not able to access the web.config file for the Web site or
application. This can occur if the NTFS permissions are set incorrectly.
IIS was not able to process configuration for the Web site or application.
The authenticated user does not have permission to use this DLL. The request is mapped to a managed handler but the .NET Extensibility Feature is not installed. Things you can try: Ensure that the NTFS permissions for the web.config file are correct and allow access to the Web server's machine account. Check the event logs to see if any additional information was logged. Verify the permissions for the DLL. Install the .NET Extensibility feature if the request is mapped to a managed handler. Create a tracing rule to track failed requests for this HTTP status code. For more information about creating a tracing rule for failed requests, click here.

Azure Internal Server Error on ASP.NET Core Web API Controller

I've created a ASP.NET Core Web API Controller with React Js App and it on Azure. After sever try I am able to upload on Azure and now I'm getting error on my API. When I click on Customer it does not give me error but there is no data from SQL Database.
Can someone guide me on How to connect DB to my ASP.NET Core Web API or suggest me where am I doing wrong?
I tried post/add data to customer table but I am getting Internal server Error
Here is sql connection string in my appsetting.json
"ConnectionStrings": {
"DevConnection": "jdbc:sqlserver://aspapireact.database.windows.net:1433;database=ReactTask;user=*****;password=*****;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.windows.net;loginTimeout=30;"}
The Startup.cs
namespace RahulTask1
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.WithOrigins("https://aspapireact.azurewebsites.net")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
services.AddDbContext<DatabaseContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DevConnection")));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
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.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
}
}
And this the API I am trying to call
https://aspapireact.azurewebsites.net/api/Customers
You can see my code on GitHub
https://github.com/rlbrs/ASPAPIReact
In this project you will see the local server connection string but I've updated with above one and same with appserver.json
you can use the configuration builder to the configure services method. To build the key value pair from the appsettings.json on any environment based appsettings file, add following code to the ConfigureServices method (This is not mandatory)
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false , reloadOnChange : true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
And then you read the value for the connection string as
Configuration["ConnectionStrings:DevConnection"]
PS: Any particular reason you are using jdbc connection? why not use the standard dot net based connection string?

401 Error accessing Azure AD protected API with react-adal

I am trying to set up an app with a react front end + a .NET Core back end in Azure with Azure AD Auth. The back end will call other APIs and hold some logic. I set up the .NET Core app and hosted it in an Azure app service, then added authentication using the connected services wizard in visual studio, which generated code similar to what is on this tutorial (back end section):
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
...
}
appsettings.json (fake IDs):
"AzureAd": {
"ClientId": "1fff1098-3bc0-40d9-8cd0-e47b065085b6",
"Domain": "mytenant.onmicrosoft.com",
"Instance": "https://login.microsoftonline.com/",
"TenantId": "mytenantid",
"AppIDURL": "https://my-api.azurewebsites.net/",
"ConfigView": "API"
}
Then I set up react-adal on my front end with:
{
tenant: "mytenant.onmicrosoft.com",
clientId: "1fff1098-3bc0-40d9-8cd0-e47b065085b6",
endpoints: {
api: "1fff1098-3bc0-40d9-8cd0-e47b065085b6"
},
cacheLocation: "localStorage"
};
Which I set up according to the github instructions to set up react-adal. The sign in works as expected but when I run adalApiFetch against my back end, I get a 401 error with description = the signature is invalid. I can see on the debugger that the authorization header (Bearer + token) is sent. Any ideas on what I might be doing wrong here? Thanks in advance!
The endpoint I'm testing with is a simple test controller (with [Authorize]) that simply returns "Authentication Tested".
I was able to find my mistake after a while and came back to post the solution. The problem was that I was using the incorrect method/settings (not matching). From the question's code: If using sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; then you should also use AddJwtBearer with the appropriate configuration options (found here: JwtBearerOptions) and not AddAzureAdBearer. In my case, the final corrected startup code was
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd",options));
....
With corresponding settings (found here: AzureADOptions)

.Net Core 2 WebApi to SQL Server using Active Directory

I am working on a project where I am using .net core 2.
I have a Client app, which calls an Web API, with the API doing communication with the database (SQL Server 2017). As this is an internal (Intranet) app only, we are using AD (Active Directory) as our method of authentication an security of the app.
When working locally the App and DB uses the users details (i.e. name and login details) correctly and are passed through the app and used by the API to talk to the database. The database call then using suser_name to automatically record the users name in a table. We need to know the individual users actions, not use a SQL Server Login which used by the app as a whole.
When the application is moved to the test environment, a web server (IIS8.5) which holds the client and the API in different virtual directories and the database which is on a different server. Both servers are in the same domain (NL-TEST).
When updating the connection string from:
Server=LT017180;Database=Payments; Integrated Security=SSPI;
to Server=testserver;Database=Payments; Integrated Security=SSPI;
it will not connect to the database.
I have confirmed that the user (NL-TEST\Joe_Bloggs for example) is setup in the security section of the database server and has access to the database and is in the NL-TEST domain as a user (I have set it to db_owner, just to try to get it to work).
I have confirmed that the API does have the users credentials. How do I get the credentials to the database so that it logs in as the user and accesses the database?
Example of my code:
Client
Startup
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
Configuration = configuration;
Environment = env;
}
public IConfiguration Configuration { get; }
private IHostingEnvironment Environment { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.Configure<Entities.APIs>(Configuration.GetSection("APIURLs"));
services.AddPolicies(Configuration.GetSection("Policies").Get<Policies>());
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromDays(1);
});
}
// 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())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
API
public IDbConnection Connection => new SqlConnection(_connectionString);
[HttpGet("payment/employee/{id}")]
public IEnumerable<PaymentDTO> GetByEmployeeId(Int64 id)
{
using (IDbConnection dbConnection = Connection)
{
var parameters = new DynamicParameters();
parameters.Add("EmployeeNo", id, DbType.Int64);
dbConnection.Open();
var payments = dbConnection.Query<PaymentDTO>("api.GetPaymentsForEmployeeNo", parameters, commandType: CommandType.StoredProcedure);
dbConnection.Close();
return payments;
}
}
Ajax call to API
var onAjaxSuccess = function (data) {
// do something
};
$.ajax({
headers: {
'Accept': 'application/json'
},
contentType: 'application/json',
url: href,
type: "POST",
cache: false,
error: function (result) {
// record an error
},
success: onAjaxSuccess,
xhrFields: {
withCredentials: true
},
processData: false
});
Controller to API call:
private static readonly HttpClient Client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true});
private static async Task<PaymentDTO> GetPaymentsDetails(Int64 paymentId)
{
var url = new Uri(ApiUrls.PaymentsAPI + "/payments/payment/" + paymentId);
Client.DefaultRequestHeaders.Accept.Clear();
Client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = await Client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var jsonString = await response.Content.ReadAsStringAsync();
var payments = JsonConvert.DeserializeObject<List<PaymentDTO>>(jsonString);
var payment = payments[0];
return payment;
}
return new PaymentDTO();
}
Am I missing a setting or wrapper?
Prior to .NET Core, all you'd have to do is disable Anonymous Authentication, enable Windows Authentication, and enable Impersonation (either via the web.config of the project or if deploying to IIS you can also do it in the Authentication settings of the site.)
.NET Core scrapped Impersonation so this is no longer possible. Without being able to use Impersonation, when deployed to IIS, the application (and connections to the database) are executing as the AppPool user of that site (not the actual user browsing the site, even when Windows Authentication is enabled).
They do offer a somewhat legacy solution for how to impersonate the Windows Authenticated user but it isn't really recommended, especially for heavy units of work. Read on the Impersonation section at the bottom: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-3.1&tabs=visual-studio
Alternative solutions to this problem are:
Develop your own login screen to get a reference to the users credentials and use those to make your database connections (obviously this should be implemented in a secure way)
Implement OAuth authentication

Infinite authentication loop when using identityserver4 in asp.net core 2.0

I have an Identity Server using identityserver4 framework, its url is http://localhost:9000
My web application is asp.net core 2.0, its url is http://localhost:60002. This application will use the login page of Identity Server.
I want after logging in, the Identity Server will redirect to the application page (http://localhost:60002)
Here is the Startup.cs of client application
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
private string AuthorityUri => Configuration.GetValue<string>("UserManagement-Authority");
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = AuthorityUri; // "http://localhost:9000"
options.RequireHttpsMetadata = false;
options.ClientId = "customer.api";
options.ClientSecret = "testsecret";
options.ResponseType = "code id_token";
options.Scope.Add("customerprivatelinesvn.api");
options.Scope.Add("offline_access");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
});
services.AddMvc();
}
// 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())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
HotModuleReplacement = true
});
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
}
}
Here is the loggin page on Identity Server
But there is an infinite loop that calls to http://localhost:9000/connect/authorize endpoint, and then it returns to http://localhost:60002/signin-oidc with "Bad Request - Request Too Long" as below.
When I look at the cookies, there ar lots of items ".AspNetCore.Correlation.OpenIdConnect.xxx"
Here is the log on Identiy Server. It said that Identiy.Application was successfully authenticated.
Does anyone know what this problem is? And how to resolve this? Thank you very much.
Best regards,
Kevin
I also had a login loop after copying the startup code from an existing .NET Core 2.2 project and reused it in a new .NET Core 3.1 project.
The problem here was, that the app.UseAuthentication() must be called before the new app.UseAuthorization();
https://learn.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1&tabs=visual-studio#migrate-startupconfigure
Only in case someone is running into this issue too...
Adding default Identity in the client app would cause an infinite redirect loop.
In the client app, if you need to use UserManager, RoleManager.
Then use the below code.
services.AddIdentityCore<IdentityUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<IdentityUser>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
In your client app, in Startup check if you have something like
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
Remove that part and try again.
In my case, I was missing RedirectUri when initiating the Signin from the client. Problem solved by adding the RedirectUri as below.
public IActionResult SignIn()
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/" }, "oidc" );
}
Well, you do have a very long request shown there in your Identity Server log - and the error says "Bad Request - request too long". I'd guess that the problem is that your request is too big :)
maximum length of HTTP GET request?
Have you tried posting rather than using a GET?
This issue was solved after I updated the latest nuget package of IdentityServer4 and .NET Core.

Resources